- 
                Notifications
    
You must be signed in to change notification settings  - Fork 402
 
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
Changes from 40 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
              047d256
              d5cf00b
              b2d7f25
              ea5e587
              2555c05
              67e99da
              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,72 @@ | ||
| --- | ||
| "@clerk/tanstack-react-start": minor | ||
| --- | ||
| 
     | 
||
| Added support for [TanStack Start v1 RC](https://tanstack.com/blog/announcing-tanstack-start-v1)! Includes a new `clerkMiddleware()` global middleware replacing the custom server handler. | ||
| 
     | 
||
| Usage: | ||
| 
     | 
||
| 1. Create a `src/start.ts` file and add `clerkMiddleware()` to the list of request middlewares: | ||
| 
     | 
||
| ```ts | ||
| // src/start.ts | ||
| import { clerkMiddleware } from '@clerk/tanstack-react-start/server' | ||
| import { createStart } from '@tanstack/react-start' | ||
| 
     | 
||
| export const startInstance = createStart(() => { | ||
| return { | ||
| requestMiddleware: [clerkMiddleware()], | ||
| } | ||
| }) | ||
| ``` | ||
| 
     | 
||
| 2. Add `<ClerkProvider>` to your root route | ||
| 
     | 
||
| ```tsx | ||
| // src/routes/__root.tsx | ||
| import { ClerkProvider } from '@clerk/tanstack-react-start' | ||
| 
     | 
||
| export const Route = createRootRoute({...}) | ||
| 
     | 
||
| function RootDocument({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <html> | ||
| <head> | ||
| <HeadContent /> | ||
| </head> | ||
| <body> | ||
| <ClerkProvider> | ||
| {children} | ||
| </ClerkProvider> | ||
| <Scripts /> | ||
| </body> | ||
| </html> | ||
| ) | ||
| } | ||
| ``` | ||
| 
     | 
||
| The `getAuth()` helper is now `auth()` and can now be called within server routes and functions, without passing a Request object: | ||
| 
     | 
||
| ```ts | ||
| import { auth } from '@clerk/tanstack-react-start/server' | ||
| 
     | 
||
| const authStateFn = createServerFn().handler(async () => { | ||
| const { userId } = await auth() | ||
| 
     | 
||
| if (!userId) { | ||
| throw redirect({ | ||
| to: '/sign-in', | ||
| }) | ||
| } | ||
| 
     | 
||
| return { userId } | ||
| }) | ||
| 
     | 
||
| export const Route = createFileRoute('/')({ | ||
| component: Home, | ||
| beforeLoad: async () => await authStateFn(), | ||
| loader: async ({ context }) => { | ||
| return { userId: context.userId } | ||
| }, | ||
| }) | ||
| ``` | 
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 sepAs per coding guidelines: “Provide meaningful error messages” and “Validate and sanitize outputs.” 🤖 Prompt for AI Agents | 
||
| <ClerkOptionsProvider options={mergedProps}> | ||
| <ReactClerkProvider | ||
| initialState={clerkSsrState} | ||
| 
          
            
          
           | 
    ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import type { SessionAuthObject } from '@clerk/backend'; | ||
| import type { AuthOptions, GetAuthFnNoRequest } from '@clerk/backend/internal'; | ||
| import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal'; | ||
| import { getGlobalStartContext } from '@tanstack/react-start'; | ||
| 
     | 
||
| import { errorThrower } from '../utils'; | ||
| import { clerkMiddlewareNotConfigured } from '../utils/errors'; | ||
| 
     | 
||
| export const auth: GetAuthFnNoRequest<SessionAuthObject, true> = (async (opts?: AuthOptions) => { | ||
| // @ts-expect-error: Untyped internal Clerk start context | ||
| const authObjectFn = getGlobalStartContext().auth; | ||
| 
     | 
||
| if (!authObjectFn) { | ||
| return errorThrower.throw(clerkMiddlewareNotConfigured); | ||
| } | ||
| 
     | 
||
| // 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 }); | ||
| }) as GetAuthFnNoRequest<SessionAuthObject, true>; | 
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.
Uh oh!
There was an error while loading. Please reload this page.