diff --git a/.changeset/gorgeous-suits-rush.md b/.changeset/gorgeous-suits-rush.md new file mode 100644 index 0000000000..9cca69bf36 --- /dev/null +++ b/.changeset/gorgeous-suits-rush.md @@ -0,0 +1,5 @@ +--- +"@clerk/nextjs": major +--- + +Stop `` from opting applications into dynamic rendering. A new prop, `` can be used to opt-in to dynamic rendering and make auth data available during server-side rendering. The RSC `auth()` helper should be preferred for accessing auth data during dynamic rendering. diff --git a/.changeset/grumpy-hairs-remember.md b/.changeset/grumpy-hairs-remember.md new file mode 100644 index 0000000000..e85253fe2e --- /dev/null +++ b/.changeset/grumpy-hairs-remember.md @@ -0,0 +1,90 @@ +--- +"@clerk/nextjs": major +"@clerk/upgrade": minor +--- + +@clerk/nextjs: Converting auth() and clerkClient() interfaces to be async +@clerk/upgrade: Adding required codemod for @clerk/nextjs breaking changes + +# Migration guide + +## `auth()` is now async + +Previously the `auth()` method from `@clerk/nextjs/server` was synchronous. + +```typescript +import { auth } from '@clerk/nextjs/server'; + +export function GET() { + const { userId } = auth(); + return new Response(JSON.stringify({ userId })); +} +``` + +The `auth` method now becomes asynchronous. You will need to make the following changes to the snippet above to make it compatible. + +```diff +- export function GET() { ++ export async function GET() { +- const { userId } = auth(); ++ const { userId } = await auth(); + return new Response(JSON.stringify({ userId })); +} +``` + +## Clerk middleware auth is now async + +```typescript +import { clerkClient, clerkMiddleware } from '@clerk/nextjs/server'; +import { NextResponse } from 'next/server'; + +export default clerkMiddleware(async (auth, request) => { + const resolvedAuth = await auth(); + + const count = await resolvedAuth.users.getCount(); + + if (count) { + return NextResponse.redirect(new URL('/new-url', request.url)); + } +}); + +export const config = { + matcher: [...], +}; +``` + +## clerkClient() is now async + +Previously the `clerkClient()` method from `@clerk/nextjs/server` was synchronous. + +```typescript +import { clerkClient, clerkMiddleware } from '@clerk/nextjs/server'; +import { NextResponse } from 'next/server'; + +export default clerkMiddleware((auth, request) => { + const client = clerkClient(); + + const count = await client.users?.getCount(); + + if (count) { + return NextResponse.redirect(new URL('/new-url', request.url)); + } +}); + +export const config = { + matcher: [...], +}; +``` + +The method now becomes async. You will need to make the following changes to the snippet above to make it compatible. + +```diff +- export default clerkMiddleware((auth, request) => { +- const client = clerkClient(); ++ export default clerkMiddleware(async (auth, request) => { ++ const client = await clerkClient(); + const count = await client.users?.getCount(); + + if (count) { +} +``` diff --git a/.changeset/shiny-numbers-walk.md b/.changeset/shiny-numbers-walk.md new file mode 100644 index 0000000000..3ffc12abb8 --- /dev/null +++ b/.changeset/shiny-numbers-walk.md @@ -0,0 +1,6 @@ +--- +"@clerk/nextjs": major +--- + +Support `unstable_rethrow` inside `clerkMiddleware`. +We changed the errors thrown by `protect()` inside `clerkMiddleware` in order for `unstable_rethrow` to recognise them and rethrow them. diff --git a/.changeset/ten-worms-report.md b/.changeset/ten-worms-report.md new file mode 100644 index 0000000000..713f10b52d --- /dev/null +++ b/.changeset/ten-worms-report.md @@ -0,0 +1,5 @@ +--- +"@clerk/nextjs": major +--- + +Removes deprecated APIs: `authMiddleware()`, `redirectToSignIn()`, and `redirectToSignUp()`. See the migration guide to learn how to update your usage. diff --git a/.changeset/two-bottles-report.md b/.changeset/two-bottles-report.md new file mode 100644 index 0000000000..aecebdc39c --- /dev/null +++ b/.changeset/two-bottles-report.md @@ -0,0 +1,5 @@ +--- +"@clerk/clerk-react": minor +--- + +Internal changes to support `` diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index eee5510608..2db94271d5 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -23,12 +23,14 @@ export const createLongRunningApps = () => { { id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks }, { id: 'remix.node.withEmailCodes', config: remix.remixNode, env: envs.withEmailCodes }, { id: 'next.appRouter.withEmailCodes', config: next.appRouter, env: envs.withEmailCodes }, + { id: 'next.appRouter.15RCwithEmailCodes', config: next.appRouter15Rc, env: envs.withEmailCodes }, { id: 'next.appRouter.withEmailCodes_persist_client', config: next.appRouter, env: envs.withEmailCodes_destroy_client, }, { id: 'next.appRouter.withCustomRoles', config: next.appRouter, env: envs.withCustomRoles }, + { id: 'next.appRouter.15RCwithCustomRoles', config: next.appRouter15Rc, env: envs.withCustomRoles }, { id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart }, { id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes }, { id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles }, diff --git a/integration/presets/next.ts b/integration/presets/next.ts index 68366b7ffa..f0a66b5802 100644 --- a/integration/presets/next.ts +++ b/integration/presets/next.ts @@ -16,6 +16,60 @@ const appRouter = applicationConfig() .addDependency('react-dom', constants.E2E_REACT_DOM_VERSION) .addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal); +const appRouter15Rc = applicationConfig() + .setName('next-app-router') + .useTemplate(templates['next-app-router']) + .setEnvFormatter('public', key => `NEXT_PUBLIC_${key}`) + .addScript('setup', constants.E2E_NPM_FORCE ? 'npm i --force' : 'npm i') + .addScript('dev', 'npm run dev') + .addScript('build', 'npm run build') + .addScript('serve', 'npm run start') + .addDependency('next', 'rc') + .addDependency('react', 'rc') + .addDependency('react-dom', 'rc') + .addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal) + .addFile( + 'src/middleware.ts', + () => `import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; +import { unstable_rethrow } from 'next/navigation'; + +const csp = \`default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' 'nonce-deadbeef'; + img-src 'self' https://img.clerk.com; + worker-src 'self' blob:; + style-src 'self' 'unsafe-inline'; + frame-src 'self' https://challenges.cloudflare.com; +\`; + +const isProtectedRoute = createRouteMatcher(['/protected(.*)', '/user(.*)', '/switcher(.*)']); +const isAdminRoute = createRouteMatcher(['/only-admin(.*)']); +const isCSPRoute = createRouteMatcher(['/csp']); + +export default clerkMiddleware(async (auth, req) => { + if (isProtectedRoute(req)) { + try { await auth.protect() } + catch (e) { unstable_rethrow(e) } + } + + if (isAdminRoute(req)) { + try { await auth.protect({role: 'admin'}) } + catch (e) { unstable_rethrow(e) } + } + + if (isCSPRoute(req)) { + req.headers.set('Content-Security-Policy', csp.replace(/\\n/g, '')); + } +}); + +export const config = { + matcher: [ + '/((?!.*\\\\..*|_next).*)', // Don't run middleware on static files + '/', // Run middleware on index page + '/(api|trpc)(.*)', + ], // Run middleware on API routes +};`, + ); + const appRouterTurbo = appRouter .clone() .setName('next-app-router-turbopack') @@ -48,6 +102,7 @@ const appRouterAPWithClerkNextV4 = appRouterQuickstart export const next = { appRouter, + appRouter15Rc, appRouterTurbo, appRouterQuickstart, appRouterAPWithClerkNextLatest, diff --git a/integration/templates/next-app-router/src/app/api/me/route.ts b/integration/templates/next-app-router/src/app/api/me/route.ts index c6088c6a97..8ae059dff0 100644 --- a/integration/templates/next-app-router/src/app/api/me/route.ts +++ b/integration/templates/next-app-router/src/app/api/me/route.ts @@ -1,6 +1,6 @@ import { auth } from '@clerk/nextjs/server'; -export function GET() { - const { userId } = auth(); +export async function GET() { + const { userId } = await auth(); return new Response(JSON.stringify({ userId })); } diff --git a/integration/templates/next-app-router/src/app/api/settings/route.ts b/integration/templates/next-app-router/src/app/api/settings/route.ts index 62553e05ef..8e6d46017e 100644 --- a/integration/templates/next-app-router/src/app/api/settings/route.ts +++ b/integration/templates/next-app-router/src/app/api/settings/route.ts @@ -1,6 +1,6 @@ import { auth } from '@clerk/nextjs/server'; -export function GET() { - const { userId } = auth().protect(has => has({ role: 'admin' }) || has({ role: 'org:editor' })); +export async function GET() { + const { userId } = await auth.protect((has: any) => has({ role: 'admin' }) || has({ role: 'org:editor' })); return new Response(JSON.stringify({ userId })); } diff --git a/integration/templates/next-app-router/src/app/csp/page.tsx b/integration/templates/next-app-router/src/app/csp/page.tsx index 803218da79..fec6f7b09c 100644 --- a/integration/templates/next-app-router/src/app/csp/page.tsx +++ b/integration/templates/next-app-router/src/app/csp/page.tsx @@ -1,10 +1,12 @@ import { headers } from 'next/headers'; import { ClerkLoaded } from '@clerk/nextjs'; -export default function CSPPage() { +export default async function CSPPage() { + const cspHeader = await headers().get('Content-Security-Policy'); + return (
- CSP:
{headers().get('Content-Security-Policy')}
+ CSP:
{cspHeader}

clerk loaded

diff --git a/integration/templates/next-app-router/src/app/only-admin/page.tsx b/integration/templates/next-app-router/src/app/only-admin/page.tsx new file mode 100644 index 0000000000..a2817c4a6d --- /dev/null +++ b/integration/templates/next-app-router/src/app/only-admin/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
User is admin
; +} diff --git a/integration/templates/next-app-router/src/app/organizations-by-id/[id]/page.tsx b/integration/templates/next-app-router/src/app/organizations-by-id/[id]/page.tsx index 41eb746d5e..794d8c6b2a 100644 --- a/integration/templates/next-app-router/src/app/organizations-by-id/[id]/page.tsx +++ b/integration/templates/next-app-router/src/app/organizations-by-id/[id]/page.tsx @@ -1,7 +1,7 @@ import { auth } from '@clerk/nextjs/server'; -export default function Home({ params }: { params: { id: string } }) { - const { orgId } = auth(); +export default async function Home({ params }: { params: { id: string } }) { + const { orgId } = await auth(); if (params.id != orgId) { console.log('Mismatch - returning nothing for now...', params.id, orgId); diff --git a/integration/templates/next-app-router/src/app/organizations-by-id/[id]/settings/page.tsx b/integration/templates/next-app-router/src/app/organizations-by-id/[id]/settings/page.tsx index 8d1adf2e24..f99a9eb4cd 100644 --- a/integration/templates/next-app-router/src/app/organizations-by-id/[id]/settings/page.tsx +++ b/integration/templates/next-app-router/src/app/organizations-by-id/[id]/settings/page.tsx @@ -1,7 +1,7 @@ import { auth } from '@clerk/nextjs/server'; -export default function Home({ params }: { params: { id: string } }) { - const { orgId } = auth(); +export default async function Home({ params }: { params: { id: string } }) { + const { orgId } = await auth(); if (params.id != orgId) { console.log('Mismatch - returning nothing for now...', params.id, orgId); diff --git a/integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/page.tsx b/integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/page.tsx index 1847d88f18..fea8138ea7 100644 --- a/integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/page.tsx +++ b/integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/page.tsx @@ -1,7 +1,7 @@ import { auth } from '@clerk/nextjs/server'; -export default function Home({ params }: { params: { slug: string } }) { - const { orgSlug } = auth(); +export default async function Home({ params }: { params: { slug: string } }) { + const { orgSlug } = await auth(); if (params.slug != orgSlug) { console.log('Mismatch - returning nothing for now...', params.slug, orgSlug); diff --git a/integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/settings/page.tsx b/integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/settings/page.tsx index f2613fdbcc..7cfcdbd93a 100644 --- a/integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/settings/page.tsx +++ b/integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/settings/page.tsx @@ -1,7 +1,7 @@ import { auth } from '@clerk/nextjs/server'; -export default function Home({ params }: { params: { slug: string } }) { - const { orgSlug } = auth(); +export default async function Home({ params }: { params: { slug: string } }) { + const { orgSlug } = await auth(); if (params.slug != orgSlug) { console.log('Mismatch - returning nothing for now...', params.slug, orgSlug); diff --git a/integration/templates/next-app-router/src/app/page-protected/page.tsx b/integration/templates/next-app-router/src/app/page-protected/page.tsx index d67679ce80..0cce9cdcd9 100644 --- a/integration/templates/next-app-router/src/app/page-protected/page.tsx +++ b/integration/templates/next-app-router/src/app/page-protected/page.tsx @@ -1,6 +1,7 @@ import { auth } from '@clerk/nextjs/server'; -export default function Page() { - auth().protect(); +export default async function Page() { + await auth.protect(); + return
Protected Page
; } diff --git a/integration/templates/next-app-router/src/app/personal-account/page.tsx b/integration/templates/next-app-router/src/app/personal-account/page.tsx index fdd6a1460d..39dc9da1da 100644 --- a/integration/templates/next-app-router/src/app/personal-account/page.tsx +++ b/integration/templates/next-app-router/src/app/personal-account/page.tsx @@ -1,7 +1,7 @@ import { auth } from '@clerk/nextjs/server'; -export default function Home(): {} { - const { orgId } = auth(); +export default async function Home() { + const { orgId } = await auth(); if (orgId != null) { console.log('Oh no, this page should only activate on the personal account!'); diff --git a/integration/templates/next-app-router/src/app/settings/auth-has/page.tsx b/integration/templates/next-app-router/src/app/settings/auth-has/page.tsx index a036ccfc7c..b1363c4972 100644 --- a/integration/templates/next-app-router/src/app/settings/auth-has/page.tsx +++ b/integration/templates/next-app-router/src/app/settings/auth-has/page.tsx @@ -1,7 +1,7 @@ import { auth } from '@clerk/nextjs/server'; -export default function Page() { - const { userId, has } = auth(); +export default async function Page() { + const { userId, has } = await auth(); if (!userId || !has({ permission: 'org:posts:manage' })) { return

User is missing permissions

; } diff --git a/integration/templates/next-app-router/src/app/settings/auth-protect/page.tsx b/integration/templates/next-app-router/src/app/settings/auth-protect/page.tsx index cddf0f7d50..c7f9b76b51 100644 --- a/integration/templates/next-app-router/src/app/settings/auth-protect/page.tsx +++ b/integration/templates/next-app-router/src/app/settings/auth-protect/page.tsx @@ -1,6 +1,6 @@ import { auth } from '@clerk/nextjs/server'; -export default function Page() { - auth().protect({ role: 'admin' }); +export default async function Page() { + await auth.protect({ role: 'admin' }); return

User has access

; } diff --git a/integration/templates/next-app-router/src/middleware.ts b/integration/templates/next-app-router/src/middleware.ts index 2deaf8df80..24c94de0e8 100644 --- a/integration/templates/next-app-router/src/middleware.ts +++ b/integration/templates/next-app-router/src/middleware.ts @@ -9,11 +9,16 @@ const csp = `default-src 'self'; `; const isProtectedRoute = createRouteMatcher(['/protected(.*)', '/user(.*)', '/switcher(.*)']); +const isAdminRoute = createRouteMatcher(['/only-admin(.*)']); const isCSPRoute = createRouteMatcher(['/csp']); -export default clerkMiddleware((auth, req) => { +export default clerkMiddleware(async (auth, req) => { if (isProtectedRoute(req)) { - auth().protect(); + await auth.protect(); + } + + if (isAdminRoute(req)) { + await auth.protect({ role: 'admin' }); } if (isCSPRoute(req)) { diff --git a/integration/tests/dynamic-keys.test.ts b/integration/tests/dynamic-keys.test.ts index e034146c64..832dfa95ad 100644 --- a/integration/tests/dynamic-keys.test.ts +++ b/integration/tests/dynamic-keys.test.ts @@ -13,22 +13,24 @@ test.describe('dynamic keys @nextjs', () => { .clone() .addFile( 'src/middleware.ts', - () => `import { clerkClient, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' - import { NextResponse } from 'next/server' + () => `import { clerkClient, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; + import { NextResponse } from 'next/server'; const isProtectedRoute = createRouteMatcher(['/protected']); const shouldFetchBapi = createRouteMatcher(['/fetch-bapi-from-middleware']); export default clerkMiddleware(async (auth, request) => { if (isProtectedRoute(request)) { - auth().protect(); + await auth.protect(); } if (shouldFetchBapi(request)){ - const count = await clerkClient().users.getCount(); + const client = await clerkClient(); + + const count = await client.users?.getCount(); if (count){ - return NextResponse.redirect(new URL('/users-count', request.url)) + return NextResponse.redirect(new URL('/users-count', request.url)); } } }, { @@ -45,7 +47,7 @@ test.describe('dynamic keys @nextjs', () => { () => `import { clerkClient } from '@clerk/nextjs/server' export default async function Page(){ - const count = await clerkClient().users.getCount() + const count = await clerkClient().users?.getCount() ?? 0; return

Users count: {count}

} @@ -62,7 +64,7 @@ test.describe('dynamic keys @nextjs', () => { await app.teardown(); }); - test('redirects to `signInUrl` on `auth().protect()`', async ({ page, context }) => { + test('redirects to `signInUrl` on `await auth.protect()`', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.page.goToAppHome(); @@ -74,7 +76,7 @@ test.describe('dynamic keys @nextjs', () => { await u.page.waitForURL(/foobar/); }); - test('resolves auth signature with `secretKey` on `auth().protect()`', async ({ page, context }) => { + test('resolves auth signature with `secretKey` on `await auth.protect()`', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.page.goToRelative('/page-protected'); await u.page.waitForURL(/foobar/); diff --git a/integration/tests/handshake.test.ts b/integration/tests/handshake.test.ts index 29b561cb6d..b6837ae189 100644 --- a/integration/tests/handshake.test.ts +++ b/integration/tests/handshake.test.ts @@ -39,14 +39,10 @@ test.describe('Client handshake @generic', () => { .clone() .addFile( 'src/middleware.ts', - () => `import { authMiddleware } from '@clerk/nextjs/server'; - - // Set the paths that don't require the user to be signed in - const publicPaths = ['/', /^(\\/(sign-in|sign-up|app-dir|custom)\\/*).*$/]; + () => `import { clerkMiddleware } from '@clerk/nextjs/server'; export const middleware = (req, evt) => { - return authMiddleware({ - publicRoutes: publicPaths, + return clerkMiddleware({ publishableKey: req.headers.get("x-publishable-key"), secretKey: req.headers.get("x-secret-key"), proxyUrl: req.headers.get("x-proxy-url"), @@ -1256,6 +1252,12 @@ test.describe('Client handshake with organization activation @nextjs', () => { redirect: 'manual', }); + if (testCase.name === 'Header-based auth should not handshake with expired auth') { + console.log(testCase.name); + console.log(res.headers.get('x-clerk-auth-status')); + console.log(res.headers.get('x-clerk-auth-reason')); + } + expect(res.status).toBe(testCase.then.expectStatus); const redirectSearchParams = new URLSearchParams(res.headers.get('location')); expect(redirectSearchParams.get('organization_id')).toBe(testCase.then.fapiOrganizationIdParamValue); @@ -1373,14 +1375,17 @@ test.describe('Client handshake with an organization activation avoids infinite */ const startAppWithOrganizationSyncOptions = async (clerkAPIUrl: string): Promise => { const env = appConfigs.envs.withEmailCodes.clone().setEnvVariable('private', 'CLERK_API_URL', clerkAPIUrl); + const middlewareFile = `import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; + + const isProtectedRoute = createRouteMatcher(['/organizations(.*)']) - const middlewareFile = `import { authMiddleware } from '@clerk/nextjs/server'; - // Set the paths that don't require the user to be signed in - const publicPaths = ['/', /^(\\/(sign-in|sign-up|app-dir|custom)\\/*).*$/]; export const middleware = (req, evt) => { const orgSyncOptions = req.headers.get("x-organization-sync-options") - return authMiddleware({ - publicRoutes: publicPaths, + return clerkMiddleware((auth, req) => { + if (isProtectedRoute(req) && !auth().userId) { + auth().redirectToSignIn() + } + }, { publishableKey: req.headers.get("x-publishable-key"), secretKey: req.headers.get("x-secret-key"), proxyUrl: req.headers.get("x-proxy-url"), diff --git a/integration/tests/next-middleware.test.ts b/integration/tests/next-middleware.test.ts deleted file mode 100644 index 3bf7c2eb74..0000000000 --- a/integration/tests/next-middleware.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import type { Application } from '../models/application'; -import { appConfigs } from '../presets'; -import { createTestUtils } from '../testUtils'; - -test.describe('next middleware @nextjs', () => { - test.describe.configure({ mode: 'parallel' }); - let app: Application; - - test.beforeAll(async () => { - const cookieExpires = new Date().getTime() + 60 * 60 * 24; - app = await appConfigs.next.appRouter - .clone() - .addFile( - 'src/middleware.ts', - () => `import { authMiddleware } from '@clerk/nextjs/server'; -import { NextResponse } from "next/server"; - -export default authMiddleware({ - publicRoutes: ['/', '/hash/sign-in', '/hash/sign-up'], - afterAuth: async (auth, req) => { - const response = NextResponse.next(); - response.cookies.set({ - name: "first", - value: "123456789", - sameSite: "Lax", - path: "/", - domain: 'localhost', - secure: false, - expires: ${cookieExpires} - }); - response.cookies.set("second", "987654321", { - sameSite: "Lax", - secure: false, - path: "/", - domain: 'localhost', - expires: ${cookieExpires} - }); - response.cookies.set("third", "foobar", { - sameSite: "Lax", - secure: false, - path: "/", - domain: 'localhost', - expires: ${cookieExpires} - }); - return response; - }, -}); - -export const config = { - matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], -};`, - ) - .addFile( - 'src/app/provider.tsx', - () => `'use client' -import { ClerkProvider } from "@clerk/nextjs" - -export function Provider({ children }: { children: any }) { - return ( - - {children} - - ) -}`, - ) - .addFile( - 'src/app/layout.tsx', - () => `import './globals.css'; -import { Inter } from 'next/font/google'; -import { Provider } from './provider'; - -const inter = Inter({ subsets: ['latin'] }); - -export const metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - - ); -} - `, - ) - .commit(); - await app.setup(); - await app.withEnv(appConfigs.envs.withEmailCodes); - await app.dev(); - }); - - test.afterAll(async () => { - await app.teardown(); - }); - - test('authMiddleware passes through all cookies', async ({ browser }) => { - // See https://playwright.dev/docs/api/class-browsercontext - const context = await browser.newContext(); - const page = await context.newPage(); - const u = createTestUtils({ app, page }); - - await page.goto(app.serverUrl); - await u.po.signIn.waitForMounted(); - - const cookies = await context.cookies(); - - expect(cookies.find(c => c.name == 'first').value).toBe('123456789'); - expect(cookies.find(c => c.name == 'second').value).toBe('987654321'); - expect(cookies.find(c => c.name == 'third').value).toBe('foobar'); - - await context.close(); - }); -}); diff --git a/integration/tests/protect.test.ts b/integration/tests/protect.test.ts index 7b2c6868d2..fc5a5bc143 100644 --- a/integration/tests/protect.test.ts +++ b/integration/tests/protect.test.ts @@ -56,6 +56,9 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('authoriz await u.page.goToRelative('/settings/auth-protect'); await expect(u.page.getByText(/User has access/i)).toBeVisible(); + await u.page.goToRelative('/only-admin'); + await expect(u.page.getByText(/User is admin/i)).toBeVisible(); + // route handler await u.page.goToRelative('/api/settings/'); await expect(u.page.getByText(/userId/i)).toBeVisible(); @@ -89,6 +92,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('authoriz await u.po.signIn.waitForMounted(); await u.page.goToRelative('/page-protected'); await u.po.signIn.waitForMounted(); + await u.page.goToRelative('/only-admin'); + await u.po.signIn.waitForMounted(); }); test('Protect in RSCs and RCCs as `viewer`', async ({ page, context }) => { @@ -114,6 +119,9 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('authoriz await u.page.goToRelative('/settings/auth-protect'); await expect(u.page.getByText(/this page could not be found/i)).toBeVisible(); + await u.page.goToRelative('/only-admin'); + await expect(u.page.getByText(/this page could not be found/i)).toBeVisible(); + // Route Handler await u.page.goToRelative('/api/settings/').catch(() => {}); diff --git a/package-lock.json b/package-lock.json index cb1aac77c7..301f92b2d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2489,42 +2489,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/register/node_modules/find-cache-dir": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/find-up": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/locate-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/register/node_modules/make-dir": { "version": "2.1.0", "dev": true, @@ -2537,25 +2501,6 @@ "node": ">=6" } }, - "node_modules/@babel/register/node_modules/p-locate": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/path-exists": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/register/node_modules/pify": { "version": "4.0.1", "dev": true, @@ -2564,17 +2509,6 @@ "node": ">=6" } }, - "node_modules/@babel/register/node_modules/pkg-dir": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/register/node_modules/semver": { "version": "5.7.2", "dev": true, @@ -7203,66 +7137,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@inkjs/ui": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "chalk": "^5.2.0", - "cli-spinners": "^2.9.0", - "deepmerge": "^4.3.1", - "figures": "^5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "peerDependencies": { - "ink": "^4.2.0" - } - }, - "node_modules/@inkjs/ui/node_modules/chalk": { - "version": "5.3.0", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@inkjs/ui/node_modules/escape-string-regexp": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@inkjs/ui/node_modules/figures": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^5.0.0", - "is-unicode-supported": "^1.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@inkjs/ui/node_modules/is-unicode-supported": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@internationalized/date": { "version": "3.5.4", "license": "Apache-2.0", @@ -12991,6 +12865,12 @@ "version": "1.5.1", "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@segment/loosely-validate-event": { "version": "2.0.0", "dev": true, @@ -13394,9 +13274,10 @@ } }, "node_modules/@tanstack/react-cross-context": { - "version": "1.60.0", + "version": "1.74.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-cross-context/-/react-cross-context-1.74.5.tgz", + "integrity": "sha512-a4BoKe1umpt4mmol2fUc7S11ilYIrn60ysoLot0NE+BeRsML83MvgPsLTAx7h1fTp0gmLjtpMjjubIJ8GlDIQg==", "dev": true, - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -13407,9 +13288,10 @@ } }, "node_modules/@tanstack/react-store": { - "version": "0.5.5", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.5.6.tgz", + "integrity": "sha512-SitIpS5jTj28DajjLpWbIX+YetmJL+6PRY0DKKiCGBKfYIqj3ryODQYF3jB3SNoR9ifUA/jFkqbJdBKFtWd+AQ==", "dev": true, - "license": "MIT", "dependencies": { "@tanstack/store": "0.5.5", "use-sync-external-store": "^1.2.2" @@ -13966,6 +13848,56 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jscodeshift": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/jscodeshift/-/jscodeshift-0.12.0.tgz", + "integrity": "sha512-Jr2fQbEoDmjwEa92TreR/mX2t9iAaY/l5P/GKezvK4BodXahex60PDLXaQR0vAgP0KfCzc1CivHusQB9NhzX8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.14.1", + "recast": "^0.20.3" + } + }, + "node_modules/@types/jscodeshift/node_modules/ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@types/jscodeshift/node_modules/recast": { + "version": "0.20.5", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.20.5.tgz", + "integrity": "sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "0.14.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@types/jscodeshift/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@types/jsdom": { "version": "20.0.1", "dev": true, @@ -15378,6 +15310,47 @@ "node": ">= 14.16" } }, + "node_modules/@vitest/mocker": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.3.tgz", + "integrity": "sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.3", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.3", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/@vitest/spy": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.3.tgz", + "integrity": "sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vitest/pretty-format": { "version": "2.0.5", "dev": true, @@ -17087,6 +17060,18 @@ "node": ">=8.0.0" } }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "license": "MIT", @@ -22308,7 +22293,6 @@ }, "node_modules/environment": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -25262,6 +25246,109 @@ "node": ">= 0.8" } }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/find-cache-dir/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/find-root": { "version": "1.1.0", "license": "MIT" @@ -25652,7 +25739,6 @@ "node_modules/get-east-asian-width": { "version": "1.2.0", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -26978,6 +27064,7 @@ "node_modules/ink": { "version": "4.4.1", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^6.0.0", @@ -27082,81 +27169,10 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/ink-link": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "prop-types": "^15.8.1", - "terminal-link": "^3.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "peerDependencies": { - "ink": ">=4" - } - }, - "node_modules/ink-link/node_modules/ansi-escapes": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "type-fest": "^1.0.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ink-link/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ink-link/node_modules/supports-hyperlinks": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ink-link/node_modules/terminal-link": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^5.0.0", - "supports-hyperlinks": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ink-link/node_modules/type-fest": { - "version": "1.4.0", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ink/node_modules/ansi-escapes": { "version": "6.2.1", "license": "MIT", + "peer": true, "engines": { "node": ">=14.16" }, @@ -27167,6 +27183,7 @@ "node_modules/ink/node_modules/ansi-regex": { "version": "6.0.1", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -27177,6 +27194,7 @@ "node_modules/ink/node_modules/ansi-styles": { "version": "6.2.1", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -27184,19 +27202,10 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ink/node_modules/auto-bind": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ink/node_modules/chalk": { "version": "5.3.0", "license": "MIT", + "peer": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -27207,6 +27216,7 @@ "node_modules/ink/node_modules/cli-cursor": { "version": "4.0.0", "license": "MIT", + "peer": true, "dependencies": { "restore-cursor": "^4.0.0" }, @@ -27220,6 +27230,7 @@ "node_modules/ink/node_modules/indent-string": { "version": "5.0.0", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -27230,6 +27241,7 @@ "node_modules/ink/node_modules/restore-cursor": { "version": "4.0.0", "license": "MIT", + "peer": true, "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -27243,11 +27255,13 @@ }, "node_modules/ink/node_modules/signal-exit": { "version": "3.0.7", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/ink/node_modules/slice-ansi": { "version": "6.0.0", "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" @@ -27262,6 +27276,7 @@ "node_modules/ink/node_modules/string-width": { "version": "5.1.2", "license": "MIT", + "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -27277,6 +27292,7 @@ "node_modules/ink/node_modules/strip-ansi": { "version": "7.1.0", "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -27290,6 +27306,7 @@ "node_modules/ink/node_modules/type-fest": { "version": "0.12.0", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -27300,6 +27317,7 @@ "node_modules/ink/node_modules/wrap-ansi": { "version": "8.1.0", "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -27679,6 +27697,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ci": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-0.1.0.tgz", + "integrity": "sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "license": "MIT", @@ -27792,6 +27825,7 @@ "node_modules/is-lower-case": { "version": "2.0.2", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.0.3" } @@ -28022,6 +28056,7 @@ "node_modules/is-upper-case": { "version": "2.0.2", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.0.3" } @@ -39847,8 +39882,7 @@ }, "node_modules/tinyexec": { "version": "0.3.0", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tinygradient": { "version": "1.1.5", @@ -43265,6 +43299,19 @@ "version": "1.0.2", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/ws": { "version": "8.17.1", "license": "MIT", @@ -43442,6 +43489,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoga-wasm-web": { "version": "0.3.3", "license": "MIT" @@ -44604,28 +44663,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "packages/dev-cli/node_modules/find-cache-dir": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "packages/dev-cli/node_modules/find-up": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "packages/dev-cli/node_modules/globby": { "version": "14.0.2", "license": "MIT", @@ -44687,17 +44724,6 @@ } } }, - "packages/dev-cli/node_modules/locate-path": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "packages/dev-cli/node_modules/make-dir": { "version": "2.1.0", "license": "MIT", @@ -44720,23 +44746,6 @@ "node": ">=8.6" } }, - "packages/dev-cli/node_modules/p-locate": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "packages/dev-cli/node_modules/path-exists": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "packages/dev-cli/node_modules/path-type": { "version": "5.0.0", "license": "MIT", @@ -44754,16 +44763,6 @@ "node": ">=6" } }, - "packages/dev-cli/node_modules/pkg-dir": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "packages/dev-cli/node_modules/recast": { "version": "0.23.9", "license": "MIT", @@ -44849,17 +44848,6 @@ "node": ">=6.0.0" } }, - "packages/dev-cli/node_modules/write-file-atomic": { - "version": "5.0.1", - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "packages/elements": { "name": "@clerk/elements", "version": "0.16.2", @@ -48265,16 +48253,18 @@ "version": "1.0.9", "license": "MIT", "dependencies": { - "@inkjs/ui": "^1.0.0", + "@inkjs/ui": "^2.0.0", "@jescalan/ink-markdown": "^2.0.0", "ejs": "3.1.10", + "execa": "9.4.1", "globby": "^14.0.1", "gray-matter": "^4.0.3", "index-to-position": "^0.1.2", - "ink": "^4.4.1", + "ink": "^5.0.1", "ink-big-text": "^2.0.0", "ink-gradient": "^3.0.0", - "ink-link": "^3.0.0", + "ink-link": "^4.1.0", + "jscodeshift": "^17.0.0", "marked": "^11.1.1", "meow": "^11.0.0", "react": "^18.3.1", @@ -48288,14 +48278,225 @@ "devDependencies": { "@babel/cli": "^7.24.7", "@babel/preset-react": "^7.24.7", - "chalk": "^5.3.0", + "@types/jscodeshift": "^0.12.0", "del-cli": "^5.1.0", - "eslint-config-custom": "*" + "eslint-config-custom": "*", + "vitest": "^2.1.3" + }, + "engines": { + "node": ">=18.17.0" + } + }, + "packages/upgrade/.yalc/@clerk/upgrade": { + "version": "1.0.9", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@clerk/upgrade": "file:.yalc/@clerk/upgrade", + "@inkjs/ui": "^2.0.0", + "@jescalan/ink-markdown": "^2.0.0", + "ejs": "3.1.10", + "globby": "^14.0.1", + "gray-matter": "^4.0.3", + "index-to-position": "^0.1.2", + "ink": "^5.0.1", + "ink-big-text": "^2.0.0", + "ink-gradient": "^3.0.0", + "ink-link": "^4.1.0", + "jscodeshift": "^17.0.0", + "marked": "^11.1.1", + "meow": "^11.0.0", + "react": "^18.3.1", + "read-pkg": "^9.0.1", + "semver-regex": "^4.0.5", + "temp-dir": "^3.0.0" + }, + "bin": { + "clerk-upgrade": "dist/cli.js" }, "engines": { "node": ">=18.17.0" } }, + "packages/upgrade/node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/upgrade/node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "packages/upgrade/node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "packages/upgrade/node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "packages/upgrade/node_modules/@babel/helper-replace-supers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/upgrade/node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "packages/upgrade/node_modules/@babel/plugin-syntax-flow": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.25.7.tgz", + "integrity": "sha512-fyoj6/YdVtlv2ROig/J0fP7hh/wNO1MJGm1NR70Pg7jbkF+jOUL9joorqaCOQh06Y+LfgTagHzC8KqZ3MF782w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/upgrade/node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz", + "integrity": "sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/upgrade/node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.7.tgz", + "integrity": "sha512-q8Td2PPc6/6I73g96SreSUCKEcwMXCwcXSIAVTyTTN6CpJe0dMj8coxu1fg1T9vfBLi6Rsi6a4ECcFBbKabS5w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-syntax-flow": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/upgrade/node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/upgrade/node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/upgrade/node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz", + "integrity": "sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "packages/upgrade/node_modules/@babel/plugin-transform-react-display-name": { "version": "7.24.7", "dev": true, @@ -48339,6 +48540,23 @@ "@babel/core": "^7.0.0-0" } }, + "packages/upgrade/node_modules/@babel/preset-flow": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.25.7.tgz", + "integrity": "sha512-q2x3g0YHzo/Ohsr51KOYS/BtZMsvkzVd8qEyhZAyTatYdobfgXCuyppTqTuIhdq5kR/P3nyyVvZ6H5dMc4PnCQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-transform-flow-strip-types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "packages/upgrade/node_modules/@babel/preset-react": { "version": "7.24.7", "dev": true, @@ -48358,6 +48576,187 @@ "@babel/core": "^7.0.0-0" } }, + "packages/upgrade/node_modules/@babel/register": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.25.7.tgz", + "integrity": "sha512-qHTd2Rhn/rKhSUwdY6+n98FmwXN+N+zxSVx3zWqRe9INyvTpv+aQ5gDV2+43ACd3VtMBzPPljbb0gZb8u5ma6Q==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/upgrade/node_modules/@inkjs/ui": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inkjs/ui/-/ui-2.0.0.tgz", + "integrity": "sha512-5+8fJmwtF9UvikzLfph9sA+LS+l37Ij/szQltkuXLOAXwNkBX9innfzh4pLGXIB59vKEQUtc6D4qGvhD7h3pAg==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-spinners": "^3.0.0", + "deepmerge": "^4.3.1", + "figures": "^6.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5" + } + }, + "packages/upgrade/node_modules/@inkjs/ui/node_modules/cli-spinners": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.2.0.tgz", + "integrity": "sha512-pXftdQloMZzjCr3pCTIRniDcys6dDzgpgVhAHHk6TKBDbRuP1MkuetTF5KSv4YUutbOPa7+7ZrAJ2kVtbMqyXA==", + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/@vitest/expect": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.3.tgz", + "integrity": "sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.3", + "@vitest/utils": "2.1.3", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/upgrade/node_modules/@vitest/pretty-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz", + "integrity": "sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/upgrade/node_modules/@vitest/runner": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.3.tgz", + "integrity": "sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.3", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/upgrade/node_modules/@vitest/snapshot": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.3.tgz", + "integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.3", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/upgrade/node_modules/@vitest/spy": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.3.tgz", + "integrity": "sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/upgrade/node_modules/@vitest/utils": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.3.tgz", + "integrity": "sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.3", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/upgrade/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "packages/upgrade/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "packages/upgrade/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "packages/upgrade/node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "packages/upgrade/node_modules/camelcase": { "version": "7.0.1", "license": "MIT", @@ -48394,9 +48793,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "packages/upgrade/node_modules/chalk": { "version": "5.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -48405,6 +48822,49 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "packages/upgrade/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "packages/upgrade/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "packages/upgrade/node_modules/decamelize": { "version": "6.0.0", "license": "MIT", @@ -48415,6 +48875,81 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "packages/upgrade/node_modules/execa": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.4.1.tgz", + "integrity": "sha512-5eo/BRqZm3GYce+1jqX/tJ7duA2AnE39i88fuedNFUV8XxGxUpF3aWkBRfbUcjV49gCkvS/pzc0YrCPhaIewdg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "packages/upgrade/node_modules/execa/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/upgrade/node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/upgrade/node_modules/find-up": { "version": "6.3.0", "license": "MIT", @@ -48429,6 +48964,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/upgrade/node_modules/globby": { "version": "14.0.1", "license": "MIT", @@ -48447,6 +49019,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "packages/upgrade/node_modules/hosted-git-info": { "version": "5.2.1", "license": "ISC", @@ -48457,6 +49038,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "packages/upgrade/node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "packages/upgrade/node_modules/indent-string": { "version": "5.0.0", "license": "MIT", @@ -48467,6 +49057,294 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/ink": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.0.1.tgz", + "integrity": "sha512-ae4AW/t8jlkj/6Ou21H2av0wxTk8vrGzXv+v2v7j4in+bl1M5XRMVbfNghzhBokV++FjF8RBDJvYo+ttR9YVRg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "indent-string": "^5.0.0", + "is-in-ci": "^0.1.0", + "lodash": "^4.17.21", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.0.0", + "type-fest": "^4.8.3", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.15.0", + "yoga-wasm-web": "~0.3.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "packages/upgrade/node_modules/ink-link": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ink-link/-/ink-link-4.1.0.tgz", + "integrity": "sha512-3nNyJXum0FJIKAXBK8qat2jEOM41nJ1J60NRivwgK9Xh92R5UMN/k4vbz0A9xFzhJVrlf4BQEmmxMgXkCE1Jeg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1", + "terminal-link": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "peerDependencies": { + "ink": ">=4" + } + }, + "packages/upgrade/node_modules/ink/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/ink/node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/ink/node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "packages/upgrade/node_modules/ink/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "packages/upgrade/node_modules/ink/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "packages/upgrade/node_modules/ink/node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/ink/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/ink/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/ink/node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/ink/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "packages/upgrade/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/jscodeshift": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.0.0.tgz", + "integrity": "sha512-Af+MFsNwLSVO+t4kKjJdJKh6iNbNHfDfFGdyltJ2wUFN3furgbvHguJmB85iou+fY7wbHgI8eiEKpp6doGgtKg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.7", + "neo-async": "^2.5.0", + "picocolors": "^1.0.1", + "recast": "^0.23.9", + "temp": "^0.9.4", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, "packages/upgrade/node_modules/locate-path": { "version": "7.2.0", "license": "MIT", @@ -48480,6 +49358,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true, + "license": "MIT" + }, "packages/upgrade/node_modules/lru-cache": { "version": "7.18.3", "license": "ISC", @@ -48487,6 +49372,28 @@ "node": ">=12" } }, + "packages/upgrade/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "packages/upgrade/node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "packages/upgrade/node_modules/marked": { "version": "11.2.0", "license": "MIT", @@ -48521,6 +49428,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "packages/upgrade/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "packages/upgrade/node_modules/normalize-package-data": { "version": "4.0.1", "license": "BSD-2-Clause", @@ -48534,6 +49461,34 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "packages/upgrade/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/npm-run-path/node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/upgrade/node_modules/p-limit": { "version": "4.0.0", "license": "MIT", @@ -48585,6 +49540,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/upgrade/node_modules/path-exists": { "version": "5.0.0", "license": "MIT", @@ -48592,6 +49559,18 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "packages/upgrade/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/upgrade/node_modules/path-type": { "version": "5.0.0", "license": "MIT", @@ -48602,6 +49581,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "packages/upgrade/node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "license": "ISC" + }, + "packages/upgrade/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "packages/upgrade/node_modules/pretty-ms": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz", + "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/upgrade/node_modules/quick-lru": { "version": "6.1.2", "license": "MIT", @@ -48759,6 +49778,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/recast": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", + "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, "packages/upgrade/node_modules/redent": { "version": "4.0.0", "license": "MIT", @@ -48773,6 +49808,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "packages/upgrade/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "packages/upgrade/node_modules/slash": { "version": "5.1.0", "license": "MIT", @@ -48783,6 +49853,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "packages/upgrade/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "packages/upgrade/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "packages/upgrade/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/upgrade/node_modules/strip-indent": { "version": "4.0.0", "license": "MIT", @@ -48796,6 +49912,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "packages/upgrade/node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "packages/upgrade/node_modules/temp-dir": { "version": "3.0.0", "license": "MIT", @@ -48803,6 +49945,49 @@ "node": ">=14.16" } }, + "packages/upgrade/node_modules/terminal-link": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-3.0.0.tgz", + "integrity": "sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^5.0.0", + "supports-hyperlinks": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/terminal-link/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/upgrade/node_modules/terminal-link/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/upgrade/node_modules/trim-newlines": { "version": "4.1.1", "license": "MIT", @@ -48823,6 +50008,93 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/upgrade/node_modules/vite-node": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz", + "integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/upgrade/node_modules/vitest": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.3.tgz", + "integrity": "sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.3", + "@vitest/mocker": "2.1.3", + "@vitest/pretty-format": "^2.1.3", + "@vitest/runner": "2.1.3", + "@vitest/snapshot": "2.1.3", + "@vitest/spy": "2.1.3", + "@vitest/utils": "2.1.3", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.3", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.3", + "@vitest/ui": "2.1.3", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "packages/upgrade/node_modules/yallist": { "version": "4.0.0", "license": "ISC" diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md index 3a6fee56c1..01eef51558 100644 --- a/packages/nextjs/README.md +++ b/packages/nextjs/README.md @@ -46,6 +46,10 @@ You'll learn how to install `@clerk/nextjs`, set up your environment keys, add ` For further information, guides, and examples visit the [Next.js reference documentation](https://clerk.com/docs/references/nextjs/overview?utm_source=github&utm_medium=clerk_nextjs). +## Upgrading + +`@clerk/nextjs` supports upgrading through automatic code migration. + ## Support You can get in touch with us in any of the following ways: diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index af71badee4..8b9099a619 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation'; import React, { useEffect, useTransition } from 'react'; import { useSafeLayoutEffect } from '../../client-boundary/hooks/useSafeLayoutEffect'; -import { ClerkNextOptionsProvider } from '../../client-boundary/NextOptionsContext'; +import { ClerkNextOptionsProvider, useClerkNextOptions } from '../../client-boundary/NextOptionsContext'; import type { NextClerkProviderProps } from '../../types'; import { ClerkJSScript } from '../../utils/clerk-js-script'; import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; @@ -62,6 +62,12 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => { const replace = useAwaitableReplace(); const [isPending, startTransition] = useTransition(); + // Avoid rendering nested ClerkProviders by checking for the existence of the ClerkNextOptions context provider + const isNested = Boolean(useClerkNextOptions()); + if (isNested) { + return props.children; + } + useEffect(() => { if (!isPending) { window.__clerk_internal_invalidateCachePromise?.(); diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 1e7060926a..b55db7dd9c 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -1,25 +1,56 @@ +import type { AuthObject } from '@clerk/backend'; import type { InitialState, Without } from '@clerk/types'; import { headers } from 'next/headers'; import React from 'react'; +import { PromisifiedAuthProvider } from '../../client-boundary/PromisifiedAuthProvider'; +import { getDynamicAuthData } from '../../server/buildClerkProps'; import type { NextClerkProviderProps } from '../../types'; import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; import { ClientClerkProvider } from '../client/ClerkProvider'; -import { initialState } from './auth'; -import { getScriptNonceFromHeader } from './utils'; +import { buildRequestLike, getScriptNonceFromHeader } from './utils'; -export function ClerkProvider(props: Without) { - const { children, ...rest } = props; - const state = initialState()?.__clerk_ssr_state as InitialState; - const cspHeader = headers().get('Content-Security-Policy'); +const getDynamicClerkState = React.cache(async function getDynamicClerkState() { + const request = await buildRequestLike(); + const data = getDynamicAuthData(request); - return ( + return data; +}); + +const getNonceFromCSPHeader = React.cache(async function getNonceFromCSPHeader() { + return getScriptNonceFromHeader((await headers()).get('Content-Security-Policy') || '') || ''; +}); + +export async function ClerkProvider( + props: Without, +) { + const { children, dynamic, ...rest } = props; + let statePromise: Promise = Promise.resolve(null); + let nonce = Promise.resolve(''); + + if (dynamic) { + statePromise = getDynamicClerkState(); + nonce = getNonceFromCSPHeader(); + } + + const output = ( {children} ); + + if (dynamic) { + return ( + // TODO: fix types so AuthObject is compatible with InitialState + }> + {output} + + ); + } + + return output; } diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 4db3d4bf2d..d00466897f 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -1,23 +1,20 @@ import type { AuthObject } from '@clerk/backend'; -import type { RedirectFun } from '@clerk/backend/internal'; -import { constants, createClerkRequest, createRedirect } from '@clerk/backend/internal'; +import { constants, createClerkRequest, createRedirect, type RedirectFun } from '@clerk/backend/internal'; import { notFound, redirect } from 'next/navigation'; -import { buildClerkProps } from '../../server/buildClerkProps'; import { PUBLISHABLE_KEY, SIGN_IN_URL, SIGN_UP_URL } from '../../server/constants'; import { createGetAuth } from '../../server/createGetAuth'; import { authAuthHeaderMissing } from '../../server/errors'; -import type { AuthProtect } from '../../server/protect'; import { createProtect } from '../../server/protect'; import { decryptClerkRequestData, getAuthKeyFromRequest, getHeader } from '../../server/utils'; import { buildRequestLike } from './utils'; -type Auth = AuthObject & { protect: AuthProtect; redirectToSignIn: RedirectFun> }; +type Auth = AuthObject & { redirectToSignIn: RedirectFun> }; -export const auth = (): Auth => { +export async function auth(): Promise { require('server-only'); - const request = buildRequestLike(); + const request = await buildRequestLike(); const authObject = createGetAuth({ debugLoggerName: 'auth()', noAuthStatusMessage: authAuthHeaderMissing(), @@ -46,11 +43,21 @@ export const auth = (): Auth => { }); }; - const protect = createProtect({ request, authObject, redirectToSignIn, notFound, redirect }); + return Object.assign(authObject, { redirectToSignIn }); +} - return Object.assign(authObject, { protect, redirectToSignIn }); -}; +auth.protect = async (...args: any[]) => { + require('server-only'); -export const initialState = () => { - return buildClerkProps(buildRequestLike()); + const request = await buildRequestLike(); + const authObject = await auth(); + + const protect = createProtect({ + request, + authObject, + redirectToSignIn: authObject.redirectToSignIn, + notFound, + redirect, + }); + return protect(...args); }; diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 4618efd821..12ff781248 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -3,15 +3,15 @@ import React from 'react'; import { auth } from './auth'; -export function SignedIn(props: React.PropsWithChildren): React.JSX.Element | null { +export async function SignedIn(props: React.PropsWithChildren): Promise { const { children } = props; - const { userId } = auth(); + const { userId } = await auth(); return userId ? <>{children} : null; } -export function SignedOut(props: React.PropsWithChildren): React.JSX.Element | null { +export async function SignedOut(props: React.PropsWithChildren): Promise { const { children } = props; - const { userId } = auth(); + const { userId } = await auth(); return userId ? null : <>{children}; } @@ -27,9 +27,9 @@ export function SignedOut(props: React.PropsWithChildren): React.JSX.Element | n * Unauthorized

} /> * ``` */ -export function Protect(props: ProtectProps): React.JSX.Element | null { +export async function Protect(props: ProtectProps): Promise { const { children, fallback, ...restAuthorizedParams } = props; - const { has, userId } = auth(); + const { has, userId } = await auth(); /** * Fallback to UI provided by user or `null` if authorization checks failed @@ -46,17 +46,11 @@ export function Protect(props: ProtectProps): React.JSX.Element | null { * Check against the results of `has` called inside the callback */ if (typeof restAuthorizedParams.condition === 'function') { - if (restAuthorizedParams.condition(has)) { - return authorized; - } - return unauthorized; + return restAuthorizedParams.condition(has) ? authorized : unauthorized; } if (restAuthorizedParams.role || restAuthorizedParams.permission) { - if (has(restAuthorizedParams)) { - return authorized; - } - return unauthorized; + return has(restAuthorizedParams) ? authorized : unauthorized; } /** diff --git a/packages/nextjs/src/app-router/server/currentUser.ts b/packages/nextjs/src/app-router/server/currentUser.ts index 336fd65565..1e7426843c 100644 --- a/packages/nextjs/src/app-router/server/currentUser.ts +++ b/packages/nextjs/src/app-router/server/currentUser.ts @@ -6,10 +6,10 @@ import { auth } from './auth'; export async function currentUser(): Promise { require('server-only'); - const { userId } = auth(); + const { userId } = await auth(); if (!userId) { return null; } - return clerkClient().users.getUser(userId); + return (await clerkClient()).users.getUser(userId); } diff --git a/packages/nextjs/src/app-router/server/utils.ts b/packages/nextjs/src/app-router/server/utils.ts index 513e84f574..d831c4fdaa 100644 --- a/packages/nextjs/src/app-router/server/utils.ts +++ b/packages/nextjs/src/app-router/server/utils.ts @@ -19,13 +19,13 @@ export const isPrerenderingBailout = (e: unknown) => { return routeRegex.test(message) || dynamicServerUsage || bailOutPrerendering; }; -export const buildRequestLike = () => { +export async function buildRequestLike() { try { // Dynamically import next/headers, otherwise Next12 apps will break - // because next/headers was introduced in next@13 - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { headers } = require('next/headers'); - return new NextRequest('https://placeholder.com', { headers: headers() }); + // @ts-ignore: Cannot find module 'next/headers' or its corresponding type declarations.ts(2307) + const { headers } = await import('next/headers'); + const resolvedHeaders = await headers(); + return new NextRequest('https://placeholder.com', { headers: resolvedHeaders }); } catch (e: any) { // rethrow the error when react throws a prerendering bailout // https://nextjs.org/docs/messages/ppr-caught-error @@ -37,7 +37,7 @@ export const buildRequestLike = () => { `Clerk: auth() and currentUser() are only supported in App Router (/app directory).\nIf you're using /pages, try getAuth() instead.\nOriginal error: ${e}`, ); } -}; +} // Original source: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/get-script-nonce-from-header.tsx export function getScriptNonceFromHeader(cspHeaderValue: string): string | undefined { diff --git a/packages/nextjs/src/client-boundary/NextOptionsContext.tsx b/packages/nextjs/src/client-boundary/NextOptionsContext.tsx index 7bc421e2c9..05fa6b0172 100644 --- a/packages/nextjs/src/client-boundary/NextOptionsContext.tsx +++ b/packages/nextjs/src/client-boundary/NextOptionsContext.tsx @@ -9,7 +9,7 @@ ClerkNextOptionsCtx.displayName = 'ClerkNextOptionsCtx'; const useClerkNextOptions = () => { const ctx = React.useContext(ClerkNextOptionsCtx) as { value: ClerkNextContextValue }; - return ctx.value; + return ctx?.value; }; const ClerkNextOptionsProvider = ( diff --git a/packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx b/packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx new file mode 100644 index 0000000000..54429ad529 --- /dev/null +++ b/packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useAuth } from '@clerk/clerk-react'; +import { useDerivedAuth } from '@clerk/clerk-react/internal'; +import type { InitialState } from '@clerk/types'; +import { useRouter } from 'next/compat/router'; +import { PHASE_PRODUCTION_BUILD } from 'next/constants'; +import React from 'react'; + +const PromisifiedAuthContext = React.createContext | InitialState | null>(null); + +export function PromisifiedAuthProvider({ + authPromise, + children, +}: { + authPromise: Promise | InitialState; + children: React.ReactNode; +}) { + return {children}; +} + +/** + * Returns the current auth state, the user and session ids and the `getToken` + * that can be used to retrieve the given template or the default Clerk token. + * + * Until Clerk loads, `isLoaded` will be set to `false`. + * Once Clerk loads, `isLoaded` will be set to `true`, and you can + * safely access the `userId` and `sessionId` variables. + * + * For projects using NextJs or Remix, you can have immediate access to this data during SSR + * simply by using the `ClerkProvider`. + * + * @example + * A simple example: + * + * import { useAuth } from '@clerk/nextjs' + * + * function Hello() { + * const { isSignedIn, sessionId, userId } = useAuth(); + * if(isSignedIn) { + * return null; + * } + * console.log(sessionId, userId) + * return
...
+ * } + * + * @example + * Basic example in a NextJs app. This page will be fully rendered during SSR: + * + * import { useAuth } from '@clerk/nextjs' + * + * export HelloPage = () => { + * const { isSignedIn, sessionId, userId } = useAuth(); + * console.log(isSignedIn, sessionId, userId) + * return
...
+ * } + */ +export function usePromisifiedAuth() { + const isPagesRouter = !useRouter(); + const valueFromContext = React.useContext(PromisifiedAuthContext); + + let resolvedData = valueFromContext; + if (valueFromContext && 'then' in valueFromContext) { + resolvedData = React.use(valueFromContext); + } + + // At this point we should have a usable auth object + + if (typeof window === 'undefined') { + // Pages router should always use useAuth as it is able to grab initial auth state from context during SSR. + if (isPagesRouter) { + return useAuth(); + } + + if (!resolvedData && process.env.NEXT_PHASE !== PHASE_PRODUCTION_BUILD) { + throw new Error( + 'Clerk: useAuth() called in static mode, wrap this component in to make auth data available during server-side rendering.', + ); + } + // We don't need to deal with Clerk being loaded here + return useDerivedAuth(resolvedData); + } else { + return useAuth(resolvedData); + } +} diff --git a/packages/nextjs/src/client-boundary/hooks.ts b/packages/nextjs/src/client-boundary/hooks.ts index 10fbeb540c..b3756943a4 100644 --- a/packages/nextjs/src/client-boundary/hooks.ts +++ b/packages/nextjs/src/client-boundary/hooks.ts @@ -1,7 +1,6 @@ 'use client'; export { - useAuth, useClerk, useEmailLink, useOrganization, @@ -20,3 +19,5 @@ export { isMetamaskError, EmailLinkErrorCode, } from '@clerk/clerk-react/errors'; + +export { usePromisifiedAuth as useAuth } from './PromisifiedAuthProvider'; diff --git a/packages/nextjs/src/client-boundary/hooks/useEnforceCatchAllRoute.tsx b/packages/nextjs/src/client-boundary/hooks/useEnforceCatchAllRoute.tsx index 2925795bf1..47e61f1a47 100644 --- a/packages/nextjs/src/client-boundary/hooks/useEnforceCatchAllRoute.tsx +++ b/packages/nextjs/src/client-boundary/hooks/useEnforceCatchAllRoute.tsx @@ -38,7 +38,7 @@ export const useEnforceCatchAllRoute = ( // because these components are usually protected by the middleware // and if the check runs before the session is available, it will fail // even if the route is a catch-all route, as the check request will result - // in a 404 because of auth().protect(); + // in a 404 because of auth.protect(); if (requireSessionBeforeCheck && !session) { return; } diff --git a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap index 127181d0aa..788676885f 100644 --- a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap @@ -3,7 +3,6 @@ exports[`/server public exports should not include a breaking change 1`] = ` [ "auth", - "authMiddleware", "buildClerkProps", "clerkClient", "clerkMiddleware", @@ -11,8 +10,6 @@ exports[`/server public exports should not include a breaking change 1`] = ` "createRouteMatcher", "currentUser", "getAuth", - "redirectToSignIn", - "redirectToSignUp", "verifyToken", ] `; diff --git a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts deleted file mode 100644 index 8051a53e8a..0000000000 --- a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts +++ /dev/null @@ -1,582 +0,0 @@ -// There is no need to execute the complete authenticateRequest to test authMiddleware -// This mock SHOULD exist before the import of authenticateRequest -import { AuthStatus } from '@clerk/backend/internal'; -import { expectTypeOf } from 'expect-type'; -import type { NextFetchEvent } from 'next/server'; -import { NextRequest, NextResponse } from 'next/server'; - -const authenticateRequestMock = jest.fn().mockResolvedValue({ - toAuth: () => ({}), - headers: new Headers(), -}); - -// Removing this mock will cause the authMiddleware tests to fail due to missing publishable key -// This mock SHOULD exist before the imports -jest.mock('../constants', () => { - return { - PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', - SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx', - }; -}); - -jest.mock('../clerkClient', () => { - return { - clerkClient: { - authenticateRequest: authenticateRequestMock, - telemetry: { record: jest.fn() }, - }, - }; -}); - -import { pathToRegexp } from '@clerk/shared/pathToRegexp'; - -import { authMiddleware, DEFAULT_CONFIG_MATCHER, DEFAULT_IGNORED_ROUTES } from '../authMiddleware'; -// used to assert the mock -import { clerkClient } from '../clerkClient'; -import { createRouteMatcher } from '../routeMatcher'; - -/** - * Disable console warnings about config matchers - */ -const consoleWarn = console.warn; -global.console.warn = jest.fn(); -beforeAll(() => { - global.console.warn = jest.fn(); -}); -afterAll(() => { - global.console.warn = consoleWarn; -}); - -type MockRequestParams = { - url: string; - appendDevBrowserCookie?: boolean; - method?: string; - headers?: any; -}; - -const mockRequest = ({ - url, - appendDevBrowserCookie = false, - method = 'GET', - headers = new Headers(), -}: MockRequestParams) => { - const headersWithCookie = new Headers(headers); - if (appendDevBrowserCookie) { - headersWithCookie.append('cookie', '__clerk_db_jwt=test_jwt'); - } - return new NextRequest(new URL(url, 'https://www.clerk.com').toString(), { - method, - headers: headersWithCookie, - }); -}; - -describe('isPublicRoute', () => { - describe('should work with path patterns', function () { - it('matches path and all sub paths using *', () => { - const isPublicRoute = createRouteMatcher(['/hello(.*)']); - expect(isPublicRoute(mockRequest({ url: '/hello' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/hello' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/hello/test/a' }))).toBe(true); - }); - - it('matches filenames with specific extensions', () => { - const isPublicRoute = createRouteMatcher(['/(.*).ts', '/(.*).js']); - expect(isPublicRoute(mockRequest({ url: '/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.ts' }))).toBe(true); - }); - - it('works with single values (non array)', () => { - const isPublicRoute = createRouteMatcher('/test/hello.ts'); - expect(isPublicRoute(mockRequest({ url: '/hello.js' }))).not.toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.js' }))).not.toBe(true); - }); - }); - - describe('should work with regex patterns', function () { - it('matches path and all sub paths using *', () => { - const isPublicRoute = createRouteMatcher([/^\/hello.*$/]); - expect(isPublicRoute(mockRequest({ url: '/hello' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/hello/' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/hello/test/a' }))).toBe(true); - }); - - it('matches filenames with specific extensions', () => { - const isPublicRoute = createRouteMatcher([/^.*\.(ts|js)$/]); - expect(isPublicRoute(mockRequest({ url: '/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.ts' }))).toBe(true); - }); - - it('works with single values (non array)', () => { - const isPublicRoute = createRouteMatcher(/hello/g); - expect(isPublicRoute(mockRequest({ url: '/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.js' }))).toBe(true); - }); - }); -}); - -const validRoutes = [ - '/api', - '/api/', - '/api/hello', - '/trpc', - '/trpc/hello', - '/trpc/hello.example', - '/protected', - '/protected/', - '/protected/hello', - '/protected/hello.example/hello', - '/my-protected-page', - '/my/$special/$pages', -]; - -const invalidRoutes = [ - '/_next', - '/favicon.ico', - '/_next/test.json', - '/files/api.pdf', - '/test/api/test.pdf', - '/imgs/img.png', - '/imgs/img-dash.jpg', -]; - -describe('default config matcher', () => { - it('compiles to regex using path-to-regex', () => { - [DEFAULT_CONFIG_MATCHER].flat().forEach(path => { - expect(pathToRegexp(path)).toBeInstanceOf(RegExp); - }); - }); - - describe('does not match any static files or next internals', function () { - it.each(invalidRoutes)(`does not match %s`, path => { - const matcher = createRouteMatcher(DEFAULT_CONFIG_MATCHER); - expect(matcher(mockRequest({ url: path }))).toBe(false); - }); - }); - - describe('matches /api or known framework routes', function () { - it.each(validRoutes)(`matches %s`, path => { - const matcher = createRouteMatcher(DEFAULT_CONFIG_MATCHER); - expect(matcher(mockRequest({ url: path }))).toBe(true); - }); - }); -}); - -describe('default ignored routes matcher', () => { - it('compiles to regex using path-to-regex', () => { - [DEFAULT_IGNORED_ROUTES].flat().forEach(path => { - expect(pathToRegexp(path)).toBeInstanceOf(RegExp); - }); - }); - - describe('matches all static files or next internals', function () { - it.each(invalidRoutes)(`matches %s`, path => { - const matcher = createRouteMatcher(DEFAULT_IGNORED_ROUTES); - expect(matcher(mockRequest({ url: path }))).toBe(true); - }); - }); - - describe('does not match /api or known framework routes', function () { - it.each(validRoutes)(`does not match %s`, path => { - const matcher = createRouteMatcher(DEFAULT_IGNORED_ROUTES); - expect(matcher(mockRequest({ url: path }))).toBe(false); - }); - }); -}); - -describe('authMiddleware(params)', () => { - beforeEach(() => { - authenticateRequestMock.mockClear(); - }); - - describe('without params', function () { - it('redirects to sign-in for protected route', async () => { - const resp = await authMiddleware()(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', - ); - }); - - it('renders public route', async () => { - const signInResp = await authMiddleware({ publicRoutes: '/sign-in' })( - mockRequest({ url: '/sign-in' }), - {} as NextFetchEvent, - ); - expect(signInResp?.status).toEqual(200); - expect(signInResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/sign-in'); - - const signUpResp = await authMiddleware({ publicRoutes: ['/sign-up'] })( - mockRequest({ url: '/sign-up' }), - {} as NextFetchEvent, - ); - expect(signUpResp?.status).toEqual(200); - expect(signUpResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/sign-up'); - }); - }); - - describe('with ignoredRoutes', function () { - it('skips auth middleware execution', async () => { - const beforeAuthSpy = jest.fn(); - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - ignoredRoutes: '/ignored', - beforeAuth: beforeAuthSpy, - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/ignored' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(clerkClient.authenticateRequest).not.toBeCalled(); - expect(beforeAuthSpy).not.toBeCalled(); - expect(afterAuthSpy).not.toBeCalled(); - }); - - it('executes auth middleware execution when is not matched', async () => { - const beforeAuthSpy = jest.fn(); - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - ignoredRoutes: '/ignored', - beforeAuth: beforeAuthSpy, - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(clerkClient.authenticateRequest).toBeCalled(); - expect(beforeAuthSpy).toBeCalled(); - expect(afterAuthSpy).toBeCalled(); - }); - }); - - describe('with publicRoutes', function () { - it('renders public route', async () => { - const resp = await authMiddleware({ - publicRoutes: '/public', - })(mockRequest({ url: '/public' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(resp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/public'); - }); - - describe('when sign-in/sign-up routes are defined in env', () => { - const currentSignInUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL; - const currentSignUpUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL; - - beforeEach(() => { - process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL = '/custom-sign-in'; - process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL = '/custom-sign-up'; - }); - - afterEach(() => { - process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL = currentSignInUrl; - process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL = currentSignUpUrl; - }); - - it('renders sign-in/sign-up routes', async () => { - const signInResp = await authMiddleware({ - publicRoutes: '/public', - })(mockRequest({ url: '/custom-sign-in' }), {} as NextFetchEvent); - expect(signInResp?.status).toEqual(200); - expect(signInResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/custom-sign-in'); - - const signUpResp = await authMiddleware({ - publicRoutes: '/public', - })(mockRequest({ url: '/custom-sign-up' }), {} as NextFetchEvent); - expect(signUpResp?.status).toEqual(200); - expect(signUpResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/custom-sign-up'); - }); - }); - - it('redirects to sign-in for protected route', async () => { - const resp = await authMiddleware({ - publicRoutes: '/public', - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', - ); - }); - }); - - describe('with beforeAuth', function () { - it('skips auth middleware execution when beforeAuth returns false', async () => { - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - beforeAuth: () => false, - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('skip'); - expect(clerkClient.authenticateRequest).not.toBeCalled(); - expect(afterAuthSpy).not.toBeCalled(); - }); - - it('executes auth middleware execution when beforeAuth returns undefined', async () => { - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - beforeAuth: () => undefined, - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(clerkClient.authenticateRequest).toBeCalled(); - expect(afterAuthSpy).toBeCalled(); - }); - - it('skips auth middleware execution when beforeAuth returns NextResponse.redirect', async () => { - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.redirect('https://www.clerk.com/custom-redirect'), - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/custom-redirect'); - expect(clerkClient.authenticateRequest).not.toBeCalled(); - expect(afterAuthSpy).not.toBeCalled(); - }); - - it('executes auth middleware when beforeAuth returns NextResponse', async () => { - const resp = await authMiddleware({ - beforeAuth: () => - NextResponse.next({ - headers: { - 'x-before-auth-header': 'before', - }, - }), - afterAuth: () => - NextResponse.next({ - headers: { - 'x-after-auth-header': 'after', - }, - }), - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(resp?.headers.get('x-before-auth-header')).toEqual('before'); - expect(resp?.headers.get('x-after-auth-header')).toEqual('after'); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); - }); - - describe('with afterAuth', function () { - it('redirects to sign-in for protected route and sets redirect as auth reason header', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', - ); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); - - it('uses authenticateRequest result as auth', async () => { - const req = mockRequest({ url: '/protected' }); - const event = {} as NextFetchEvent; - authenticateRequestMock.mockResolvedValueOnce({ toAuth: () => ({ userId: null }), headers: new Headers() }); - const afterAuthSpy = jest.fn(); - - await authMiddleware({ afterAuth: afterAuthSpy })(req, event); - - expect(clerkClient.authenticateRequest).toBeCalled(); - expect(afterAuthSpy).toBeCalledWith( - { - userId: null, - isPublicRoute: false, - isApiRoute: false, - }, - req, - event, - ); - }); - }); - - describe('authenticateRequest', function () { - it('returns 307 and starts the handshake flow for handshake requestState status', async () => { - const mockLocationUrl = 'https://example.com'; - authenticateRequestMock.mockResolvedValueOnce({ - status: AuthStatus.Handshake, - headers: new Headers({ Location: mockLocationUrl }), - }); - const resp = await authMiddleware()(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('Location')).toEqual(mockLocationUrl); - }); - }); -}); - -describe('Dev Browser JWT when redirecting to cross origin', function () { - it('does NOT append the Dev Browser JWT when cookie is missing', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - })(mockRequest({ url: '/protected', appendDevBrowserCookie: false }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', - ); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); - - it('appends the Dev Browser JWT to the search when cookie __clerk_db_jwt exists and location is an Account Portal URL', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - })(mockRequest({ url: '/protected', appendDevBrowserCookie: true }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected&__clerk_db_jwt=test_jwt', - ); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); - - it('does NOT append the Dev Browser JWT if x-clerk-redirect-to header is not set', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.redirect('https://google.com/'), - })(mockRequest({ url: '/protected', appendDevBrowserCookie: true }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual('https://google.com/'); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); -}); - -describe('isApiRoute', function () { - it('treats route as API route if apiRoutes match the route path', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/api/(.*)'], - })(mockRequest({ url: '/api/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('treats route as Page route if apiRoutes do not match the route path', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/api/(.*)'], - })(mockRequest({ url: '/page' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - }); - - it('treats route as API route if apiRoutes prop is missing and route path matches the default matcher (/api/(.*))', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - })(mockRequest({ url: '/api/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('treats route as API route if apiRoutes prop is missing and route path matches the default matcher (/trpc/(.*))', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - })(mockRequest({ url: '/trpc/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('treats route as API route if apiRoutes prop is missing and Request method is not-GET,OPTIONS,HEAD', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - })(mockRequest({ url: '/products/items', method: 'POST' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('treats route as API route if apiRoutes prop is missing and Request headers Content-Type is application/json', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - })( - mockRequest({ url: '/products/items', headers: new Headers({ 'content-type': 'application/json' }) }), - {} as NextFetchEvent, - ); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); -}); - -describe('401 Response on Api Routes', function () { - it('returns 401 when route is not public and route matches API routes', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/products/(.*)'], - })(mockRequest({ url: '/products/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('returns 307 when route is not public and route does not match API routes', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/products/(.*)'], - })(mockRequest({ url: '/api/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('content-type')).not.toEqual('application/json'); - }); - - it('returns 200 when API route is public', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/public'], - })(mockRequest({ url: '/public' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - }); -}); - -describe('Type tests', () => { - type AuthMiddleware = Parameters[0]; - describe('AuthMiddleware', () => { - it('is the options argument for authMiddleware', () => { - () => { - authMiddleware({} as AuthMiddleware); - }; - }); - - it('can receive the appropriate keys', () => { - expectTypeOf({ publishableKey: '', secretKey: '' }).toMatchTypeOf(); - expectTypeOf({ secretKey: '' }).toMatchTypeOf(); - expectTypeOf({ publishableKey: '', secretKey: '' }).toMatchTypeOf(); - expectTypeOf({ secretKey: '' }).toMatchTypeOf(); - }); - - describe('Multi domain', () => { - const defaultProps = { publishableKey: '', secretKey: '' }; - - it('proxyUrl (primary app)', () => { - expectTypeOf({ ...defaultProps, proxyUrl: 'test' }).toMatchTypeOf(); - }); - - it('proxyUrl + isSatellite (satellite app)', () => { - expectTypeOf({ ...defaultProps, proxyUrl: 'test', isSatellite: true }).toMatchTypeOf(); - }); - - it('domain + isSatellite (satellite app)', () => { - expectTypeOf({ ...defaultProps, domain: 'test', isSatellite: true }).toMatchTypeOf(); - }); - }); - }); -}); diff --git a/packages/nextjs/src/server/__tests__/clerkClient.test.ts b/packages/nextjs/src/server/__tests__/clerkClient.test.ts index 58b6e883dd..d41af5a6ad 100644 --- a/packages/nextjs/src/server/__tests__/clerkClient.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkClient.test.ts @@ -4,7 +4,9 @@ import { clerkClient } from '../clerkClient'; describe('clerkClient', () => { it('should pass version package to userAgent', async () => { - await clerkClient().users.getUser('user_test'); + const resolvedClerkClient = await clerkClient(); + + await resolvedClerkClient.users.getUser('user_test'); expect(global.fetch).toBeCalled(); expect((global.fetch as any).mock.calls[0][1].headers).toMatchObject({ diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index ea116a664d..318414d6cf 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -267,7 +267,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('redirects to sign-in url when redirectToSignIn is called with the correct returnBackUrl', async () => { @@ -284,7 +284,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toContain('/protected'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('redirects to sign-in url with redirect_url set to the provided returnBackUrl param', async () => { @@ -303,7 +303,7 @@ describe('clerkMiddleware(params)', () => { expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toEqual( 'https://www.clerk.com/hello', ); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('redirects to sign-in url without a redirect_url when returnBackUrl is null', async () => { @@ -320,11 +320,11 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toBeNull(); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); }); - describe('auth().protect()', () => { + describe('auth.protect()', () => { it('redirects to sign-in url when protect is called, the user is signed out and the request is a page request', async () => { const req = mockRequest({ url: '/protected', @@ -339,13 +339,13 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: null }), }); - const resp = await clerkMiddleware(auth => { - auth().protect(); + const resp = await clerkMiddleware(async auth => { + await auth.protect(); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('does not redirect to sign-in url when protect is called, the user is signed in and the request is a page request', async () => { @@ -362,13 +362,13 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: 'user-id' }), }); - const resp = await clerkMiddleware(auth => { - auth().protect(); + const resp = await clerkMiddleware(async auth => { + await auth.protect(); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(200); expect(resp?.headers.get('location')).toBeFalsy(); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('throws a not found error when protect is called, the user is signed out, and is not a page request', async () => { @@ -385,13 +385,13 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: null }), }); - const resp = await clerkMiddleware(auth => { - auth().protect(); + const resp = await clerkMiddleware(async auth => { + await auth.protect(); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(200); expect(resp?.headers.get(constants.Headers.AuthReason)).toContain('protect-rewrite'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('throws a not found error when protect is called with RBAC params the user does not fulfill, and is a page request', async () => { @@ -408,13 +408,13 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: 'user-id', has: () => false }), }); - const resp = await clerkMiddleware(auth => { - auth().protect({ role: 'random-role' }); + const resp = await clerkMiddleware(async auth => { + await auth.protect({ role: 'random-role' }); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(200); expect(resp?.headers.get(constants.Headers.AuthReason)).toContain('protect-rewrite'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('redirects to unauthenticatedUrl when protect is called with the redirectUrl param, the user is signed out, and is a page request', async () => { @@ -431,14 +431,14 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: null }), }); - const resp = await clerkMiddleware(auth => { - auth().protect({ unauthenticatedUrl: 'https://www.clerk.com/hello' }); + const resp = await clerkMiddleware(async auth => { + await auth.protect({ unauthenticatedUrl: 'https://www.clerk.com/hello' }); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/hello'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect(await (await clerkClient()).authenticateRequest).toBeCalled(); }); it('redirects to unauthorizedUrl when protect is called with the redirectUrl param, the user does not fulfill the RBAC params, and is a page request', async () => { @@ -455,8 +455,8 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: 'user-id', has: () => false }), }); - const resp = await clerkMiddleware(auth => { - auth().protect( + const resp = await clerkMiddleware(async auth => { + await auth.protect( { role: 'random-role' }, { unauthorizedUrl: 'https://www.clerk.com/discover', @@ -468,7 +468,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/discover'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); }); @@ -487,16 +487,16 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: null }), }); - const resp = await clerkMiddleware(auth => { - auth().protect(); + const resp = await clerkMiddleware(async auth => { + await auth.protect(); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); - it('forwards headers from authenticateRequest when auth().protect() is called', async () => { + it('forwards headers from authenticateRequest when auth.protect() is called', async () => { const req = mockRequest({ url: '/protected', headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), @@ -513,15 +513,15 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: null }), }); - const resp = await clerkMiddleware(auth => { - auth().protect(); + const resp = await clerkMiddleware(async auth => { + await auth.protect(); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(307); expect(resp?.headers.get('X-Clerk-Auth')).toEqual('1'); expect(resp?.headers.get('Set-Cookie')).toEqual('session=;'); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('redirects to unauthenticatedUrl when protect is called with the unauthenticatedUrl param, the user is signed out, and is a page request', async () => { @@ -538,8 +538,8 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: null }), }); - const resp = await clerkMiddleware(auth => { - auth().protect({ + const resp = await clerkMiddleware(async auth => { + await auth.protect({ unauthenticatedUrl: 'https://www.clerk.com/unauthenticatedUrl', unauthorizedUrl: 'https://www.clerk.com/unauthorizedUrl', }); @@ -548,7 +548,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('https://www.clerk.com/unauthenticatedUrl'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('redirects to unauthorizedUrl when protect is called with the unauthorizedUrl param, the user is signed in but does not have permissions, and is a page request', async () => { @@ -565,8 +565,8 @@ describe('clerkMiddleware(params)', () => { toAuth: () => ({ userId: 'userId', has: () => false }), }); - const resp = await clerkMiddleware(auth => { - auth().protect( + const resp = await clerkMiddleware(async auth => { + await auth.protect( { permission: 'random-permission' }, { unauthenticatedUrl: 'https://www.clerk.com/unauthenticatedUrl', @@ -578,7 +578,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('https://www.clerk.com/unauthorizedUrl'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); }); @@ -624,15 +624,15 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f toAuth: () => ({ userId: null }), }); - const resp = await clerkMiddleware(auth => { - auth().protect(); + const resp = await clerkMiddleware(async auth => { + await auth.protect(); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('appends the Dev Browser JWT to the search when cookie __clerk_db_jwt exists and location is an Account Portal URL', async () => { @@ -649,14 +649,15 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f toAuth: () => ({ userId: null }), }); - const resp = await clerkMiddleware(auth => { - auth().protect(); + const resp = await clerkMiddleware(async auth => { + await auth.protect(); })(req, {} as NextFetchEvent); + expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected&__clerk_db_jwt=test_jwt', ); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); it('does NOT append the Dev Browser JWT if x-clerk-redirect-to header is not set (user-returned redirect)', async () => { @@ -678,10 +679,11 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); })(req, {} as NextFetchEvent); + expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect((await clerkClient()).authenticateRequest).toBeCalled(); }); }); diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts deleted file mode 100644 index c3076b6c6c..0000000000 --- a/packages/nextjs/src/server/authMiddleware.ts +++ /dev/null @@ -1,346 +0,0 @@ -import type { AuthObject } from '@clerk/backend'; -import type { AuthenticateRequestOptions, ClerkRequest } from '@clerk/backend/internal'; -import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal'; -import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; -import { eventMethodCalled } from '@clerk/shared/telemetry'; -import type { NextFetchEvent, NextMiddleware, NextRequest } from 'next/server'; -import { NextResponse } from 'next/server'; - -import { isRedirect, mergeResponses, serverRedirectWithAuth, setHeader, stringifyHeaders } from '../utils'; -import { withLogger } from '../utils/debugLogger'; -import { clerkClient } from './clerkClient'; -import { createAuthenticateRequestOptions } from './clerkMiddleware'; -import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; -import { informAboutProtectedRouteInfo, receivedRequestForIgnoredRoute } from './errors'; -import { errorThrower } from './errorThrower'; -import type { RouteMatcherParam } from './routeMatcher'; -import { createRouteMatcher } from './routeMatcher'; -import type { NextMiddlewareReturn } from './types'; -import { - apiEndpointUnauthorizedNextResponse, - assertKey, - decorateRequest, - redirectAdapter, - setRequestHeadersOnNextResponse, -} from './utils'; - -/** - * The default ideal matcher that excludes the _next directory (internals) and all static files, - * but it will match the root route (/) and any routes that start with /api or /trpc. - */ -export const DEFAULT_CONFIG_MATCHER = ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)']; - -/** - * Any routes matching this path will be ignored by the middleware. - * This is the inverted version of DEFAULT_CONFIG_MATCHER. - */ -export const DEFAULT_IGNORED_ROUTES = [`/((?!api|trpc))(_next.*|.+\\.[\\w]+$)`]; -/** - * Any routes matching this path will be treated as API endpoints by the middleware. - */ -export const DEFAULT_API_ROUTES = ['/api/(.*)', '/trpc/(.*)']; - -type IgnoredRoutesParam = Array | RegExp | string | ((req: NextRequest) => boolean); -type ApiRoutesParam = IgnoredRoutesParam; - -type WithExperimentalClerkUrl = T & { - /** - * When a NextJs app is hosted on a platform different from Vercel - * or inside a container (Netlify, Fly.io, AWS Amplify, docker etc), - * req.url is always set to `localhost:3000` instead of the actual host of the app. - * - * The `authMiddleware` uses the value of the available req.headers in order to construct - * and use the correct url internally. This url is then exposed as `experimental_clerkUrl`, - * intended to be used within `beforeAuth` and `afterAuth` if needed. - */ - experimental_clerkUrl: NextRequest['nextUrl']; -}; - -type BeforeAuthHandler = ( - req: WithExperimentalClerkUrl, - evt: NextFetchEvent, -) => NextMiddlewareReturn | false | Promise; - -type AfterAuthHandler = ( - auth: AuthObject & { isPublicRoute: boolean; isApiRoute: boolean }, - req: WithExperimentalClerkUrl, - evt: NextFetchEvent, -) => NextMiddlewareReturn; - -type AuthMiddlewareParams = AuthenticateRequestOptions & { - /** - * A function that is called before the authentication middleware is executed. - * If a redirect response is returned, the middleware will respect it and redirect the user. - * If false is returned, the auth middleware will not execute and the request will be handled as if the auth middleware was not present. - */ - beforeAuth?: BeforeAuthHandler; - /** - * A function that is called after the authentication middleware is executed. - * This function has access to the auth object and can be used to execute logic based on the auth state. - */ - afterAuth?: AfterAuthHandler; - /** - * A list of routes that should be accessible without authentication. - * You can use glob patterns to match multiple routes or a function to match against the request object. - * Path patterns and regular expressions are supported, for example: `['/foo', '/bar(.*)'] or `[/^\/foo\/.*$/]` - * The sign in and sign up URLs are included by default, unless a function is provided. - * For more information, see: https://clerk.com/docs - */ - publicRoutes?: RouteMatcherParam; - /** - * A list of routes that should be ignored by the middleware. - * This list typically includes routes for static files or Next.js internals. - * For improved performance, these routes should be skipped using the default config.matcher instead. - */ - ignoredRoutes?: IgnoredRoutesParam; - /** - * A list of routes that should be treated as API endpoints. - * When user is signed out, the middleware will return a 401 response for these routes, instead of redirecting the user. - * - * If omitted, the following heuristics will be used to determine an API endpoint: - * - The route path is ['/api/(.*)', '/trpc/(.*)'], - * - or the request has `Content-Type` set to `application/json`, - * - or the request method is not one of: `GET`, `OPTIONS` ,` HEAD` - * - * @default undefined - */ - apiRoutes?: ApiRoutesParam; - /** - * Enables extra debug logging. - */ - debug?: boolean; -}; - -export interface AuthMiddleware { - (params?: AuthMiddlewareParams): NextMiddleware; -} - -/** - * @deprecated `authMiddleware` is deprecated and will be removed in the next major version. - * Use {@link clerkMiddleware}` instead. - * Migration guide: https://clerk.com/docs/upgrade-guides/core-2/nextjs - */ -const authMiddleware: AuthMiddleware = (...args: unknown[]) => { - const [params = {}] = args as [AuthMiddlewareParams?]; - const publishableKey = assertKey(params.publishableKey || PUBLISHABLE_KEY, () => - errorThrower.throwMissingPublishableKeyError(), - ); - const secretKey = assertKey(params.secretKey || SECRET_KEY, () => errorThrower.throwMissingPublishableKeyError()); - const signInUrl = params.signInUrl || SIGN_IN_URL; - const signUpUrl = params.signUpUrl || SIGN_UP_URL; - - const options = { ...params, publishableKey, secretKey, signInUrl, signUpUrl }; - - const isIgnoredRoute = createRouteMatcher(options.ignoredRoutes || DEFAULT_IGNORED_ROUTES); - const isPublicRoute = createRouteMatcher(withDefaultPublicRoutes(options.publicRoutes)); - const isApiRoute = createApiRoutes(options.apiRoutes); - const defaultAfterAuth = createDefaultAfterAuth(isPublicRoute, isApiRoute, options); - - clerkClient.telemetry.record( - eventMethodCalled('authMiddleware', { - publicRoutes: Boolean(options.publicRoutes), - ignoredRoutes: Boolean(options.ignoredRoutes), - beforeAuth: Boolean(options.beforeAuth), - afterAuth: Boolean(options.afterAuth), - }), - ); - - return withLogger('authMiddleware', logger => async (_req: NextRequest, evt: NextFetchEvent) => { - if (options.debug) { - logger.enable(); - } - const clerkRequest = createClerkRequest(_req); - const nextRequest = withNormalizedClerkUrl(clerkRequest, _req); - - logger.debug('URL debug', { - url: nextRequest.nextUrl.href, - method: nextRequest.method, - headers: stringifyHeaders(nextRequest.headers), - nextUrl: nextRequest.nextUrl.href, - clerkUrl: nextRequest.experimental_clerkUrl.href, - }); - - logger.debug('Options debug', { ...options, beforeAuth: !!options.beforeAuth, afterAuth: !!options.afterAuth }); - - if (isIgnoredRoute(nextRequest)) { - logger.debug({ isIgnoredRoute: true }); - if (isDevelopmentFromSecretKey(options.secretKey) && !options.ignoredRoutes) { - console.warn( - receivedRequestForIgnoredRoute( - nextRequest.experimental_clerkUrl.href, - JSON.stringify(DEFAULT_CONFIG_MATCHER), - ), - ); - } - return setHeader(NextResponse.next(), constants.Headers.AuthReason, 'ignored-route'); - } - - const beforeAuthRes = await (options.beforeAuth && options.beforeAuth(nextRequest, evt)); - - if (beforeAuthRes === false) { - logger.debug('Before auth returned false, skipping'); - return setHeader(NextResponse.next(), constants.Headers.AuthReason, 'skip'); - } else if (beforeAuthRes && isRedirect(beforeAuthRes)) { - logger.debug('Before auth returned redirect, following redirect'); - return setHeader(beforeAuthRes, constants.Headers.AuthReason, 'before-auth-redirect'); - } - - const requestState = await clerkClient.authenticateRequest( - clerkRequest, - createAuthenticateRequestOptions(clerkRequest, options), - ); - - const locationHeader = requestState.headers.get('location'); - if (locationHeader) { - // triggering a handshake redirect - return new Response(null, { status: 307, headers: requestState.headers }); - } - - if (requestState.status === AuthStatus.Handshake) { - throw new Error('Clerk: unexpected handshake without redirect'); - } - - const auth = Object.assign(requestState.toAuth(), { - isPublicRoute: isPublicRoute(nextRequest), - isApiRoute: isApiRoute(nextRequest), - }); - - logger.debug(() => ({ auth: JSON.stringify(auth), debug: auth.debug() })); - const afterAuthRes = await (options.afterAuth || defaultAfterAuth)(auth, nextRequest, evt); - const finalRes = mergeResponses(beforeAuthRes, afterAuthRes) || NextResponse.next(); - logger.debug(() => ({ mergedHeaders: stringifyHeaders(finalRes.headers) })); - - if (isRedirect(finalRes)) { - logger.debug('Final response is redirect, following redirect'); - return serverRedirectWithAuth(clerkRequest, finalRes, options); - } - - if (options.debug) { - setRequestHeadersOnNextResponse(finalRes, nextRequest, { [constants.Headers.EnableDebug]: 'true' }); - logger.debug(`Added ${constants.Headers.EnableDebug} on request`); - } - - const result = decorateRequest(clerkRequest, finalRes, requestState, { secretKey }) || NextResponse.next(); - - if (requestState.headers) { - requestState.headers.forEach((value, key) => { - result.headers.append(key, value); - }); - } - - return result; - }); -}; - -export { authMiddleware }; - -const createDefaultAfterAuth = ( - isPublicRoute: ReturnType, - isApiRoute: ReturnType, - options: { signInUrl: string; signUpUrl: string; publishableKey: string; secretKey: string }, -) => { - return (auth: AuthObject, req: WithExperimentalClerkUrl) => { - if (!auth.userId && !isPublicRoute(req)) { - if (isApiRoute(req)) { - informAboutProtectedRoute(req.experimental_clerkUrl.pathname, options, true); - return apiEndpointUnauthorizedNextResponse(); - } else { - informAboutProtectedRoute(req.experimental_clerkUrl.pathname, options, false); - } - return createRedirect({ - redirectAdapter, - signInUrl: options.signInUrl, - signUpUrl: options.signUpUrl, - publishableKey: options.publishableKey, - // We're setting baseUrl to '' here as we want to keep the legacy behavior of - // the redirectToSignIn, redirectToSignUp helpers in the backend package. - baseUrl: '', - }).redirectToSignIn({ returnBackUrl: req.experimental_clerkUrl.href }); - } - return NextResponse.next(); - }; -}; - -const matchRoutesStartingWith = (path: string) => { - path = path.replace(/\/$/, ''); - return new RegExp(`^${path}(/.*)?$`); -}; - -const withDefaultPublicRoutes = (publicRoutes: RouteMatcherParam | undefined) => { - if (typeof publicRoutes === 'function') { - return publicRoutes; - } - - const routes = [publicRoutes || ''].flat().filter(Boolean); - // TODO: refactor it to use common config file eg SIGN_IN_URL from ./clerkClient - // we use process.env for now to support testing - const signInUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL || ''; - if (signInUrl) { - routes.push(matchRoutesStartingWith(signInUrl)); - } - // TODO: refactor it to use common config file eg SIGN_UP_URL from ./clerkClient - // we use process.env for now to support testing - const signUpUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL || ''; - if (signUpUrl) { - routes.push(matchRoutesStartingWith(signUpUrl)); - } - return routes; -}; - -// - Default behavior: -// If the route path is `['/api/(.*)*', '*/trpc/(.*)']` -// or Request has `Content-Type: application/json` -// or Request method is not-GET,OPTIONS,HEAD, -// then this is considered an API route. -// -// - If the user has provided a specific `apiRoutes` prop in `authMiddleware` then all the above are discarded, -// and only routes that match the user’s provided paths are considered API routes. -const createApiRoutes = ( - apiRoutes: RouteMatcherParam | undefined, -): ((req: WithExperimentalClerkUrl) => boolean) => { - if (apiRoutes) { - return createRouteMatcher(apiRoutes); - } - const isDefaultApiRoute = createRouteMatcher(DEFAULT_API_ROUTES); - return (req: WithExperimentalClerkUrl) => - isDefaultApiRoute(req) || isRequestMethodIndicatingApiRoute(req) || isRequestContentTypeJson(req); -}; - -const isRequestContentTypeJson = (req: NextRequest): boolean => { - const requestContentType = req.headers.get(constants.Headers.ContentType); - return requestContentType === constants.ContentTypes.Json; -}; - -const isRequestMethodIndicatingApiRoute = (req: NextRequest): boolean => { - const requestMethod = req.method.toLowerCase(); - return !['get', 'head', 'options'].includes(requestMethod); -}; - -const withNormalizedClerkUrl = ( - clerkRequest: ClerkRequest, - nextRequest: NextRequest, -): WithExperimentalClerkUrl => { - const res = nextRequest.nextUrl.clone(); - res.port = clerkRequest.clerkUrl.port; - res.protocol = clerkRequest.clerkUrl.protocol; - res.host = clerkRequest.clerkUrl.host; - return Object.assign(nextRequest, { experimental_clerkUrl: res }); -}; - -const informAboutProtectedRoute = ( - path: string, - options: AuthMiddlewareParams & { secretKey: string }, - isApiRoute: boolean, -) => { - if (options.debug || isDevelopmentFromSecretKey(options.secretKey)) { - console.warn( - informAboutProtectedRouteInfo( - path, - !!options.publicRoutes, - !!options.ignoredRoutes, - isApiRoute, - DEFAULT_IGNORED_ROUTES, - ), - ); - } -}; diff --git a/packages/nextjs/src/server/buildClerkProps.ts b/packages/nextjs/src/server/buildClerkProps.ts index f9cb3f41eb..a6f51aa068 100644 --- a/packages/nextjs/src/server/buildClerkProps.ts +++ b/packages/nextjs/src/server/buildClerkProps.ts @@ -1,17 +1,8 @@ -import type { Organization, Session, User } from '@clerk/backend'; -import { - AuthStatus, - constants, - makeAuthObjectSerializable, - signedInAuthObject, - signedOutAuthObject, - stripPrivateDataFromObject, -} from '@clerk/backend/internal'; -import { decodeJwt } from '@clerk/backend/jwt'; +import type { AuthObject, Organization, Session, User } from '@clerk/backend'; +import { makeAuthObjectSerializable, stripPrivateDataFromObject } from '@clerk/backend/internal'; -import { API_URL, API_VERSION, SECRET_KEY } from './constants'; +import { getAuthDataFromRequest } from './data/getAuthDataFromRequest'; import type { RequestLike } from './types'; -import { decryptClerkRequestData, getAuthKeyFromRequest, getHeader, injectSSRStateIntoObject } from './utils'; type BuildClerkPropsInitState = { user?: User | null; session?: Session | null; organization?: Organization | null }; @@ -33,34 +24,18 @@ type BuildClerkPropsInitState = { user?: User | null; session?: Session | null; */ type BuildClerkProps = (req: RequestLike, authState?: BuildClerkPropsInitState) => Record; -export const buildClerkProps: BuildClerkProps = (req, initState = {}) => { - const authStatus = getAuthKeyFromRequest(req, 'AuthStatus'); - const authToken = getAuthKeyFromRequest(req, 'AuthToken'); - const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); - const authReason = getAuthKeyFromRequest(req, 'AuthReason'); +export const buildClerkProps: BuildClerkProps = (req, initialState = {}) => { + const sanitizedAuthObject = getDynamicAuthData(req, initialState); - const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); - const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); - - const options = { - secretKey: decryptedRequestData.secretKey || SECRET_KEY, - apiUrl: API_URL, - apiVersion: API_VERSION, - authStatus, - authMessage, - authReason, - }; - - let authObject; - if (!authStatus || authStatus !== AuthStatus.SignedIn) { - authObject = signedOutAuthObject(options); - } else { - const jwt = decodeJwt(authToken as string); + // Serializing the state on dev env is a temp workaround for the following issue: + // https://github.com/vercel/next.js/discussions/11209|Next.js + const __clerk_ssr_state = + process.env.NODE_ENV !== 'production' ? JSON.parse(JSON.stringify(sanitizedAuthObject)) : sanitizedAuthObject; + return { __clerk_ssr_state }; +}; - // @ts-expect-error - TODO @nikos: Align types - authObject = signedInAuthObject(options, jwt.raw.text, jwt.payload); - } +export function getDynamicAuthData(req: RequestLike, initialState = {}) { + const authObject = getAuthDataFromRequest(req); - const sanitizedAuthObject = makeAuthObjectSerializable(stripPrivateDataFromObject({ ...authObject, ...initState })); - return injectSSRStateIntoObject({}, sanitizedAuthObject); -}; + return makeAuthObjectSerializable(stripPrivateDataFromObject({ ...authObject, ...initialState })) as AuthObject; +} diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index 84d3ba236a..59c05a5fe0 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -1,7 +1,5 @@ -import type { ClerkClient } from '@clerk/backend'; import { createClerkClient } from '@clerk/backend'; import { constants } from '@clerk/backend/internal'; -import { deprecated } from '@clerk/shared/deprecated'; import { buildRequestLike, isPrerenderingBailout } from '../app-router/server/utils'; import { clerkMiddlewareRequestDataStorage } from './clerkMiddleware'; @@ -38,21 +36,15 @@ const clerkClientDefaultOptions = { const createClerkClientWithOptions: typeof createClerkClient = options => createClerkClient({ ...clerkClientDefaultOptions, ...options }); -/** - * @deprecated - * This singleton is deprecated and will be removed in a future release. Please use `clerkClient()` as a function instead. - */ -const clerkClientSingleton = createClerkClient(clerkClientDefaultOptions); - /** * Constructs a BAPI client that accesses request data within the runtime. * Necessary if middleware dynamic keys are used. */ -const clerkClientForRequest = () => { +const clerkClient = async () => { let requestData; try { - const request = buildRequestLike(); + const request = await buildRequestLike(); const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData); requestData = decryptClerkRequestData(encryptedRequestData); } catch (err) { @@ -67,20 +59,7 @@ const clerkClientForRequest = () => { return createClerkClientWithOptions(options); } - return clerkClientSingleton; + return createClerkClientWithOptions({}); }; -interface ClerkClientExport extends ClerkClient { - (): ClerkClient; -} - -// TODO SDK-1839 - Remove `clerkClient` singleton in the next major version of `@clerk/nextjs` -const clerkClient = new Proxy(Object.assign(clerkClientForRequest, clerkClientSingleton), { - get(target, prop: string, receiver) { - deprecated('clerkClient singleton', 'Use `clerkClient()` as a function instead.'); - - return Reflect.get(target, prop, receiver); - }, -}) as ClerkClientExport; - export { clerkClient }; diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 592064c8a3..52ace3c8d6 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -12,6 +12,14 @@ import { withLogger } from '../utils/debugLogger'; import { clerkClient } from './clerkClient'; import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { errorThrower } from './errorThrower'; +import { + isNextjsNotFoundError, + isNextjsRedirectError, + isRedirectToSignInError, + nextjsNotFound, + nextjsRedirectError, + redirectToSignInError, +} from './nextErrors'; import type { AuthProtect } from './protect'; import { createProtect } from './protect'; import type { NextMiddlewareEvtParam, NextMiddlewareRequestParam, NextMiddlewareReturn } from './types'; @@ -23,18 +31,14 @@ import { setRequestHeadersOnNextResponse, } from './utils'; -const CONTROL_FLOW_ERROR = { - FORCE_NOT_FOUND: 'CLERK_PROTECT_REWRITE', - REDIRECT_TO_URL: 'CLERK_PROTECT_REDIRECT_TO_URL', - REDIRECT_TO_SIGN_IN: 'CLERK_PROTECT_REDIRECT_TO_SIGN_IN', -}; - export type ClerkMiddlewareAuthObject = AuthObject & { - protect: AuthProtect; redirectToSignIn: RedirectFun; }; -export type ClerkMiddlewareAuth = () => ClerkMiddlewareAuthObject; +export interface ClerkMiddlewareAuth { + (): ClerkMiddlewareAuthObject; + protect: AuthProtect; +} type ClerkMiddlewareHandler = ( auth: ClerkMiddlewareAuth, @@ -56,16 +60,19 @@ interface ClerkMiddleware { * export default clerkMiddleware((auth, request, event) => { ... }, options); */ (handler: ClerkMiddlewareHandler, options?: ClerkMiddlewareOptions): NextMiddleware; + /** * @example * export default clerkMiddleware((auth, request, event) => { ... }, (req) => options); */ (handler: ClerkMiddlewareHandler, options?: ClerkMiddlewareOptionsCallback): NextMiddleware; + /** * @example * export default clerkMiddleware(options); */ (options?: ClerkMiddlewareOptions): NextMiddleware; + /** * @example * export default clerkMiddleware; @@ -76,7 +83,8 @@ interface ClerkMiddleware { const clerkMiddlewareRequestDataStore = new Map<'requestData', AuthenticateRequestOptions>(); export const clerkMiddlewareRequestDataStorage = new AsyncLocalStorage(); -export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { +// @ts-expect-error TS is not happy here. Will dig into it +export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => { const [request, event] = parseRequestAndEvent(args); const [handler, params] = parseHandlerAndOptions(args); @@ -105,7 +113,9 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { // Propagates the request data to be accessed on the server application runtime from helpers such as `clerkClient` clerkMiddlewareRequestDataStore.set('requestData', options); - clerkClient().telemetry.record( + const resolvedClerkClient = await clerkClient(); + + resolvedClerkClient.telemetry.record( eventMethodCalled('clerkMiddleware', { handler: Boolean(handler), satellite: Boolean(options.isSatellite), @@ -120,7 +130,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { logger.debug('options', options); logger.debug('url', () => clerkRequest.toJSON()); - const requestState = await clerkClient().authenticateRequest( + const requestState = await resolvedClerkClient.authenticateRequest( clerkRequest, createAuthenticateRequestOptions(clerkRequest, options), ); @@ -142,14 +152,17 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { logger.debug('auth', () => ({ auth: authObject, debug: authObject.debug() })); const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest); - const protect = createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn); - const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { protect, redirectToSignIn }); + const protect = await createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn); + + const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { redirectToSignIn }); + const authHandler = () => authObjWithMethods; + authHandler.protect = protect; let handlerResult: Response = NextResponse.next(); try { const userHandlerResult = await clerkMiddlewareRequestDataStorage.run( clerkMiddlewareRequestDataStore, - async () => handler?.(() => authObjWithMethods, request, event), + async () => handler?.(authHandler, request, event), ); handlerResult = userHandlerResult || handlerResult; } catch (e: any) { @@ -220,9 +233,8 @@ const createMiddlewareRedirectToSignIn = ( clerkRequest: ClerkRequest, ): ClerkMiddlewareAuthObject['redirectToSignIn'] => { return (opts = {}) => { - const err = new Error(CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN) as any; - err.returnBackUrl = opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkRequest.clerkUrl.toString(); - throw err; + const url = clerkRequest.clerkUrl.toString(); + redirectToSignInError(url, opts.returnBackUrl); }; }; @@ -230,21 +242,17 @@ const createMiddlewareProtect = ( clerkRequest: ClerkRequest, authObject: AuthObject, redirectToSignIn: RedirectFun, -): ClerkMiddlewareAuthObject['protect'] => { - return ((params, options) => { - const notFound = () => { - throw new Error(CONTROL_FLOW_ERROR.FORCE_NOT_FOUND) as any; - }; - - const redirect = (url: string) => { - const err = new Error(CONTROL_FLOW_ERROR.REDIRECT_TO_URL) as any; - err.redirectUrl = url; - throw err; - }; - - // @ts-expect-error TS is not happy even though the types are correct +) => { + return (async (params: any, options: any) => { + const notFound = () => nextjsNotFound(); + + const redirect = (url: string) => + nextjsRedirectError(url, { + redirectUrl: url, + }); + return createProtect({ request: clerkRequest, redirect, notFound, authObject, redirectToSignIn })(params, options); - }) as AuthProtect; + }) as unknown as Promise; }; // Handle errors thrown by protect() and redirectToSignIn() calls, @@ -255,25 +263,28 @@ const createMiddlewareProtect = ( // This function handles the known errors thrown by the APIs described above, // and returns the appropriate response. const handleControlFlowErrors = (e: any, clerkRequest: ClerkRequest, requestState: RequestState): Response => { - switch (e.message) { - case CONTROL_FLOW_ERROR.FORCE_NOT_FOUND: - // Rewrite to a bogus URL to force not found error - return setHeader( - NextResponse.rewrite(`${clerkRequest.clerkUrl.origin}/clerk_${Date.now()}`), - constants.Headers.AuthReason, - 'protect-rewrite', - ); - case CONTROL_FLOW_ERROR.REDIRECT_TO_URL: - return redirectAdapter(e.redirectUrl); - case CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN: - return createRedirect({ - redirectAdapter, - baseUrl: clerkRequest.clerkUrl, - signInUrl: requestState.signInUrl, - signUpUrl: requestState.signUpUrl, - publishableKey: requestState.publishableKey, - }).redirectToSignIn({ returnBackUrl: e.returnBackUrl }); - default: - throw e; + if (isNextjsNotFoundError(e)) { + // Rewrite to a bogus URL to force not found error + return setHeader( + NextResponse.rewrite(`${clerkRequest.clerkUrl.origin}/clerk_${Date.now()}`), + constants.Headers.AuthReason, + 'protect-rewrite', + ); } + + if (isRedirectToSignInError(e)) { + return createRedirect({ + redirectAdapter, + baseUrl: clerkRequest.clerkUrl, + signInUrl: requestState.signInUrl, + signUpUrl: requestState.signUpUrl, + publishableKey: requestState.publishableKey, + }).redirectToSignIn({ returnBackUrl: e.returnBackUrl }); + } + + if (isNextjsRedirectError(e)) { + return redirectAdapter(e.redirectUrl); + } + + throw e; }; diff --git a/packages/nextjs/src/server/createGetAuth.ts b/packages/nextjs/src/server/createGetAuth.ts index 39b60a0d4d..c8acc34ff6 100644 --- a/packages/nextjs/src/server/createGetAuth.ts +++ b/packages/nextjs/src/server/createGetAuth.ts @@ -1,12 +1,13 @@ import type { AuthObject } from '@clerk/backend'; -import { AuthStatus, constants, signedInAuthObject, signedOutAuthObject } from '@clerk/backend/internal'; +import { constants } from '@clerk/backend/internal'; import { decodeJwt } from '@clerk/backend/jwt'; +import { isTruthy } from '@clerk/shared'; import { withLogger } from '../utils/debugLogger'; -import { API_URL, API_VERSION, SECRET_KEY } from './constants'; +import { getAuthDataFromRequest } from './data/getAuthDataFromRequest'; import { getAuthAuthHeaderMissing } from './errors'; import type { RequestLike } from './types'; -import { assertTokenSignature, decryptClerkRequestData, getAuthKeyFromRequest, getCookie, getHeader } from './utils'; +import { assertAuthStatus, getCookie, getHeader } from './utils'; export const createGetAuth = ({ noAuthStatusMessage, @@ -17,50 +18,13 @@ export const createGetAuth = ({ }) => withLogger(debugLoggerName, logger => { return (req: RequestLike, opts?: { secretKey?: string }): AuthObject => { - if (getHeader(req, constants.Headers.EnableDebug) === 'true') { + if (isTruthy(getHeader(req, constants.Headers.EnableDebug))) { logger.enable(); } - // When the auth status is set, we trust that the middleware has already run - // Then, we don't have to re-verify the JWT here, - // we can just strip out the claims manually. - const authToken = getAuthKeyFromRequest(req, 'AuthToken'); - const authSignature = getAuthKeyFromRequest(req, 'AuthSignature'); - const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); - const authReason = getAuthKeyFromRequest(req, 'AuthReason'); - const authStatus = getAuthKeyFromRequest(req, 'AuthStatus') as AuthStatus; - logger.debug('Headers debug', { authStatus, authMessage, authReason }); + assertAuthStatus(req, noAuthStatusMessage); - if (!authStatus) { - throw new Error(noAuthStatusMessage); - } - - const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); - const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); - - const options = { - authStatus, - apiUrl: API_URL, - apiVersion: API_VERSION, - authMessage, - secretKey: opts?.secretKey || decryptedRequestData.secretKey || SECRET_KEY, - authReason, - }; - - logger.debug('Options debug', options); - - if (authStatus === AuthStatus.SignedIn) { - assertTokenSignature(authToken as string, options.secretKey, authSignature); - - const jwt = decodeJwt(authToken as string); - - logger.debug('JWT debug', jwt.raw.text); - - // @ts-expect-error - TODO @nikos: Align types - return signedInAuthObject(options, jwt.raw.text, jwt.payload); - } - - return signedOutAuthObject(options); + return getAuthDataFromRequest(req, { ...opts, logger }); }; }); diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts new file mode 100644 index 0000000000..69cbe911ad --- /dev/null +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -0,0 +1,55 @@ +import type { AuthObject } from '@clerk/backend'; +import { AuthStatus, constants, signedInAuthObject, signedOutAuthObject } from '@clerk/backend/internal'; +import { decodeJwt } from '@clerk/backend/jwt'; + +import type { LoggerNoCommit } from '../../utils/debugLogger'; +import { API_URL, API_VERSION, SECRET_KEY } from '../constants'; +import type { RequestLike } from '../types'; +import { assertTokenSignature, decryptClerkRequestData, getAuthKeyFromRequest, getHeader } from '../utils'; + +/** + * Given a request object, builds an auth object from the request data. Used in server-side environments to get access + * to auth data for a given request. + */ +export function getAuthDataFromRequest( + req: RequestLike, + opts: { secretKey?: string; logger?: LoggerNoCommit } = {}, +): AuthObject { + const authStatus = getAuthKeyFromRequest(req, 'AuthStatus'); + const authToken = getAuthKeyFromRequest(req, 'AuthToken'); + const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); + const authReason = getAuthKeyFromRequest(req, 'AuthReason'); + const authSignature = getAuthKeyFromRequest(req, 'AuthSignature'); + + opts.logger?.debug('headers', { authStatus, authMessage, authReason }); + + const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); + const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); + + const options = { + secretKey: opts?.secretKey || decryptedRequestData.secretKey || SECRET_KEY, + apiUrl: API_URL, + apiVersion: API_VERSION, + authStatus, + authMessage, + authReason, + }; + + opts.logger?.debug('auth options', options); + + let authObject; + if (!authStatus || authStatus !== AuthStatus.SignedIn) { + authObject = signedOutAuthObject(options); + } else { + assertTokenSignature(authToken as string, options.secretKey, authSignature); + + const jwt = decodeJwt(authToken as string); + + opts.logger?.debug('jwt', jwt.raw); + + // @ts-expect-error -- Restrict parameter type of options to only list what's needed + authObject = signedInAuthObject(options, jwt.raw.text, jwt.payload); + } + + return authObject; +} diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index eba3589e23..fa179ff167 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -2,7 +2,7 @@ export const missingDomainAndProxy = ` Missing domain and proxyUrl. A satellite application needs to specify a domain or a proxyUrl. 1) With middleware - e.g. export default clerkMiddleware({domain:'YOUR_DOMAIN',isSatellite:true}); // or the deprecated authMiddleware() + e.g. export default clerkMiddleware({domain:'YOUR_DOMAIN',isSatellite:true}); 2) With environment variables e.g. NEXT_PUBLIC_CLERK_DOMAIN='YOUR_DOMAIN' NEXT_PUBLIC_CLERK_IS_SATELLITE='true' @@ -13,26 +13,16 @@ Invalid signInUrl. A satellite application requires a signInUrl for development Check if signInUrl is missing from your configuration or if it is not an absolute URL 1) With middleware - e.g. export default clerkMiddleware({signInUrl:'SOME_URL', isSatellite:true}); // or the deprecated authMiddleware() + e.g. export default clerkMiddleware({signInUrl:'SOME_URL', isSatellite:true}); 2) With environment variables e.g. NEXT_PUBLIC_CLERK_SIGN_IN_URL='SOME_URL' NEXT_PUBLIC_CLERK_IS_SATELLITE='true'`; -export const receivedRequestForIgnoredRoute = (url: string, matcher: string) => - `Clerk: The middleware was skipped for this request URL: ${url}. For performance reasons, it's recommended to your middleware matcher to: -export const config = { - matcher: ${matcher}, -}; - -Alternatively, you can set your own ignoredRoutes. See https://clerk.com/docs/nextjs/middleware -(This log only appears in development mode) -`; - export const getAuthAuthHeaderMissing = () => authAuthHeaderMissing('getAuth'); export const authAuthHeaderMissing = (helperName = 'auth') => - `Clerk: ${helperName}() was called but Clerk can't detect usage of clerkMiddleware() (or the deprecated authMiddleware()). Please ensure the following: -- clerkMiddleware() (or the deprecated authMiddleware()) is used in your Next.js Middleware. + `Clerk: ${helperName}() was called but Clerk can't detect usage of clerkMiddleware(). Please ensure the following: +- clerkMiddleware() is used in your Next.js Middleware. - Your Middleware matcher is configured to match this route or page. - If you are using the src directory, make sure the Middleware file is inside of it. @@ -48,54 +38,6 @@ To resolve this issue, make sure your system's clock is set to the correct time ${verifyMessage}`; -export const infiniteRedirectLoopDetected = () => - `Clerk: Infinite redirect loop detected. That usually means that we were not able to determine the auth state for this request. A list of common causes and solutions follows. - -Reason 1: -Your Clerk instance keys are incorrect, or you recently changed keys (Publishable Key, Secret Key). -How to resolve: --> Make sure you're using the correct keys from the Clerk Dashboard. If you changed keys recently, make sure to clear your browser application data and cookies. - -Reason 2: -A bug that may have already been fixed in the latest version of Clerk NextJS package. -How to resolve: --> Make sure you are using the latest version of '@clerk/nextjs' and 'next'. -`; - -export const informAboutProtectedRouteInfo = ( - path: string, - hasPublicRoutes: boolean, - hasIgnoredRoutes: boolean, - isApiRoute: boolean, - defaultIgnoredRoutes: string[], -) => { - const infoText = isApiRoute - ? `INFO: Clerk: The request to ${path} is being protected (401) because there is no signed-in user, and the path is included in \`apiRoutes\`. To prevent this behavior, choose one of:` - : `INFO: Clerk: The request to ${path} is being redirected because there is no signed-in user, and the path is not included in \`ignoredRoutes\` or \`publicRoutes\`. To prevent this behavior, choose one of:`; - const apiRoutesText = isApiRoute - ? `To prevent Clerk authentication from protecting (401) the api route, remove the rule matching "${path}" from the \`apiRoutes\` array passed to authMiddleware` - : undefined; - const publicRoutesText = hasPublicRoutes - ? `To make the route accessible to both signed in and signed out users, add "${path}" to the \`publicRoutes\` array passed to authMiddleware` - : `To make the route accessible to both signed in and signed out users, pass \`publicRoutes: ["${path}"]\` to authMiddleware`; - const ignoredRoutes = [...defaultIgnoredRoutes, path].map(r => `"${r}"`).join(', '); - const ignoredRoutesText = hasIgnoredRoutes - ? `To prevent Clerk authentication from running at all, add "${path}" to the \`ignoredRoutes\` array passed to authMiddleware` - : `To prevent Clerk authentication from running at all, pass \`ignoredRoutes: [${ignoredRoutes}]\` to authMiddleware`; - const afterAuthText = - "Pass a custom `afterAuth` to authMiddleware, and replace Clerk's default behavior of redirecting unless a route is included in publicRoutes"; - - return `${infoText} - -${[apiRoutesText, publicRoutesText, ignoredRoutesText, afterAuthText] - .filter(Boolean) - .map((text, index) => `${index + 1}. ${text}`) - .join('\n')} - -For additional information about middleware, please visit https://clerk.com/docs/nextjs/middleware -(This log only appears in development mode, or if \`debug: true\` is passed to authMiddleware)`; -}; - export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. For more information, see: https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys. (code=encryption_key_invalid)`; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index cbfa2e800b..f7f4e07321 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -63,10 +63,3 @@ export type { Token, User, } from '@clerk/backend'; - -/** - * Deprecated APIs - * These APIs will be removed in v6 - */ -export { authMiddleware } from './authMiddleware'; -export { redirectToSignIn, redirectToSignUp } from './redirectHelpers'; diff --git a/packages/nextjs/src/server/nextErrors.ts b/packages/nextjs/src/server/nextErrors.ts new file mode 100644 index 0000000000..c0594fd3a3 --- /dev/null +++ b/packages/nextjs/src/server/nextErrors.ts @@ -0,0 +1,111 @@ +/** + * Clerk's identifiers that are used alongside the ones from Next.js + */ +const CONTROL_FLOW_ERROR = { + FORCE_NOT_FOUND: 'CLERK_PROTECT_REWRITE', + REDIRECT_TO_URL: 'CLERK_PROTECT_REDIRECT_TO_URL', + REDIRECT_TO_SIGN_IN: 'CLERK_PROTECT_REDIRECT_TO_SIGN_IN', +}; + +/** + * In-house implementation of `notFound()` + * https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/not-found.ts + */ +const NOT_FOUND_ERROR_CODE = 'NEXT_NOT_FOUND'; + +type NotFoundError = Error & { + digest: typeof NOT_FOUND_ERROR_CODE; + clerk_digest: typeof CONTROL_FLOW_ERROR.FORCE_NOT_FOUND; +}; + +function isNextjsNotFoundError(error: unknown): error is NotFoundError { + if (typeof error !== 'object' || error === null || !('digest' in error)) { + return false; + } + + return error.digest === NOT_FOUND_ERROR_CODE; +} + +function nextjsNotFound(): never { + const error = new Error(NOT_FOUND_ERROR_CODE); + (error as NotFoundError).digest = NOT_FOUND_ERROR_CODE; + (error as NotFoundError).clerk_digest = CONTROL_FLOW_ERROR.FORCE_NOT_FOUND; + throw error; +} + +/** + * In-house implementation of `redirect()` + * https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/redirect.ts + */ + +const REDIRECT_ERROR_CODE = 'NEXT_REDIRECT'; + +type RedirectError = Error & { + digest: `${typeof REDIRECT_ERROR_CODE};${'replace'};${string};${307};`; + clerk_digest: typeof CONTROL_FLOW_ERROR.REDIRECT_TO_URL | typeof CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN; +} & T; + +function nextjsRedirectError( + url: string, + extra: Record, + type: 'replace' = 'replace', + statusCode: 307 = 307, +): never { + const error = new Error(REDIRECT_ERROR_CODE) as RedirectError; + error.digest = `${REDIRECT_ERROR_CODE};${type};${url};${statusCode};`; + error.clerk_digest = CONTROL_FLOW_ERROR.REDIRECT_TO_URL; + Object.assign(error, extra); + throw error; +} + +function redirectToSignInError(url: string, returnBackUrl?: string | URL | null): never { + nextjsRedirectError(url, { + clerk_digest: CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN, + returnBackUrl: returnBackUrl === null ? '' : returnBackUrl || url, + }); +} + +/** + * Checks an error to determine if it's an error generated by the + * `redirect(url)` helper. + * + * @param error the error that may reference a redirect error + * @returns true if the error is a redirect error + */ +function isNextjsRedirectError(error: unknown): error is RedirectError<{ redirectUrl: string | URL }> { + if (typeof error !== 'object' || error === null || !('digest' in error) || typeof error.digest !== 'string') { + return false; + } + + const digest = error.digest.split(';'); + const [errorCode, type] = digest; + const destination = digest.slice(2, -2).join(';'); + const status = digest.at(-2); + + const statusCode = Number(status); + + return ( + errorCode === REDIRECT_ERROR_CODE && + (type === 'replace' || type === 'push') && + typeof destination === 'string' && + !isNaN(statusCode) && + statusCode === 307 + ); +} + +function isRedirectToSignInError(error: unknown): error is RedirectError<{ returnBackUrl: string | URL }> { + if (isNextjsRedirectError(error) && 'clerk_digest' in error) { + return error.clerk_digest === CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN; + } + + return false; +} + +export { + isNextjsNotFoundError, + nextjsNotFound, + redirectToSignInError, + nextjsRedirectError, + isNextjsRedirectError, + isRedirectToSignInError, +}; diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index 0d9721aa8c..463dc8db3b 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -15,17 +15,17 @@ type AuthProtectOptions = { unauthorizedUrl?: string; unauthenticatedUrl?: strin * Throws a Nextjs notFound error if user is not authenticated or authorized. */ export interface AuthProtect { - (params?: CheckAuthorizationParamsWithCustomPermissions, options?: AuthProtectOptions): SignedInAuthObject; + (params?: CheckAuthorizationParamsWithCustomPermissions, options?: AuthProtectOptions): Promise; ( params?: (has: CheckAuthorizationWithCustomPermissions) => boolean, options?: AuthProtectOptions, - ): SignedInAuthObject; + ): Promise; - (options?: AuthProtectOptions): SignedInAuthObject; + (options?: AuthProtectOptions): Promise; } -export const createProtect = (opts: { +export function createProtect(opts: { request: Request; authObject: AuthObject; /** @@ -44,10 +44,10 @@ export const createProtect = (opts: { * use this callback to customise the behavior */ redirectToSignIn: RedirectFun; -}): AuthProtect => { +}): AuthProtect { const { redirectToSignIn, authObject, redirect, notFound, request } = opts; - return ((...args: any[]) => { + return (async (...args: any[]) => { const optionValuesAsParam = args[0]?.unauthenticatedUrl || args[0]?.unauthorizedUrl; const paramsOrFunction = optionValuesAsParam ? undefined @@ -108,7 +108,7 @@ export const createProtect = (opts: { return handleUnauthorized(); }) as AuthProtect; -}; +} const isServerActionRequest = (req: Request) => { return ( diff --git a/packages/nextjs/src/server/redirectHelpers.ts b/packages/nextjs/src/server/redirectHelpers.ts deleted file mode 100644 index 8a95633588..0000000000 --- a/packages/nextjs/src/server/redirectHelpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { constants, createRedirect } from '@clerk/backend/internal'; -import { NextResponse } from 'next/server'; - -import { setHeader } from '../utils'; -import { PUBLISHABLE_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; - -const redirectAdapter = (url: string) => { - const res = NextResponse.redirect(url); - return setHeader(res, constants.Headers.ClerkRedirectTo, 'true'); -}; - -const redirectHelpers = createRedirect({ - redirectAdapter, - signInUrl: SIGN_IN_URL, - signUpUrl: SIGN_UP_URL, - publishableKey: PUBLISHABLE_KEY, - // We're setting baseUrl to '' here as we want to keep the legacy behavior of - // the redirectToSignIn, redirectToSignUp helpers in the backend package. - baseUrl: '', -}); - -/** - * @deprecated - * This function is deprecated and will be removed in a future release. Please use `auth().redirectToSignIn()` instead. - */ -export const redirectToSignIn = redirectHelpers.redirectToSignIn; - -/** - * @deprecated - * This function is deprecated and will be removed in a future release. Please use `auth().redirectToSignIn()` instead. - */ -export const redirectToSignUp = redirectHelpers.redirectToSignUp; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 29b558f2e4..90c2a0cb4c 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -92,15 +92,6 @@ export const setRequestHeadersOnNextResponse = ( }); }; -export const injectSSRStateIntoObject = (obj: O, authObject: T) => { - // Serializing the state on dev env is a temp workaround for the following issue: - // https://github.com/vercel/next.js/discussions/11209|Next.js - const __clerk_ssr_state = ( - process.env.NODE_ENV !== 'production' ? JSON.parse(JSON.stringify({ ...authObject })) : { ...authObject } - ) as T; - return { ...obj, __clerk_ssr_state }; -}; - // Auth result will be set as both a query param & header when applicable export function decorateRequest( req: ClerkRequest, @@ -196,6 +187,14 @@ export const redirectAdapter = (url: string | URL) => { return NextResponse.redirect(url, { headers: { [constants.Headers.ClerkRedirectTo]: 'true' } }); }; +export function assertAuthStatus(req: RequestLike, error: string) { + const authStatus = getAuthKeyFromRequest(req, 'AuthStatus'); + + if (!authStatus) { + throw new Error(error); + } +} + export function assertKey(key: string, onError: () => never): string { if (!key) { onError(); diff --git a/packages/nextjs/src/types.ts b/packages/nextjs/src/types.ts index d0eb56cbe6..96a89d74e5 100644 --- a/packages/nextjs/src/types.ts +++ b/packages/nextjs/src/types.ts @@ -16,4 +16,10 @@ export type NextClerkProviderProps = Without (path: string) => { describe('nextjs matcher', () => { /** * 🚨🚨🚨🚨 - * This is the matcher we document for clerkMiddleware + authMiddleware. + * This is the matcher we document for clerkMiddleware. * Any change made to the matcher here needs to be reflected in the documentation, the dashboard * and vice versa. * 🚨🚨🚨🚨 diff --git a/packages/nextjs/src/utils/__tests__/response.test.ts b/packages/nextjs/src/utils/__tests__/response.test.ts deleted file mode 100644 index fd7d85056f..0000000000 --- a/packages/nextjs/src/utils/__tests__/response.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { NextResponse } from 'next/server'; - -import { mergeResponses } from '../response'; - -describe('mergeResponses', function () { - it('should fail unless one response is passed', function () { - expect(mergeResponses(null, undefined)).toBe(undefined); - }); - - it('should handle non-response values', function () { - const response1 = new NextResponse(); - response1.headers.set('foo', '1'); - const finalResponse = mergeResponses(null, undefined, response1); - expect(finalResponse!.headers.get('foo')).toEqual('1'); - }); - - it('should merge the headers', function () { - const response1 = new NextResponse(); - const response2 = new NextResponse(); - response1.headers.set('foo', '1'); - response1.headers.set('bar', '1'); - response2.headers.set('bar', '2'); - const finalResponse = mergeResponses(response1, response2); - expect(finalResponse!.headers.get('foo')).toEqual('1'); - expect(finalResponse!.headers.get('bar')).toEqual('2'); - }); - - it('should merge the cookies', function () { - const response1 = new NextResponse(); - const response2 = new NextResponse(); - response1.cookies.set('foo', '1'); - response1.cookies.set('second', '2'); - response1.cookies.set('bar', '1'); - response2.cookies.set('bar', '2'); - const finalResponse = mergeResponses(response1, response2); - expect(finalResponse!.cookies.get('foo')).toEqual(response1.cookies.get('foo')); - expect(finalResponse!.cookies.get('second')).toEqual(response1.cookies.get('second')); - expect(finalResponse!.cookies.get('bar')).toEqual(response2.cookies.get('bar')); - }); - - it('should merge the cookies with non-response values', function () { - const response2 = NextResponse.next(); - response2.cookies.set('foo', '1'); - response2.cookies.set({ - name: 'second', - value: '2', - path: '/', - sameSite: 'none', - secure: true, - }); - response2.cookies.set('bar', '1', { - sameSite: 'none', - secure: true, - }); - const finalResponse = mergeResponses(null, response2); - expect(finalResponse!.cookies.get('foo')).toEqual(response2.cookies.get('foo')); - expect(finalResponse!.cookies.get('second')).toEqual(response2.cookies.get('second')); - expect(finalResponse!.cookies.get('bar')).toEqual(response2.cookies.get('bar')); - }); - - it('should use the status of the last response', function () { - const response1 = new NextResponse('', { status: 200, statusText: 'OK' }); - const response2 = new NextResponse('', { status: 201, statusText: 'Created' }); - const finalResponse = mergeResponses(response1, response2); - expect(finalResponse!.status).toEqual(response2.status); - expect(finalResponse!.statusText).toEqual(response2.statusText); - }); - - it('should use the body of the last response', function () { - const response1 = new NextResponse('1'); - const response2 = new NextResponse('2'); - const finalResponse = mergeResponses(response1, response2); - expect(finalResponse!.body).toEqual(response2.body); - }); -}); diff --git a/packages/nextjs/src/utils/debugLogger.ts b/packages/nextjs/src/utils/debugLogger.ts index d2ddab1604..9d2ca496d8 100644 --- a/packages/nextjs/src/utils/debugLogger.ts +++ b/packages/nextjs/src/utils/debugLogger.ts @@ -11,6 +11,7 @@ export type Logger = { debug: (...args: Array L)>) => void; enable: () => void; }; +export type LoggerNoCommit = Omit; export const createDebugLogger = (name: string, formatter: (val: LogEntry) => string) => (): Logger => { const entries: LogEntry[] = []; @@ -57,7 +58,7 @@ export const createDebugLogger = (name: string, formatter: (val: LogEntry) => st type WithLogger = any>( loggerFactoryOrName: string | (() => L), - handlerCtor: (logger: Omit) => H, + handlerCtor: (logger: LoggerNoCommit) => H, ) => H; export const withLogger: WithLogger = (loggerFactoryOrName, handlerCtor) => { diff --git a/packages/nextjs/src/utils/response.ts b/packages/nextjs/src/utils/response.ts index 7cb87b535d..56c11df06b 100644 --- a/packages/nextjs/src/utils/response.ts +++ b/packages/nextjs/src/utils/response.ts @@ -1,44 +1,5 @@ -import { NextResponse } from 'next/server'; - import { constants as nextConstants } from '../constants'; -/** - * A function that merges two Response objects into a single response. - * The final response respects the body and the status of the last response, - * but the cookies and headers of all responses are merged. - */ -export const mergeResponses = (...responses: (NextResponse | Response | null | undefined | void)[]) => { - const normalisedResponses = responses.filter(Boolean).map(res => { - // If the response is a NextResponse, we can just return it - if (res instanceof NextResponse) { - return res; - } - - return new NextResponse(res!.body, res!); - }); - - if (normalisedResponses.length === 0) { - return; - } - - const lastResponse = normalisedResponses[normalisedResponses.length - 1]; - const finalResponse = new NextResponse(lastResponse.body, lastResponse); - - for (const response of normalisedResponses) { - response.headers.forEach((value: string, name: string) => { - finalResponse.headers.set(name, value); - }); - - response.cookies.getAll().forEach(cookie => { - const { name, value, ...options } = cookie; - - finalResponse.cookies.set(name, value, options); - }); - } - - return finalResponse; -}; - export const isRedirect = (res: Response) => { return res.headers.get(nextConstants.Headers.NextRedirect); }; diff --git a/packages/react/src/contexts/AuthContext.ts b/packages/react/src/contexts/AuthContext.ts index a17cfb43df..db59ac0fe7 100644 --- a/packages/react/src/contexts/AuthContext.ts +++ b/packages/react/src/contexts/AuthContext.ts @@ -1,7 +1,7 @@ import { createContextAndHook } from '@clerk/shared/react'; import type { ActJWTClaim, OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from '@clerk/types'; -export const [AuthContext, useAuthContext] = createContextAndHook<{ +export type AuthContextValue = { userId: string | null | undefined; sessionId: string | null | undefined; actor: ActJWTClaim | null | undefined; @@ -10,4 +10,6 @@ export const [AuthContext, useAuthContext] = createContextAndHook<{ orgSlug: string | null | undefined; orgPermissions: OrganizationCustomPermissionKey[] | null | undefined; __experimental_factorVerificationAge: [number, number] | null; -}>('AuthContext'); +}; + +export const [AuthContext, useAuthContext] = createContextAndHook('AuthContext'); diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index d3aed86858..9cfbac8bdd 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -6,7 +6,7 @@ import type { OrganizationCustomRoleKey, SignOut, } from '@clerk/types'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useAuthContext } from '../contexts/AuthContext'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; @@ -72,7 +72,7 @@ type UseAuthReturn = getToken: GetToken; }; -type UseAuth = () => UseAuthReturn; +type UseAuth = (initialAuthState?: any) => UseAuthReturn; /** * Returns the current auth state, the user and session ids and the `getToken` @@ -110,11 +110,28 @@ type UseAuth = () => UseAuthReturn; * return
...
* } */ -export const useAuth: UseAuth = () => { +export const useAuth: UseAuth = (initialAuthState = {}) => { useAssertWrappedByClerkProvider('useAuth'); + const authContext = useAuthContext(); + + const [authState, setAuthState] = useState(() => { + // This indicates the authContext is not available, and so we fallback to the provided initialState + if (authContext.sessionId === undefined && authContext.userId === undefined) { + return initialAuthState ?? {}; + } + return authContext; + }); + + useEffect(() => { + if (authContext.sessionId === undefined && authContext.userId === undefined) { + return; + } + setAuthState(authContext); + }, [authContext]); + const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions, __experimental_factorVerificationAge } = - useAuthContext(); + authState; const isomorphicClerk = useIsomorphicClerkContext(); const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); @@ -133,6 +150,12 @@ export const useAuth: UseAuth = () => { [userId, __experimental_factorVerificationAge, orgId, orgRole, orgPermissions], ); + return useDerivedAuth({ sessionId, userId, actor, orgId, orgSlug, orgRole, getToken, signOut, has }); +}; + +export function useDerivedAuth(authObject: any): UseAuthReturn { + const { sessionId, userId, actor, orgId, orgSlug, orgRole, has, signOut, getToken } = authObject; + if (sessionId === undefined && userId === undefined) { return { isLoaded: false, @@ -198,4 +221,4 @@ export const useAuth: UseAuth = () => { } return errorThrower.throw(invalidStateError); -}; +} diff --git a/packages/react/src/internal.ts b/packages/react/src/internal.ts index 938f3f4d9d..3ee8e579cc 100644 --- a/packages/react/src/internal.ts +++ b/packages/react/src/internal.ts @@ -1,6 +1,7 @@ export { setErrorThrowerOptions } from './errors/errorThrower'; export { MultisessionAppSupport } from './components/controlComponents'; export { useRoutingProps } from './hooks/useRoutingProps'; +export { useDerivedAuth } from './hooks/useAuth'; export { clerkJsScriptUrl, diff --git a/packages/upgrade/package.json b/packages/upgrade/package.json index 7dea16c548..1a1156cd2d 100644 --- a/packages/upgrade/package.json +++ b/packages/upgrade/package.json @@ -16,11 +16,12 @@ "dist" ], "scripts": { - "build": "npm run clean && NODE_ENV=production babel --out-dir=dist src --copy-files", + "build": "npm run clean && NODE_ENV=production babel --keep-file-extension --out-dir=dist src --copy-files", "clean": "del-cli dist/*", - "dev": "babel --out-dir=dist --watch src --copy-files", + "dev": "babel --keep-file-extension --out-dir=dist --watch src --copy-files", "lint": "eslint src/", - "lint:publint": "publint" + "lint:publint": "publint", + "test": "vitest" }, "babel": { "presets": [ @@ -28,16 +29,18 @@ ] }, "dependencies": { - "@inkjs/ui": "^1.0.0", + "@inkjs/ui": "^2.0.0", "@jescalan/ink-markdown": "^2.0.0", "ejs": "3.1.10", + "execa": "9.4.1", "globby": "^14.0.1", "gray-matter": "^4.0.3", "index-to-position": "^0.1.2", - "ink": "^4.4.1", + "ink": "^5.0.1", "ink-big-text": "^2.0.0", "ink-gradient": "^3.0.0", - "ink-link": "^3.0.0", + "ink-link": "^4.1.0", + "jscodeshift": "^17.0.0", "marked": "^11.1.1", "meow": "^11.0.0", "react": "^18.3.1", @@ -48,9 +51,10 @@ "devDependencies": { "@babel/cli": "^7.24.7", "@babel/preset-react": "^7.24.7", - "chalk": "^5.3.0", + "@types/jscodeshift": "^0.12.0", "del-cli": "^5.1.0", - "eslint-config-custom": "*" + "eslint-config-custom": "*", + "vitest": "^2.1.3" }, "engines": { "node": ">=18.17.0" diff --git a/packages/upgrade/src/app.js b/packages/upgrade/src/app.js index 332530a2dc..94df0bd23f 100644 --- a/packages/upgrade/src/app.js +++ b/packages/upgrade/src/app.js @@ -1,37 +1,47 @@ import { MultiSelect, Select, TextInput } from '@inkjs/ui'; -import { Newline, Text } from 'ink'; -import BigText from 'ink-big-text'; -import Gradient from 'ink-gradient'; +import { Newline, Text, useApp } from 'ink'; import React, { useState } from 'react'; +import { Header } from './components/Header.js'; +import { Scan } from './components/Scan.js'; +import { SDKWorkflow } from './components/SDKWorkflow.js'; import SDKS from './constants/sdks.js'; -import Scan from './scan.js'; -import getClerkMajorVersion from './util/get-clerk-version.js'; +import { getClerkMajorVersion } from './util/get-clerk-version.js'; import guessFrameworks from './util/guess-framework.js'; -export default function App({ - _fromVersion, - _toVersion, - _sdk, - _dir = false, - _ignore = [], - _yolo = false, - noWarnings = false, - disableTelemetry = false, -}) { - const [yolo, setYolo] = useState(_yolo); - const [sdks, setSdks] = useState(_sdk ? [_sdk] : []); +/** + * Main CLI application component for handling Clerk SDK upgrades. + * + * @param {Object} props - The `props` object. + * @param {string} [props.dir] - The directory to scan for files. + * @param {boolean} [props.disableTelemetry=false] - Flag to disable telemetry. + * @param {string} [props.fromVersion] - The current version of the SDK. + * @param {Array} [props.ignore] - List of files or directories to ignore. + * @param {boolean} [props.noWarnings=false] - Flag to disable warnings. + * @param {string} [props.sdk] - The SDK to upgrade. + * @param {string} [props.toVersion] - The target version of the SDK. + * @param {boolean} [props.yolo=false] - Flag to enable YOLO mode. + * + * @returns {JSX.Element} The rendered component. + */ +export default function App(props) { + const { noWarnings = false, disableTelemetry = false } = props; + const { exit } = useApp(); + + const [yolo, setYolo] = useState(props.yolo ?? false); + const [sdks, setSdks] = useState(props.sdk ? [props.sdk] : []); const [sdkGuesses, setSdkGuesses] = useState([]); const [sdkGuessConfirmed, setSdkGuessConfirmed] = useState(false); const [sdkGuessAttempted, setSdkGuessAttempted] = useState(false); // See comments below, can be enabled on next major + // eslint-disable-next-line no-unused-vars - const [fromVersion, setFromVersion] = useState(_fromVersion); + const [fromVersion, setFromVersion] = useState(props.fromVersion); const [fromVersionGuessAttempted, setFromVersionGuessAttempted] = useState(false); // eslint-disable-next-line no-unused-vars - const [toVersion, setToVersion] = useState(_toVersion); - const [dir, setDir] = useState(_dir); - const [ignore, setIgnore] = useState(_ignore); + const [toVersion, setToVersion] = useState(props.toVersion); + const [dir, setDir] = useState(props.dir); + const [ignore, setIgnore] = useState(props.ignore); const [configComplete, setConfigComplete] = useState(false); const [configVerified, setConfigVerified] = useState(false); const [uuid, setUuid] = useState(); @@ -42,20 +52,29 @@ export default function App({ setYolo(false); } + // Handle the individual SDK upgrade + if (sdks.length === 1) { + return ( + + ); + } + // We try to guess which SDK they are using if (isEmpty(sdks) && isEmpty(sdkGuesses) && !sdkGuessAttempted) { if (!dir) { return setDir(process.cwd()); } const { guesses, _uuid } = guessFrameworks(dir, disableTelemetry); - console.log({ guesses, _uuid }); setUuid(_uuid); setSdkGuesses(guesses); setSdkGuessAttempted(true); } // We try to guess which version of Clerk they are using - if (!fromVersion && !fromVersionGuess && !fromVersionGuessAttempted) { + if (isEmpty(sdks) && !fromVersion && !fromVersionGuess && !fromVersionGuessAttempted) { fromVersionGuess = getClerkMajorVersion(); setFromVersionGuessAttempted(true); } @@ -72,20 +91,14 @@ export default function App({ return ( <> - - - +
{/* Welcome to the upgrade script! */} {!configComplete && ( <> - Hello friend! We're excited to help you upgrade Clerk - {fromVersion ? ` from ${fromVersion}` : ''} - {toVersion ? ` to ${toVersion}` : ''}. Before we get started, a couple questions... + Hello friend! We're excited to help you upgrade Clerk modules. Before we get + started, a couple questions... @@ -120,6 +133,8 @@ export default function App({ // if true, we were right so we set the sdk if (item === 'yes') { setSdks(sdkGuesses.map(guess => guess.value)); + } else { + setSdkGuesses([]); } }} /> @@ -186,7 +201,7 @@ export default function App({ /> )} */} - {!isEmpty(sdks) > 0 && fromVersion && toVersion && !dir && ( + {!isEmpty(sdks) && fromVersion && toVersion && !dir && ( <> Where would you like for us to scan for files in your project? (globstar syntax supported) @@ -243,12 +258,12 @@ export default function App({ Does this look right? { + if (value === 'yes') { + setUpgradeComplete(true); + } else { + setDone(true); + } + }} + /> + )} + + )} + {done && ( + + Done upgrading @clerk/nextjs + + )} + + ); +} + +/** + * Component that runs an upgrade command for a given SDK and handles the result. + * + * @component + * @param {Object} props + * @param {Function} props.callback - The callback function to be called after the command execution. + * @param {string} props.sdk - The SDK for which the upgrade command is run. + * @returns {JSX.Element} The rendered component. + * + * @example + * + */ +function UpgradeCommand({ callback, sdk }) { + const [error, setError] = useState(); + const [result, setResult] = useState(); + + const command = getUpgradeCommand(sdk); + + useEffect(() => { + execa({ shell: true })`${command}` + .then(res => { + setResult(res); + callback(true); + }) + .catch(err => { + setError(err); + }); + }, [command]); + + return ( + <> + {!result && !error && } + {result && ( + + @clerk/{sdk} upgraded successfully to latest! + + )} + {error && Upgrade failed!} + + ); +} diff --git a/packages/upgrade/src/scan.js b/packages/upgrade/src/components/Scan.js similarity index 97% rename from packages/upgrade/src/scan.js rename to packages/upgrade/src/components/Scan.js index 100317ff63..a0d06af6d7 100644 --- a/packages/upgrade/src/scan.js +++ b/packages/upgrade/src/components/Scan.js @@ -6,9 +6,9 @@ import { Newline, Text } from 'ink'; import path from 'path'; import React, { useEffect, useState } from 'react'; -import ExpandableList from './util/expandable-list.js'; +import ExpandableList from '../util/expandable-list.js'; -export default function Scan({ fromVersion, toVersion, sdks, dir, ignore, noWarnings, uuid, disableTelemetry }) { +export function Scan({ fromVersion, toVersion, sdks, dir, ignore, noWarnings, uuid, disableTelemetry }) { // NOTE: if the difference between fromVersion and toVersion is greater than 1 // we need to do a little extra work here and import two matchers, // sequence them after each other, and clearly mark which version migration diff --git a/packages/upgrade/src/util/detect-package-manager.js b/packages/upgrade/src/util/detect-package-manager.js new file mode 100644 index 0000000000..5ba8e8f7ae --- /dev/null +++ b/packages/upgrade/src/util/detect-package-manager.js @@ -0,0 +1,24 @@ +import { existsSync } from 'fs'; + +export function detectPackageManager() { + if (existsSync('package-lock.json')) { + return 'npm'; + } else if (existsSync('yarn.lock')) { + return 'yarn'; + } else if (existsSync('pnpm-lock.yaml')) { + return 'pnpm'; + } else { + return 'npm'; + } +} + +export function getUpgradeCommand(sdk, packageManager) { + switch (packageManager || detectPackageManager()) { + case 'yarn': + return `yarn add @clerk/${sdk}@latest`; + case 'pnpm': + return `pnpm add @clerk/${sdk}@latest`; + default: + return `npm install @clerk/${sdk}@latest`; + } +} diff --git a/packages/upgrade/src/util/get-clerk-version.js b/packages/upgrade/src/util/get-clerk-version.js index 98baeddc9f..316aefeb54 100644 --- a/packages/upgrade/src/util/get-clerk-version.js +++ b/packages/upgrade/src/util/get-clerk-version.js @@ -1,8 +1,31 @@ import { readPackageSync } from 'read-pkg'; import semverRegex from 'semver-regex'; -export default function getClerkMajorVersion() { +export function getClerkMajorVersion() { const pkg = readPackageSync(); const clerk = pkg.dependencies.clerk; - return clerk ? semverRegex.exec(clerk)[0][0] : false; + return clerk ? semverRegex().exec(clerk)[0][0] : false; +} + +export function getClerkSdkVersion(sdk) { + const pkg = readPackageSync(); + const clerkSdk = pkg.dependencies[`@clerk/${sdk}`]; + if (!clerkSdk) { + return false; + } + + try { + return getMajorVersion(clerkSdk.replace('^', '').replace('~', '')); + } catch { + return false; + } +} + +function getMajorVersion(semver) { + const match = semver.match(semverRegex()); + if (match) { + const [major] = match[0].split('.'); + return parseInt(major, 10); // Return as an integer + } + throw new Error('Invalid semver string'); } diff --git a/packages/upgrade/vitest.config.js b/packages/upgrade/vitest.config.js new file mode 100644 index 0000000000..2dcea8c54d --- /dev/null +++ b/packages/upgrade/vitest.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + }, +}); diff --git a/playground/app-router/src/app/action/page.tsx b/playground/app-router/src/app/action/page.tsx index a143b30183..5962909a82 100644 --- a/playground/app-router/src/app/action/page.tsx +++ b/playground/app-router/src/app/action/page.tsx @@ -3,7 +3,7 @@ import { auth, currentUser } from '@clerk/nextjs/server'; export default function AddToCart() { async function addItem(data: any) { 'use server'; - console.log(auth().userId); + console.log((await auth()).userId); console.log((await currentUser())?.firstName); console.log('add item server action', data); } diff --git a/playground/app-router/src/app/protected/page.tsx b/playground/app-router/src/app/protected/page.tsx index ec27013595..dbf53f5c6c 100644 --- a/playground/app-router/src/app/protected/page.tsx +++ b/playground/app-router/src/app/protected/page.tsx @@ -1,11 +1,13 @@ -import { auth, ClerkLoaded, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'; +import { useAuth, ClerkLoaded, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'; import React from 'react'; import { ClientSideWrapper } from '@/app/protected/ClientSideWrapper'; import { headers } from 'next/headers'; -export default function Page() { - const { userId } = auth(); - console.log('Auth run in /protected', userId, headers().get('x-clerk-debug'), headers().keys()); +export default async function Page() { + const { userId } = useAuth(); + const resolvedHeaders = await headers().get('x-clerk-debug'); + + console.log('Auth run in /protected', userId, resolvedHeaders.get('x-clerk-debug'), resolvedHeaders.keys()); // console.log(auth()); return (
diff --git a/playground/app-router/src/middleware.ts b/playground/app-router/src/middleware.ts index 642c1859ec..8c4dd060d1 100644 --- a/playground/app-router/src/middleware.ts +++ b/playground/app-router/src/middleware.ts @@ -1,35 +1,7 @@ import { clerkMiddleware } from '@clerk/nextjs/server'; -import { NextMiddleware, NextResponse } from 'next/server'; -export default clerkMiddleware((auth)=> { - -}) - -// export default authMiddleware({ -// publicRoutes: ['/'], -// beforeAuth: req => { -// // console.log('middleware:beforeAuth', req.url); -// if (req.nextUrl.searchParams.get('redirect')) { -// return NextResponse.redirect('https://google.com'); -// } -// const res = NextResponse.next(); -// res.headers.set('x-before-auth', 'true'); -// return res; -// }, -// afterAuth: (auth, req) => { -// // console.log('middleware:afterAuth', auth.userId, req.url, auth.isPublicRoute); -// if (!auth.userId && !auth.isPublicRoute) { -// const url = new URL('/sign-in', req.url); -// url.searchParams.append('redirect_url', req.url); -// return NextResponse.redirect(url); -// } -// const res = NextResponse.next(); -// res.headers.set('x-after-auth', 'true'); -// return res; -// }, -// debug: true -// }); +export default clerkMiddleware() export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], diff --git a/playground/nextjs/app/app-dir/page.tsx b/playground/nextjs/app/app-dir/page.tsx index 7fc36ec68c..2f21a6da38 100644 --- a/playground/nextjs/app/app-dir/page.tsx +++ b/playground/nextjs/app/app-dir/page.tsx @@ -3,7 +3,7 @@ import { auth, clerkClient, currentUser } from '@clerk/nextjs/server'; import Link from 'next/link'; export default async function Page() { - const { userId } = auth(); + const { userId } = await auth(); const currentUser_ = await currentUser(); const user = userId ? await clerkClient.users.getUser(userId) : null; diff --git a/playground/nextjs/middleware.ts b/playground/nextjs/middleware.ts index e41136e55e..cf79854ca1 100644 --- a/playground/nextjs/middleware.ts +++ b/playground/nextjs/middleware.ts @@ -1,11 +1,6 @@ -import { authMiddleware } from '@clerk/nextjs/server'; +import { clerkMiddleware } from '@clerk/nextjs/server'; -// Set the paths that don't require the user to be signed in -const publicPaths = ['/', /^(\/(sign-in|sign-up|app-dir|custom)\/*).*$/]; - -export default authMiddleware({ - publicRoutes: publicPaths, -}); +export default clerkMiddleware(); export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], diff --git a/turbo.json b/turbo.json index d86965d760..dd7a6d82cc 100644 --- a/turbo.json +++ b/turbo.json @@ -24,7 +24,8 @@ "VERCEL", "VITE_CLERK_*", "EXPO_PUBLIC_CLERK_*", - "REACT_APP_CLERK_*" + "REACT_APP_CLERK_*", + "NEXT_PHASE" ], "globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN", "ACTIONS_RUNNER_DEBUG", "ACTIONS_STEP_DEBUG"], "tasks": {