-
Notifications
You must be signed in to change notification settings - Fork 391
feat(tanstack-react-start): Introduce middleware and support for TanStack Start RC #6859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 35 commits
1c717f5
443e7ce
c0342c5
3f9b230
793acc5
4f4ee08
14a31e3
5c42683
a0f5987
02a3269
c938089
41fe920
951cc06
1b772e6
d55720c
bf496bc
f77df91
900ac85
01ec8ee
456c86c
86cb369
d8a96ab
2de4a2c
5427d6e
da42e5a
33b064b
52459e0
80a3018
ec71828
5848fa5
8ef7ac6
525dd91
2c5f793
a612233
cc8e86d
40cf986
b971abc
b19fa85
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@clerk/tanstack-react-start": minor | ||
--- | ||
|
||
placeholder trigger test |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { clerkMiddleware } from '@clerk/tanstack-react-start/server'; | ||
import { createStart } from '@tanstack/react-start'; | ||
|
||
export const startInstance = createStart(() => { | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return { | ||
requestMiddleware: [clerkMiddleware()], | ||
}; | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -76,13 +76,13 @@ | |
"tslib": "catalog:repo" | ||
}, | ||
"devDependencies": { | ||
"@tanstack/react-router": "1.131.49", | ||
"@tanstack/react-start": "1.131.49", | ||
"@tanstack/react-router": "1.132.0", | ||
"@tanstack/react-start": "1.132.0", | ||
"esbuild-plugin-file-path-extensions": "^2.1.4" | ||
}, | ||
"peerDependencies": { | ||
"@tanstack/react-router": "^1.131.0 <1.132.0", | ||
"@tanstack/react-start": "^1.131.0 <1.132.0", | ||
"@tanstack/react-router": "^1.132.0", | ||
"@tanstack/react-start": "^1.132.0", | ||
Comment on lines
+84
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Raising the peer dependency floor is a breaking change Moving the peer requirements to 🤖 Prompt for AI Agents
|
||
"react": "catalog:peer-react", | ||
"react-dom": "catalog:peer-react" | ||
}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; | ||
import { ScriptOnce, useRouteContext } from '@tanstack/react-router'; | ||
import { ScriptOnce } from '@tanstack/react-router'; | ||
import { getGlobalStartContext } from '@tanstack/react-start'; | ||
wobsoriano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import { useEffect } from 'react'; | ||
|
||
import { isClient } from '../utils'; | ||
|
@@ -19,15 +20,14 @@ const awaitableNavigateRef: { current: ReturnType<typeof useAwaitableNavigate> | | |
|
||
export function ClerkProvider({ children, ...providerProps }: TanstackStartClerkProviderProps): JSX.Element { | ||
const awaitableNavigate = useAwaitableNavigate(); | ||
const routerContext = useRouteContext({ | ||
strict: false, | ||
}); | ||
// @ts-expect-error: Untyped internal Clerk initial state | ||
const clerkInitialState = getGlobalStartContext()?.clerkInitialState ?? {}; | ||
|
||
useEffect(() => { | ||
awaitableNavigateRef.current = awaitableNavigate; | ||
}, [awaitableNavigate]); | ||
|
||
const clerkInitState = isClient() ? (window as any).__clerk_init_state : routerContext?.clerkInitialState; | ||
const clerkInitState = isClient() ? (window as any).__clerk_init_state : clerkInitialState; | ||
|
||
const { clerkSsrState, ...restInitState } = pickFromClerkInitState(clerkInitState?.__internal_clerk_state); | ||
|
||
|
@@ -38,7 +38,7 @@ export function ClerkProvider({ children, ...providerProps }: TanstackStartClerk | |
|
||
return ( | ||
<> | ||
<ScriptOnce>{`window.__clerk_init_state = ${JSON.stringify(routerContext?.clerkInitialState)};`}</ScriptOnce> | ||
<ScriptOnce>{`window.__clerk_init_state = ${JSON.stringify(clerkInitialState)};`}</ScriptOnce> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escape serialized JSON to avoid XSS/script-breaking sequences. Embedding raw Apply this diff: - <ScriptOnce>{`window.__clerk_init_state = ${JSON.stringify(clerkInitialState)};`}</ScriptOnce>
+ <ScriptOnce>{`window.__clerk_init_state = ${safeSerialize(clerkInitialState)};`}</ScriptOnce> Add this helper near the top of the file (outside the component): // Safe JSON serializer for embedding into inline <script> tags
const safeSerialize = (data: unknown): string =>
JSON.stringify(data)
.replace(/</g, '\\u003c') // avoid </script
.replace(/-->/g, '--\\u003e') // avoid HTML comment close
.replace(/\u2028/g, '\\u2028') // line sep
.replace(/\u2029/g, '\\u2029'); // paragraph sep As per coding guidelines: “Provide meaningful error messages” and “Validate and sanitize outputs.” 🤖 Prompt for AI Agents
|
||
<ClerkOptionsProvider options={mergedProps}> | ||
<ReactClerkProvider | ||
initialState={clerkSsrState} | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import type { RequestState } from '@clerk/backend/internal'; | ||
import { AuthStatus, constants } from '@clerk/backend/internal'; | ||
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler'; | ||
import type { PendingSessionOptions } from '@clerk/types'; | ||
import type { AnyRequestMiddleware } from '@tanstack/react-start'; | ||
import { createMiddleware, json } from '@tanstack/react-start'; | ||
|
||
import { clerkClient } from './clerkClient'; | ||
import { loadOptions } from './loadOptions'; | ||
import type { ClerkMiddlewareOptions } from './types'; | ||
import { getResponseClerkState } from './utils'; | ||
|
||
export const clerkMiddleware = (options?: ClerkMiddlewareOptions): AnyRequestMiddleware => { | ||
return createMiddleware().server(async args => { | ||
const loadedOptions = loadOptions(args.request, options); | ||
const requestState = await clerkClient().authenticateRequest(args.request, { | ||
...loadedOptions, | ||
acceptsToken: 'any', | ||
}); | ||
|
||
const locationHeader = requestState.headers.get(constants.Headers.Location); | ||
if (locationHeader) { | ||
handleNetlifyCacheInDevInstance({ | ||
locationHeader, | ||
requestStateHeaders: requestState.headers, | ||
publishableKey: requestState.publishableKey, | ||
}); | ||
// Trigger a handshake redirect | ||
// eslint-disable-next-line @typescript-eslint/only-throw-error | ||
throw json(null, { status: 307, headers: requestState.headers }); | ||
} | ||
|
||
if (requestState.status === AuthStatus.Handshake) { | ||
throw new Error('Clerk: handshake status without redirect'); | ||
} | ||
|
||
const clerkInitialState = getResponseClerkState(requestState as RequestState, loadedOptions); | ||
|
||
const result = await args.next({ | ||
context: { | ||
clerkInitialState, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be accessed by |
||
auth: (opts?: PendingSessionOptions) => requestState.toAuth(opts), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is for |
||
}, | ||
}); | ||
|
||
if (requestState.headers) { | ||
requestState.headers.forEach((value, key) => { | ||
result.response.headers.append(key, value); | ||
}); | ||
} | ||
|
||
return result; | ||
}); | ||
}; |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,18 @@ | ||
import type { AuthOptions, GetAuthFn } from '@clerk/backend/internal'; | ||
import type { AuthOptions, GetAuthFnNoRequest } from '@clerk/backend/internal'; | ||
import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal'; | ||
import { getContext } from '@tanstack/react-start/server'; | ||
import { getGlobalStartContext } from '@tanstack/react-start'; | ||
|
||
import { errorThrower } from '../utils'; | ||
import { clerkHandlerNotConfigured, noFetchFnCtxPassedInGetAuth } from '../utils/errors'; | ||
import { clerkMiddlewareNotConfigured, noFetchFnCtxPassedInGetAuth } from '../utils/errors'; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
export const getAuth: GetAuthFn<Request, true> = (async (request: Request, opts?: AuthOptions) => { | ||
if (!request) { | ||
return errorThrower.throw(noFetchFnCtxPassedInGetAuth); | ||
} | ||
|
||
const authObjectFn = getContext('auth'); | ||
export const getAuth: GetAuthFnNoRequest<{}, true> = (async (opts?: AuthOptions) => { | ||
// @ts-expect-error: Untyped internal Clerk start context | ||
const authObjectFn = getGlobalStartContext().auth; | ||
Comment on lines
+10
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Replace Suppressing type errors with This refactor is included in the diff above (lines 10-15). 🤖 Prompt for AI Agents
|
||
|
||
if (!authObjectFn) { | ||
return errorThrower.throw(clerkHandlerNotConfigured); | ||
return errorThrower.throw(clerkMiddlewareNotConfigured); | ||
} | ||
wobsoriano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// We're keeping it a promise for now to minimize breaking changes | ||
// We're keeping it a promise for now for future changes | ||
const authObject = await Promise.resolve(authObjectFn({ treatPendingAsSignedOut: opts?.treatPendingAsSignedOut })); | ||
|
||
return getAuthObjectForAcceptedToken({ authObject, acceptsToken: opts?.acceptsToken }); | ||
|
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.