Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 470
feat(astro): [WIP] Add support for keyless mode#7800
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
5995193da2bc6f69a31194d0ee39490699de1bf0acf7d0ce9d3a360afd376a2458c600238f5d37abc1bd11ed1eeFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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): Promise<void> => { | ||
| 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 }); | ||
| }); | ||
| }; |
| 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 } from '../../testUtils'; | ||
| import { mockClaimedInstanceEnvironmentCall } from '../../testUtils/keylessHelpers'; | ||
| const commonSetup = appConfigs.astro.node.clone(); | ||
| test.describe('Keyless mode @astro', () => { | ||
| 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 () => { | ||
| 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 (Astro 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 | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -24,10 +24,12 @@ | ||||||||||||||||||||
| import { authAsyncStorage } from '#async-local-storage'; | ||||||||||||||||||||
| import { canUseKeyless } from '../utils/feature-flags'; | ||||||||||||||||||||
| import { buildClerkHotloadScript } from './build-clerk-hotload-script'; | ||||||||||||||||||||
| import { clerkClient } from './clerk-client'; | ||||||||||||||||||||
| import { createCurrentUser } from './current-user'; | ||||||||||||||||||||
| import { getClientSafeEnv, getSafeEnv } from './get-safe-env'; | ||||||||||||||||||||
| import { resolveKeysWithKeylessFallback } from './keyless/utils'; | ||||||||||||||||||||
| import { serverRedirectWithAuth } from './server-redirect-with-auth'; | ||||||||||||||||||||
| import type { | ||||||||||||||||||||
| AstroMiddleware, | ||||||||||||||||||||
| @@ -79,9 +81,38 @@ | ||||||||||||||||||||
| const clerkRequest = createClerkRequest(context.request); | ||||||||||||||||||||
| // Resolve keyless URLs per-request in development | ||||||||||||||||||||
| let keylessClaimUrl: string | undefined; | ||||||||||||||||||||
| let keylessApiKeysUrl: string | undefined; | ||||||||||||||||||||
| let keylessOptions = options; | ||||||||||||||||||||
| if (canUseKeyless) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const env = getSafeEnv(context); | ||||||||||||||||||||
| const configuredPublishableKey = options?.publishableKey || env.pk; | ||||||||||||||||||||
| const configuredSecretKey = options?.secretKey || env.sk; | ||||||||||||||||||||
| const keylessResult = await resolveKeysWithKeylessFallback(configuredPublishableKey, configuredSecretKey); | ||||||||||||||||||||
| keylessClaimUrl = keylessResult.claimUrl; | ||||||||||||||||||||
| keylessApiKeysUrl = keylessResult.apiKeysUrl; | ||||||||||||||||||||
| // Override keys with keyless values if returned | ||||||||||||||||||||
| if (keylessResult.publishableKey || keylessResult.secretKey) { | ||||||||||||||||||||
| keylessOptions = { | ||||||||||||||||||||
| ...options, | ||||||||||||||||||||
| ...(keylessResult.publishableKey && { publishableKey: keylessResult.publishableKey }), | ||||||||||||||||||||
| ...(keylessResult.secretKey && { secretKey: keylessResult.secretKey }), | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||
| // Silently fail - continue without keyless | ||||||||||||||||||||
| } | ||||||||||||||||||||
Comment on lines
+108
to
+110
Contributor 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. Silent error swallowing hinders debugging. The empty catch block will make it extremely difficult to diagnose keyless mode failures. At minimum, log the error in development: } catch (error) {
- // Silently fail - continue without keyless+ // Continue without keyless, but log for debugging+ if (import.meta.env.DEV) {+ console.warn('[Clerk] Keyless resolution failed, falling back to configured keys:', error);+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||
| } | ||||||||||||||||||||
| const requestState = await clerkClient(context).authenticateRequest( | ||||||||||||||||||||
| clerkRequest, | ||||||||||||||||||||
| createAuthenticateRequestOptions(clerkRequest, options, context), | ||||||||||||||||||||
| createAuthenticateRequestOptions(clerkRequest, keylessOptions, context), | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| const locationHeader = requestState.headers.get(constants.Headers.Location); | ||||||||||||||||||||
| @@ -104,6 +135,16 @@ | ||||||||||||||||||||
| decorateAstroLocal(clerkRequest, authObjectFn, context, requestState); | ||||||||||||||||||||
| // Store keyless data for injection into client | ||||||||||||||||||||
| if (keylessClaimUrl || keylessApiKeysUrl) { | ||||||||||||||||||||
| context.locals.keylessClaimUrl = keylessClaimUrl; | ||||||||||||||||||||
| context.locals.keylessApiKeysUrl = keylessApiKeysUrl; | ||||||||||||||||||||
| // Also store the resolved publishable key so client can use it | ||||||||||||||||||||
| if (keylessOptions?.publishableKey) { | ||||||||||||||||||||
| context.locals.keylessPublishableKey = keylessOptions.publishableKey; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| /** | ||||||||||||||||||||
| * ALS is crucial for guaranteeing SSR in UI frameworks like React. | ||||||||||||||||||||
| * This currently powers the `useAuth()` React hook and any other hook or Component that depends on it. | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import type { KeylessStorage } from '@clerk/shared/keyless'; | ||
| export type { KeylessStorage }; | ||
| export interface FileStorageOptions { | ||
| cwd?: () => string; | ||
| } | ||
| export async function createFileStorage(options: FileStorageOptions = {}): Promise<KeylessStorage> { | ||
| const { cwd = () => process.cwd() } = options; | ||
| const [{ default: fs }, { default: path }] = await Promise.all([import('node:fs'), import('node:path')]); | ||
| const { createNodeFileStorage } = await import('@clerk/shared/keyless'); | ||
| return createNodeFileStorage(fs, path, { | ||
| cwd, | ||
| frameworkPackageName: '@clerk/astro', | ||
| }); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: clerk/javascript
Length of output: 291
🏁 Script executed:
Repository: clerk/javascript
Length of output: 1426
Remove or downgrade vite to version 6.
Astro 5.15.9 specifies
viteas a dependency with^6.4.1. Settingvite@^7.1.0as a devDependency creates a version conflict that will cause installation to fail. Either remove the vite devDependency to use Astro's pinned version, or downgrade to^6.4.1or higher within the 6.x range.🤖 Prompt for AI Agents