-
Notifications
You must be signed in to change notification settings - Fork 436
feat(react-router): Add support for keyless mode #7794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
a659eaf
e08192c
140ba30
6bc3243
15d5fc0
6638d89
fe7577e
4a6b959
f79def9
9354709
ae6c168
9e962c2
818352a
57a8049
834c6eb
069aaef
32bec51
9fee85e
a70b8a2
1c39d21
6b04f9f
567b5a5
7183175
0049b7f
2e71569
ab36fa8
cda1649
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import type { Page } from '@playwright/test'; | ||
|
|
||
| /** | ||
| * Mocks the environment API call to return a claimed instance. | ||
| * Used in keyless mode tests to simulate an instance that has been claimed. | ||
| */ | ||
| export const mockClaimedInstanceEnvironmentCall = async (page: Page) => { | ||
| await page.route('*/**/v1/environment*', async route => { | ||
| const response = await route.fetch(); | ||
| const json = await response.json(); | ||
| const newJson = { | ||
| ...json, | ||
| auth_config: { | ||
| ...json.auth_config, | ||
| claimed_at: Date.now(), | ||
| }, | ||
| }; | ||
| await route.fulfill({ response, json: newJson }); | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
|
|
||
| import type { Application } from '../../models/application'; | ||
| import { appConfigs } from '../../presets'; | ||
| import { createTestUtils, mockClaimedInstanceEnvironmentCall } from '../../testUtils'; | ||
|
|
||
| const commonSetup = appConfigs.reactRouter.reactRouterNode.clone(); | ||
|
|
||
| test.describe('Keyless mode @react-router', () => { | ||
| test.describe.configure({ mode: 'serial' }); | ||
| test.setTimeout(90_000); | ||
|
|
||
| test.use({ | ||
| extraHTTPHeaders: { | ||
| 'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET || '', | ||
| }, | ||
| }); | ||
|
|
||
| let app: Application; | ||
| let dashboardUrl = 'https://dashboard.clerk.com/'; | ||
|
|
||
| test.beforeAll(async () => { | ||
| app = await commonSetup.commit(); | ||
| await app.setup(); | ||
| await app.withEnv(appConfigs.envs.withKeyless); | ||
| if (appConfigs.envs.withKeyless.privateVariables.get('CLERK_API_URL')?.includes('clerkstage')) { | ||
| dashboardUrl = 'https://dashboard.clerkstage.dev/'; | ||
| } | ||
| await app.dev(); | ||
| }); | ||
|
|
||
| test.afterAll(async () => { | ||
| // Keep files for debugging | ||
| await app?.teardown(); | ||
| }); | ||
|
|
||
| test('Toggle collapse popover and claim.', async ({ page, context }) => { | ||
| const u = createTestUtils({ app, page, context }); | ||
| await u.page.goToAppHome(); | ||
| await u.page.waitForClerkJsLoaded(); | ||
| await u.po.expect.toBeSignedOut(); | ||
|
|
||
| await u.po.keylessPopover.waitForMounted(); | ||
|
|
||
| expect(await u.po.keylessPopover.isExpanded()).toBe(false); | ||
| await u.po.keylessPopover.toggle(); | ||
| expect(await u.po.keylessPopover.isExpanded()).toBe(true); | ||
|
|
||
| const claim = await u.po.keylessPopover.promptsToClaim(); | ||
|
|
||
| const [newPage] = await Promise.all([context.waitForEvent('page'), claim.click()]); | ||
|
|
||
| await newPage.waitForLoadState(); | ||
|
|
||
| await newPage.waitForURL(url => { | ||
| const urlToReturnTo = `${dashboardUrl}apps/claim?token=`; | ||
|
|
||
| const signUpForceRedirectUrl = url.searchParams.get('sign_up_force_redirect_url'); | ||
|
|
||
| const signUpForceRedirectUrlCheck = | ||
| signUpForceRedirectUrl?.startsWith(urlToReturnTo) || | ||
| (signUpForceRedirectUrl?.startsWith(`${dashboardUrl}prepare-account`) && | ||
| signUpForceRedirectUrl?.includes(encodeURIComponent('apps/claim?token='))); | ||
|
|
||
| return ( | ||
| url.pathname === '/apps/claim/sign-in' && | ||
| url.searchParams.get('sign_in_force_redirect_url')?.startsWith(urlToReturnTo) && | ||
| signUpForceRedirectUrlCheck | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({ | ||
| page, | ||
| context, | ||
| }) => { | ||
| await mockClaimedInstanceEnvironmentCall(page); | ||
| const u = createTestUtils({ app, page, context }); | ||
| await u.page.goToAppHome(); | ||
| await u.page.waitForClerkJsLoaded(); | ||
|
|
||
| await u.po.keylessPopover.waitForMounted(); | ||
| expect(await u.po.keylessPopover.isExpanded()).toBe(true); | ||
| await expect(u.po.keylessPopover.promptToUseClaimedKeys()).toBeVisible(); | ||
|
|
||
| const [newPage] = await Promise.all([ | ||
| context.waitForEvent('page'), | ||
| u.po.keylessPopover.promptToUseClaimedKeys().click(), | ||
| ]); | ||
|
|
||
| await newPage.waitForLoadState(); | ||
| await newPage.waitForURL(url => { | ||
| return url.href.startsWith(`${dashboardUrl}sign-in?redirect_url=${encodeURIComponent(dashboardUrl)}apps%2Fapp_`); | ||
| }); | ||
| }); | ||
|
|
||
| test('Keyless popover is removed after adding keys to .env and restarting.', async ({ page, context }) => { | ||
| const u = createTestUtils({ app, page, context }); | ||
| await u.page.goToAppHome(); | ||
|
|
||
| await u.po.keylessPopover.waitForMounted(); | ||
| expect(await u.po.keylessPopover.isExpanded()).toBe(false); | ||
|
|
||
| // Copy keys from keyless.json to .env | ||
| await app.keylessToEnv(); | ||
|
|
||
| // Restart the dev server to pick up new env vars (Vite doesn't hot-reload .env) | ||
| await app.restart(); | ||
|
|
||
| await u.page.goToAppHome(); | ||
|
|
||
| // Keyless popover should no longer be present since we now have explicit keys | ||
| await u.po.keylessPopover.waitForUnmounted(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { createNodeFileStorage, type KeylessStorage } from '@clerk/shared/keyless'; | ||
|
|
||
| export type { KeylessStorage }; | ||
|
|
||
| export interface FileStorageOptions { | ||
| cwd?: () => string; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a file-based storage adapter for keyless mode. | ||
| * Uses dynamic imports to avoid bundler issues with edge runtimes. | ||
| */ | ||
| export async function createFileStorage(options: FileStorageOptions = {}): Promise<KeylessStorage> { | ||
| const { cwd = () => process.cwd() } = options; | ||
|
|
||
| try { | ||
| const [fs, path] = await Promise.all([import('node:fs'), import('node:path')]); | ||
|
|
||
| return createNodeFileStorage(fs, path, { | ||
| cwd, | ||
| frameworkPackageName: '@clerk/react-router', | ||
| }); | ||
| } catch { | ||
| throw new Error( | ||
| 'Keyless mode requires a Node.js runtime with file system access. ' + | ||
| 'Set VITE_CLERK_KEYLESS_DISABLED=1 to disable keyless mode.', | ||
| ); | ||
| } | ||
| } | ||
|
Comment on lines
+13
to
+29
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be extracted as well, but will plan to do so while implementing keyless for other SDKs. Having these node imports in the shared |
||
Uh oh!
There was an error while loading. Please reload this page.