From 641781155310e891262ccb30ab26d1d303f5c08c Mon Sep 17 00:00:00 2001 From: Devin Gould Date: Wed, 26 Aug 2026 13:02:52 -0400 Subject: [PATCH] chore(repo): remove leftover keyless creation code Co-Authored-By: Claude Fable 5 --- .changeset/keyless-leftover-cleanup.md | 7 + integration/scripts/waitForServer.ts | 2 +- integration/testUtils/keylessHelpers.ts | 100 +----------- .../AccountlessApplicationsApi.test.ts | 42 ----- .../endpoints/AccountlessApplicationsAPI.ts | 12 -- .../src/app-router/server/ClerkProvider.tsx | 13 +- .../app-router/server/keyless-provider.tsx | 85 ++++------ .../__tests__/clerkMiddlewareKeyless.test.ts | 3 +- .../__tests__/getAuthDataFromRequest.test.ts | 1 - packages/nextjs/src/server/clerkMiddleware.ts | 1 - .../nextjs/src/server/keyless-log-cache.ts | 9 +- packages/nextjs/src/server/keyless-node.ts | 10 -- packages/nextjs/src/server/keyless.ts | 54 ------- packages/nextjs/src/server/utils.ts | 10 +- packages/nextjs/src/utils/only-try.ts | 12 -- .../src/keyless/__tests__/service.spec.ts | 101 +++++------- packages/shared/src/keyless/devCache.ts | 11 -- packages/shared/src/keyless/index.ts | 20 +-- .../keyless/resolveKeysWithKeylessFallback.ts | 74 --------- packages/shared/src/keyless/service.ts | 147 +----------------- packages/shared/src/keyless/types.ts | 5 - 21 files changed, 108 insertions(+), 611 deletions(-) create mode 100644 .changeset/keyless-leftover-cleanup.md delete mode 100644 packages/nextjs/src/server/keyless.ts delete mode 100644 packages/nextjs/src/utils/only-try.ts delete mode 100644 packages/shared/src/keyless/resolveKeysWithKeylessFallback.ts diff --git a/.changeset/keyless-leftover-cleanup.md b/.changeset/keyless-leftover-cleanup.md new file mode 100644 index 00000000000..5f8afe7695a --- /dev/null +++ b/.changeset/keyless-leftover-cleanup.md @@ -0,0 +1,7 @@ +--- +'@clerk/shared': minor +'@clerk/nextjs': patch +'@clerk/backend': minor +--- + +Remove leftover keyless-mode creation code now that no SDK mints keyless applications. `@clerk/shared/keyless` drops `resolveKeysWithKeylessFallback`, `getOrCreateKeys`, and related exports (internal APIs consumed only by Clerk SDKs); `@clerk/backend` removes the experimental `createAccountlessApplication` method; `@clerk/nextjs` deletes the unused keyless cookie reader and dead keyless middleware parameters, and logs a pointer to existing `.clerk/.tmp/keyless.json` keys when env keys are missing. diff --git a/integration/scripts/waitForServer.ts b/integration/scripts/waitForServer.ts index 817f9454541..2fd6ca6c83a 100644 --- a/integration/scripts/waitForServer.ts +++ b/integration/scripts/waitForServer.ts @@ -6,7 +6,7 @@ type WaitForServerArgsType = { acceptAnyResponse?: boolean; }; -// Poll a url until it returns a 200 status code +// Poll a url until it returns 2xx (or any HTTP response when acceptAnyResponse is set) export const waitForServer = async (url: string, opts: WaitForServerArgsType) => { const { log, delayInMs = 1000, maxAttempts = 20, shouldExit = () => false, acceptAnyResponse = false } = opts; let attempts = 0; diff --git a/integration/testUtils/keylessHelpers.ts b/integration/testUtils/keylessHelpers.ts index bd737498d30..4c941e64f62 100644 --- a/integration/testUtils/keylessHelpers.ts +++ b/integration/testUtils/keylessHelpers.ts @@ -1,8 +1,4 @@ -import type { BrowserContext, Page } from '@playwright/test'; -import { expect } from '@playwright/test'; - -import type { Application } from '../models/application'; -import { createTestUtils } from './index'; +import type { Page } from '@playwright/test'; /** * Mocks the environment API call to return a claimed instance. @@ -22,97 +18,3 @@ export const mockClaimedInstanceEnvironmentCall = async (page: Page): Promise { - const u = createTestUtils({ app, page, context }); - await u.page.goToAppHome(); - await u.page.waitForClerkJsLoaded(); - await u.po.expect.toBeSignedOut(); - - await u.po.keylessPopover.waitForMounted(); - - const claim = u.po.keylessPopover.promptsToClaim(); - - const href = await claim.getAttribute('href'); - expect(href).toBeTruthy(); - - const claimUrl = new URL(href!); - expect(claimUrl.origin + '/').toBe(dashboardUrl); - expect(claimUrl.pathname).toBe('/apps/claim'); - expect(claimUrl.searchParams.get('framework')).toBe(framework); - expect(claimUrl.searchParams.has('token')).toBe(true); - expect(claimUrl.searchParams.has('return_url')).toBe(true); -} - -/** - * Tests that a claimed application with missing explicit keys shows the popover expanded - * with a prompt to get keys from the dashboard. - */ -export async function testClaimedAppWithMissingKeys({ - page, - context, - app, - dashboardUrl, -}: { - page: Page; - context: BrowserContext; - app: Application; - dashboardUrl: string; -}): Promise { - 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 href = await u.po.keylessPopover.promptToUseClaimedKeys().getAttribute('href'); - expect(href).toBeTruthy(); - expect(href).toContain(dashboardUrl); -} - -/** - * Tests that the keyless popover is removed after adding keys to .env and restarting the dev server. - */ -export async function testKeylessRemovedAfterEnvAndRestart({ - page, - context, - app, -}: { - page: Page; - context: BrowserContext; - app: Application; -}): Promise { - const u = createTestUtils({ app, page, context }); - await u.page.goToAppHome(); - - await u.po.keylessPopover.waitForMounted(); - - // 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(); -} diff --git a/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts b/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts index 98f0e1c9c19..5ae3bad02ce 100644 --- a/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts +++ b/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts @@ -13,48 +13,6 @@ describe('AccountlessApplications', () => { api_keys_url: 'https://dashboard.clerk.com/api-keys', }; - it('creates an accountless application with a source query parameter', async () => { - const apiClient = createBackendApiClient({ - apiUrl: 'https://api.clerk.test', - }); - - server.use( - http.post('https://api.clerk.test/v1/accountless_applications', ({ request }) => { - const url = new URL(request.url); - expect(url.searchParams.get('source')).toBe('nextjs'); - expect(request.headers.get('Clerk-API-Version')).toBeTruthy(); - expect(request.headers.get('User-Agent')).toBe('@clerk/backend@0.0.0-test'); - - return HttpResponse.json(mockAccountlessApplication); - }), - ); - - const response = await apiClient.__experimental_accountlessApplications.createAccountlessApplication({ - source: 'nextjs', - }); - - expect(response.publishableKey).toBe('pk_test_keyless'); - }); - - it('creates an accountless application without a source query parameter when source is omitted', async () => { - const apiClient = createBackendApiClient({ - apiUrl: 'https://api.clerk.test', - }); - - server.use( - http.post('https://api.clerk.test/v1/accountless_applications', ({ request }) => { - const url = new URL(request.url); - expect(url.searchParams.has('source')).toBe(false); - - return HttpResponse.json(mockAccountlessApplication); - }), - ); - - const response = await apiClient.__experimental_accountlessApplications.createAccountlessApplication(); - - expect(response.publishableKey).toBe('pk_test_keyless'); - }); - it('completes accountless application onboarding with a source query parameter', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', diff --git a/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts b/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts index 37b69809753..cdb34fddca3 100644 --- a/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts +++ b/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts @@ -10,18 +10,6 @@ type AccountlessApplicationParams = { }; export class AccountlessApplicationAPI extends AbstractAPI { - public async createAccountlessApplication(params?: AccountlessApplicationParams): Promise { - const headerParams = params?.requestHeaders ? Object.fromEntries(params.requestHeaders.entries()) : undefined; - return this.request({ - method: 'POST', - path: basePath, - headerParams, - queryParams: { - source: params?.source, - }, - }); - } - public async completeAccountlessApplicationOnboarding( params?: AccountlessApplicationParams, ): Promise { diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index b43d83306c8..6ab7fec7c5f 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -5,6 +5,7 @@ import React, { Suspense } from 'react'; import { getDynamicAuthData } from '../../server/buildClerkProps'; import { errorThrower } from '../../server/errorThrower'; import type { NextClerkProviderProps } from '../../types'; +import { canUseKeyless } from '../../utils/feature-flags'; import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; import { ClientClerkProvider } from '../client/ClerkProvider'; import { DynamicClerkScripts } from './DynamicClerkScripts'; @@ -32,7 +33,7 @@ export async function ClerkProvider( initialState: statePromiseOrValue as InitialState | undefined, }); - const { shouldRunAsKeyless, runningWithClaimedKeys } = await getKeylessStatus(propsWithEnvs); + const { runningWithClaimedKeys } = await getKeylessStatus(propsWithEnvs); // When dynamic mode is enabled, render scripts in a Suspense boundary to isolate // the nonce fetching (which calls headers()) from the rest of the page. @@ -52,14 +53,14 @@ export async function ClerkProvider( ) : undefined; - if (shouldRunAsKeyless) { - if (!propsWithEnvs.publishableKey) { - errorThrower.throwMissingPublishableKeyError(); - } + if (canUseKeyless && !propsWithEnvs.publishableKey) { + errorThrower.throwMissingPublishableKeyError(); + } + + if (runningWithClaimedKeys) { return ( {children} diff --git a/packages/nextjs/src/app-router/server/keyless-provider.tsx b/packages/nextjs/src/app-router/server/keyless-provider.tsx index b2956a2b1e6..e84135538f1 100644 --- a/packages/nextjs/src/app-router/server/keyless-provider.tsx +++ b/packages/nextjs/src/app-router/server/keyless-provider.tsx @@ -11,40 +11,42 @@ import { deleteKeylessAction } from '../keyless-actions'; export async function getKeylessStatus( params: Without, ) { - let [shouldRunAsKeyless, runningWithClaimedKeys, locallyStoredPublishableKey] = [false, false, '']; - if (canUseKeyless) { - locallyStoredPublishableKey = await import('../../server/keyless-node.js') - .then(mod => mod.keyless().readKeys()?.publishableKey || '') - .catch(() => ''); + if (!canUseKeyless) { + return { runningWithClaimedKeys: false }; + } - runningWithClaimedKeys = Boolean(params.publishableKey) && params.publishableKey === locallyStoredPublishableKey; - shouldRunAsKeyless = !params.publishableKey || runningWithClaimedKeys; + const storedKeys = await import('../../server/keyless-node.js') + .then(mod => mod.keyless().readKeys() ?? null) + .catch(() => null); + if (!storedKeys) { + return { runningWithClaimedKeys: false }; } - return { - shouldRunAsKeyless, - runningWithClaimedKeys, - }; + if (!params.publishableKey) { + const { clerkDevelopmentCache } = await import('../../server/keyless-log-cache.js'); + clerkDevelopmentCache?.log({ + cacheKey: `${storedKeys.publishableKey}_stored`, + msg: `[Clerk]: Found existing keyless-mode keys in .clerk/.tmp/keyless.json. Copy the publishableKey and secretKey into .env.local (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY / CLERK_SECRET_KEY) to keep using that application, or claim it at ${storedKeys.claimUrl}`, + }); + return { runningWithClaimedKeys: false }; + } + + return { runningWithClaimedKeys: params.publishableKey === storedKeys.publishableKey }; } type KeylessProviderProps = PropsWithChildren<{ rest: Without; - runningWithClaimedKeys: boolean; __internal_scriptsSlot?: React.ReactNode; }>; export const KeylessProvider = async (props: KeylessProviderProps) => { - const { rest, runningWithClaimedKeys, __internal_scriptsSlot, children } = props; + const { rest, __internal_scriptsSlot, children } = props; - // Read-only: the SDK no longer mints keyless applications, it only reads claimed keys from disk. - const newOrReadKeys = await import('../../server/keyless-node.js') + const storedKeys = await import('../../server/keyless-node.js') .then(mod => mod.keyless().readKeys() ?? null) .catch(() => null); - const { clerkDevelopmentCache, createConfirmationMessage } = await import('../../server/keyless-log-cache.js'); - - if (!newOrReadKeys) { - // When case keyless should run, but keys are not available, then fallback to throwing for missing keys + if (!storedKeys) { return ( { ); } - const clientProvider = ( + try { + const keylessService = await import('../../server/keyless-node.js').then(mod => mod.keyless()); + const { completeClaimedOnboarding } = await import('@clerk/shared/keyless'); + await completeClaimedOnboarding(storedKeys.publishableKey, keylessService); + } catch { + // noop + } + + return ( {children} ); - - if (runningWithClaimedKeys) { - try { - const keylessService = await import('../../server/keyless-node.js').then(mod => mod.keyless()); - - /** - * Notifying the dashboard should run once. We are controlling this behaviour by caching the result of the request. - * If the request fails, it will be considered stale after 10 minutes, otherwise it is cached for 24 hours. - */ - await clerkDevelopmentCache?.run(() => keylessService.completeOnboarding(), { - cacheKey: `${newOrReadKeys.publishableKey}_complete`, - onSuccessStale: 24 * 60 * 60 * 1000, // 24 hours - }); - } catch { - // noop - } - - /** - * Notify developers. - */ - clerkDevelopmentCache?.log({ - cacheKey: `${newOrReadKeys.publishableKey}_claimed`, - msg: createConfirmationMessage(), - }); - } - - return clientProvider; }; diff --git a/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts index 8f4e8842cf4..95fec07a690 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts @@ -3,8 +3,7 @@ import type { NextFetchEvent } from 'next/server'; import { NextRequest } from 'next/server'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -// The mock SHOULD exist before the imports: unlike clerkMiddleware.test.ts, keys are empty so the -// missing-key error path is reachable. +// The mock SHOULD exist before the imports. Keys are intentionally empty so the missing-key error path is reachable. vi.mock(import('../constants.js'), async importOriginal => { const actual = await importOriginal(); return { diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index 1a49c6b6c13..a3836889c04 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -37,7 +37,6 @@ const mockRequest = (params: MockRequestParams) => { if (machineAuthObject) { const encryptedData = encryptClerkRequestData( {}, // requestData - {}, // keylessModeKeys // @ts-expect-error - mock machine auth object machineAuthObject, ); diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 98d7edb26f4..9cbe161a9c5 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -389,7 +389,6 @@ async function runHandlerWithRequestState({ handlerResult, requestState, resolvedParams, - {}, authObject.tokenType === 'session_token' ? null : makeAuthObjectSerializable(authObject), ); diff --git a/packages/nextjs/src/server/keyless-log-cache.ts b/packages/nextjs/src/server/keyless-log-cache.ts index 7e0fdb90e34..905a0b4cf15 100644 --- a/packages/nextjs/src/server/keyless-log-cache.ts +++ b/packages/nextjs/src/server/keyless-log-cache.ts @@ -1,10 +1,5 @@ /** - * Re-export keyless development cache utilities from shared. + * Re-export the keyless development cache from shared. * This maintains backward compatibility with existing imports. */ -export { - clerkDevelopmentCache, - createClerkDevCache, - createConfirmationMessage, - createKeylessModeMessage, -} from '@clerk/shared/keyless'; +export { clerkDevelopmentCache } from '@clerk/shared/keyless'; diff --git a/packages/nextjs/src/server/keyless-node.ts b/packages/nextjs/src/server/keyless-node.ts index 5df56892ab9..320968125bb 100644 --- a/packages/nextjs/src/server/keyless-node.ts +++ b/packages/nextjs/src/server/keyless-node.ts @@ -24,16 +24,6 @@ export function keyless() { keylessServiceInstance = createKeylessService({ storage: createFileStorage(), api: { - async createAccountlessApplication(requestHeaders?: Headers, source?: string) { - try { - return await client.__experimental_accountlessApplications.createAccountlessApplication({ - requestHeaders, - source, - }); - } catch { - return null; - } - }, async completeOnboarding(requestHeaders?: Headers, source?: string) { try { return await client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ diff --git a/packages/nextjs/src/server/keyless.ts b/packages/nextjs/src/server/keyless.ts deleted file mode 100644 index ca40e33ca7b..00000000000 --- a/packages/nextjs/src/server/keyless.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { AccountlessApplication } from '@clerk/backend'; - -import { canUseKeyless } from '../utils/feature-flags'; - -const keylessCookiePrefix = `__clerk_keys_`; - -async function hashString(str: string) { - const encoder = new TextEncoder(); - const data = encoder.encode(str); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - return hashHex.slice(0, 16); // Take only the first 16 characters -} - -async function getKeylessCookieName(): Promise { - // eslint-disable-next-line turbo/no-undeclared-env-vars - const PATH = process.env.PWD; - - // Handle gracefully missing PWD - if (!PATH) { - return `${keylessCookiePrefix}${0}`; - } - - const lastThreeDirs = PATH.split('/').filter(Boolean).slice(-3).reverse().join('/'); - - // Hash the resulting string - const hash = await hashString(lastThreeDirs); - - return `${keylessCookiePrefix}${hash}`; -} - -async function getKeylessCookieValue( - getter: (cookieName: string) => string | undefined, -): Promise { - if (!canUseKeyless) { - return undefined; - } - - const keylessCookieName = await getKeylessCookieName(); - let keyless; - - try { - if (keylessCookieName) { - keyless = JSON.parse(getter(keylessCookieName) || '{}'); - } - } catch { - keyless = undefined; - } - - return keyless; -} - -export { getKeylessCookieValue, getKeylessCookieName }; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index e6504478823..b25c5a859d0 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -53,7 +53,6 @@ export function decorateRequest( res: Response, requestState: RequestState, requestData: AuthenticateRequestOptions, - keylessMode: Pick, machineAuthObject: AuthObject | null, ): Response { const { reason, message, status, token } = requestState; @@ -89,13 +88,13 @@ export function decorateRequest( } if (rewriteURL) { - const clerkRequestData = encryptClerkRequestData(requestData, keylessMode, machineAuthObject); + const clerkRequestData = encryptClerkRequestData(requestData, machineAuthObject); setRequestHeadersOnNextResponse(res, req, { [constants.Headers.AuthStatus]: status, [constants.Headers.AuthToken]: token || '', [constants.Headers.AuthSignature]: token - ? createTokenSignature(token, requestData?.secretKey || SECRET_KEY || keylessMode.secretKey || '') + ? createTokenSignature(token, requestData?.secretKey || SECRET_KEY || '') : '', [constants.Headers.AuthMessage]: message || '', [constants.Headers.AuthReason]: reason || '', @@ -208,7 +207,6 @@ const KEYLESS_ENCRYPTION_KEY = 'clerk_keyless_dummy_key'; **/ export function encryptClerkRequestData( requestData: Partial, - keylessModeKeys: Pick, machineAuthObject: AuthObject | null, ) { const isEmpty = (obj: Record | undefined) => { @@ -218,7 +216,7 @@ export function encryptClerkRequestData( return !Object.values(obj).some(v => v !== undefined); }; - if (isEmpty(requestData) && isEmpty(keylessModeKeys) && !machineAuthObject) { + if (isEmpty(requestData) && !machineAuthObject) { return; } @@ -231,7 +229,7 @@ export function encryptClerkRequestData( : ENCRYPTION_KEY || SECRET_KEY || KEYLESS_ENCRYPTION_KEY; return AES.encrypt( - JSON.stringify({ ...keylessModeKeys, ...requestData, machineAuthObject: machineAuthObject ?? undefined }), + JSON.stringify({ ...requestData, machineAuthObject: machineAuthObject ?? undefined }), maybeKeylessEncryptionKey, ).toString(); } diff --git a/packages/nextjs/src/utils/only-try.ts b/packages/nextjs/src/utils/only-try.ts deleted file mode 100644 index 5f8c62e87f8..00000000000 --- a/packages/nextjs/src/utils/only-try.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Discards errors thrown by attempted code - */ -const onlyTry = (cb: () => unknown) => { - try { - cb(); - } catch { - // ignore - } -}; - -export { onlyTry }; diff --git a/packages/shared/src/keyless/__tests__/service.spec.ts b/packages/shared/src/keyless/__tests__/service.spec.ts index dca8716d5b2..b2ef21d2ba6 100644 --- a/packages/shared/src/keyless/__tests__/service.spec.ts +++ b/packages/shared/src/keyless/__tests__/service.spec.ts @@ -25,53 +25,11 @@ const createStorage = (): KeylessStorage => { }; const createApi = (overrides: Partial = {}): KeylessAPI => ({ - createAccountlessApplication: vi.fn(() => Promise.resolve(accountlessApplication)), completeOnboarding: vi.fn(() => Promise.resolve(accountlessApplication)), ...overrides, }); describe('createKeylessService', () => { - it('passes the framework as the source when creating an accountless application', async () => { - const createAccountlessApplication = vi.fn(() => - Promise.resolve(accountlessApplication), - ); - - const service = createKeylessService({ - storage: createStorage(), - api: createApi({ createAccountlessApplication }), - framework: 'nextjs', - }); - - await service.getOrCreateKeys(); - - const [headers, source] = createAccountlessApplication.mock.calls[0]; - expect(headers).toBeInstanceOf(Headers); - expect(source).toBe('nextjs'); - }); - - it('getOrCreateKeys resolves to null when the API cannot create applications and storage is empty', async () => { - const service = createKeylessService({ - storage: createStorage(), - api: { completeOnboarding: vi.fn(() => Promise.resolve(accountlessApplication)) }, - framework: 'astro', - }); - - await expect(service.getOrCreateKeys()).resolves.toBeNull(); - }); - - it('getOrCreateKeys returns stored keys even when the API cannot create applications', async () => { - const storage = createStorage(); - storage.write(JSON.stringify(accountlessApplication)); - - const service = createKeylessService({ - storage, - api: { completeOnboarding: vi.fn(() => Promise.resolve(accountlessApplication)) }, - framework: 'astro', - }); - - await expect(service.getOrCreateKeys()).resolves.toEqual(accountlessApplication); - }); - it('passes the framework as the source when completing accountless application onboarding', async () => { const completeOnboarding = vi.fn(() => Promise.resolve(accountlessApplication)); @@ -89,50 +47,73 @@ describe('createKeylessService', () => { }); it('sanitizes the framework before passing it as the source', async () => { - const createAccountlessApplication = vi.fn(() => - Promise.resolve(accountlessApplication), - ); + const completeOnboarding = vi.fn(() => Promise.resolve(accountlessApplication)); const service = createKeylessService({ storage: createStorage(), - api: createApi({ createAccountlessApplication }), + api: createApi({ completeOnboarding }), framework: 'Next.js @ Canary!', }); - await service.getOrCreateKeys(); + await service.completeOnboarding(); - expect(createAccountlessApplication.mock.calls[0][1]).toBe('next.js-canary'); + expect(completeOnboarding.mock.calls[0][1]).toBe('next.js-canary'); }); it('falls back to javascript when framework sanitization produces an empty source', async () => { - const createAccountlessApplication = vi.fn(() => - Promise.resolve(accountlessApplication), - ); + const completeOnboarding = vi.fn(() => Promise.resolve(accountlessApplication)); const service = createKeylessService({ storage: createStorage(), - api: createApi({ createAccountlessApplication }), + api: createApi({ completeOnboarding }), framework: '!!!', }); - await service.getOrCreateKeys(); + await service.completeOnboarding(); - expect(createAccountlessApplication.mock.calls[0][1]).toBe('javascript'); + expect(completeOnboarding.mock.calls[0][1]).toBe('javascript'); }); it('truncates the source before passing it to the accountless application API', async () => { - const createAccountlessApplication = vi.fn(() => - Promise.resolve(accountlessApplication), - ); + const completeOnboarding = vi.fn(() => Promise.resolve(accountlessApplication)); const service = createKeylessService({ storage: createStorage(), - api: createApi({ createAccountlessApplication }), + api: createApi({ completeOnboarding }), framework: 'a'.repeat(50), }); - await service.getOrCreateKeys(); + await service.completeOnboarding(); + + expect(completeOnboarding.mock.calls[0][1]).toBe('a'.repeat(36)); + }); + + it('readKeys returns the stored configuration', () => { + const storage = createStorage(); + storage.write(JSON.stringify(accountlessApplication)); + + const service = createKeylessService({ storage, api: createApi() }); + + expect(service.readKeys()).toEqual(accountlessApplication); + }); + + it('readKeys returns undefined when storage is empty or invalid', () => { + const emptyService = createKeylessService({ storage: createStorage(), api: createApi() }); + expect(emptyService.readKeys()).toBeUndefined(); + + const corruptStorage = createStorage(); + corruptStorage.write('not-json'); + const corruptService = createKeylessService({ storage: corruptStorage, api: createApi() }); + expect(corruptService.readKeys()).toBeUndefined(); + }); + + it('removeKeys clears the stored configuration', () => { + const storage = createStorage(); + storage.write(JSON.stringify(accountlessApplication)); + + const service = createKeylessService({ storage, api: createApi() }); + service.removeKeys(); - expect(createAccountlessApplication.mock.calls[0][1]).toBe('a'.repeat(36)); + expect(service.readKeys()).toBeUndefined(); }); }); diff --git a/packages/shared/src/keyless/devCache.ts b/packages/shared/src/keyless/devCache.ts index 0fbdabd24e1..ebc060453af 100644 --- a/packages/shared/src/keyless/devCache.ts +++ b/packages/shared/src/keyless/devCache.ts @@ -1,5 +1,4 @@ import { isDevelopmentEnvironment } from '../utils/runtimeEnvironment'; -import type { AccountlessApplication, PublicKeylessApplication } from './types'; // 10 minutes in milliseconds const THROTTLE_DURATION_MS = 10 * 60 * 1000; @@ -84,16 +83,6 @@ export function createClerkDevCache(): ClerkDevCache | undefined { return globalThis.__clerk_internal_keyless_logger; } -/** - * Creates the console message shown when running in keyless mode. - * - * @param keys - The keyless application keys - * @returns Formatted console message - */ -export function createKeylessModeMessage(keys: AccountlessApplication | PublicKeylessApplication): string { - return `\n\x1b[35m\n[Clerk]:\x1b[0m You are running in keyless mode.\nYou can \x1b[35mclaim your keys\x1b[0m by visiting ${keys.claimUrl}\n`; -} - /** * Creates the console message shown when keys have been claimed. * diff --git a/packages/shared/src/keyless/index.ts b/packages/shared/src/keyless/index.ts index 92c5175ea45..ee2512336f7 100644 --- a/packages/shared/src/keyless/index.ts +++ b/packages/shared/src/keyless/index.ts @@ -1,9 +1,4 @@ -export { - clerkDevelopmentCache, - createClerkDevCache, - createConfirmationMessage, - createKeylessModeMessage, -} from './devCache'; +export { clerkDevelopmentCache, createConfirmationMessage } from './devCache'; export type { ClerkDevCache } from './devCache'; export { createNodeFileStorage } from './nodeFileStorage'; @@ -12,15 +7,6 @@ export type { FileSystemAdapter, NodeFileStorageOptions, PathAdapter } from './n export { completeClaimedOnboarding } from './completeClaimedOnboarding'; export { createKeylessService } from './service'; -export type { - KeylessAPI, - KeylessCompletionAPI, - KeylessService, - KeylessServiceOptions, - KeylessStorage, -} from './service'; +export type { KeylessAPI, KeylessService, KeylessServiceOptions, KeylessStorage } from './service'; -export { resolveKeysWithKeylessFallback } from './resolveKeysWithKeylessFallback'; -export type { KeylessResult } from './resolveKeysWithKeylessFallback'; - -export type { AccountlessApplication, PublicKeylessApplication } from './types'; +export type { AccountlessApplication } from './types'; diff --git a/packages/shared/src/keyless/resolveKeysWithKeylessFallback.ts b/packages/shared/src/keyless/resolveKeysWithKeylessFallback.ts deleted file mode 100644 index 8d54a315d43..00000000000 --- a/packages/shared/src/keyless/resolveKeysWithKeylessFallback.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { completeClaimedOnboarding } from './completeClaimedOnboarding'; -import { clerkDevelopmentCache, createKeylessModeMessage } from './devCache'; -import type { KeylessService } from './service'; -import type { AccountlessApplication } from './types'; - -export interface KeylessResult { - publishableKey: string | undefined; - secretKey: string | undefined; - claimUrl: string | undefined; - apiKeysUrl: string | undefined; -} - -/** - * Resolves Clerk keys, falling back to keyless mode in development if configured keys are missing. - * - * @param configuredPublishableKey - The publishable key from options or environment - * @param configuredSecretKey - The secret key from options or environment - * @param keylessService - The keyless service instance (or null if unavailable) - * @param canUseKeyless - Whether keyless mode is enabled in the current environment - * @returns The resolved keys (either configured or from keyless mode) - */ -export async function resolveKeysWithKeylessFallback( - configuredPublishableKey: string | undefined, - configuredSecretKey: string | undefined, - keylessService: KeylessService | null, - canUseKeyless: boolean, -): Promise { - let publishableKey = configuredPublishableKey; - let secretKey = configuredSecretKey; - let claimUrl: string | undefined; - let apiKeysUrl: string | undefined; - - if (!canUseKeyless) { - return { publishableKey, secretKey, claimUrl, apiKeysUrl }; - } - - if (!keylessService) { - return { publishableKey, secretKey, claimUrl, apiKeysUrl }; - } - - try { - const locallyStoredKeys = keylessService.readKeys(); - - // Check if running with claimed keys (configured keys match locally stored keyless keys) - const runningWithClaimedKeys = - Boolean(configuredPublishableKey) && configuredPublishableKey === locallyStoredKeys?.publishableKey; - - if (runningWithClaimedKeys && locallyStoredKeys) { - await completeClaimedOnboarding(locallyStoredKeys.publishableKey, keylessService); - return { publishableKey, secretKey, claimUrl, apiKeysUrl }; - } - - // In keyless mode, try to read/create keys from the file system - if (!publishableKey && !secretKey) { - const keylessApp: AccountlessApplication | null = await keylessService.getOrCreateKeys(); - - if (keylessApp) { - publishableKey = keylessApp.publishableKey; - secretKey = keylessApp.secretKey; - claimUrl = keylessApp.claimUrl; - apiKeysUrl = keylessApp.apiKeysUrl; - - clerkDevelopmentCache?.log({ - cacheKey: keylessApp.publishableKey, - msg: createKeylessModeMessage(keylessApp), - }); - } - } - } catch { - // noop - fall through to return whatever keys we have - } - - return { publishableKey, secretKey, claimUrl, apiKeysUrl }; -} diff --git a/packages/shared/src/keyless/service.ts b/packages/shared/src/keyless/service.ts index 760f5cb136d..795812d8c59 100644 --- a/packages/shared/src/keyless/service.ts +++ b/packages/shared/src/keyless/service.ts @@ -1,5 +1,3 @@ -import { completeClaimedOnboarding } from './completeClaimedOnboarding'; -import { clerkDevelopmentCache, createKeylessModeMessage } from './devCache'; import type { AccountlessApplication } from './types'; const KEYLESS_SOURCE_FALLBACK = 'javascript'; @@ -35,21 +33,10 @@ export interface KeylessStorage { } /** - * API adapter for SDKs that only complete onboarding for already-claimed keyless - * applications and no longer mint new ones. + * API adapter for keyless mode operations on already-claimed applications. * This abstraction allows the service to work without depending on @clerk/backend. */ -export interface KeylessCompletionAPI { - /** - * Creates a new accountless application. - * Omitted by SDKs that no longer mint keyless applications. - * - * @param requestHeaders - Optional headers to include with the request. - * @param source - Optional source value to include with the request. - * @returns The created AccountlessApplication or null if failed. - */ - createAccountlessApplication?(requestHeaders?: Headers, source?: string): Promise; - +export interface KeylessAPI { /** * Notifies the backend that onboarding is complete (instance has been claimed). * @@ -60,13 +47,6 @@ export interface KeylessCompletionAPI { completeOnboarding(requestHeaders?: Headers, source?: string): Promise; } -/** - * API adapter for keyless mode operations, for SDKs that can still mint keyless applications. - */ -export interface KeylessAPI extends KeylessCompletionAPI { - createAccountlessApplication(requestHeaders?: Headers, source?: string): Promise; -} - /** * Options for creating a keyless service. */ @@ -77,9 +57,9 @@ export interface KeylessServiceOptions { storage: KeylessStorage; /** - * API adapter for keyless operations (complete onboarding, optionally create application). + * API adapter for keyless operations (complete onboarding). */ - api: KeylessCompletionAPI; + api: KeylessAPI; /** * Optional: Framework name for metadata (e.g., 'Next.js', 'TanStack Start'). @@ -92,25 +72,10 @@ export interface KeylessServiceOptions { frameworkVersion?: string; } -/** - * Result type for key resolution. - */ -export interface KeylessResult { - publishableKey: string | undefined; - secretKey: string | undefined; - claimUrl: string | undefined; - apiKeysUrl: string | undefined; -} - /** * The keyless service interface. */ export interface KeylessService { - /** - * Gets existing keyless keys or creates new ones via the API. - */ - getOrCreateKeys: () => Promise; - /** * Reads existing keyless keys without creating new ones. */ @@ -126,23 +91,6 @@ export interface KeylessService { * This should be called once when the user claims their instance. */ completeOnboarding: () => Promise; - - /** - * Logs a keyless mode message to the console (throttled to once per process). - */ - logKeylessMessage: (claimUrl: string) => void; - - /** - * Resolves Clerk keys, falling back to keyless mode if configured keys are missing. - * - * @param configuredPublishableKey - The publishable key from options or environment - * @param configuredSecretKey - The secret key from options or environment - * @returns The resolved keys (either configured or from keyless mode) - */ - resolveKeysWithKeylessFallback: ( - configuredPublishableKey: string | undefined, - configuredSecretKey: string | undefined, - ) => Promise; } /** @@ -172,8 +120,8 @@ function createSource(framework?: string): string { } /** - * Creates a keyless service that handles accountless application creation and storage. - * This provides a simple API for frameworks to integrate keyless mode. + * Creates a keyless service that reads stored keyless keys and completes onboarding + * for claimed applications. * * @param options - Configuration for the service including storage and API adapters * @returns A keyless service instance @@ -184,20 +132,16 @@ function createSource(framework?: string): string { * * const keylessService = createKeylessService({ * storage: createFileStorage(), - * api: createKeylessAPI({ secretKey }), + * api: { completeOnboarding }, * framework: 'TanStack Start', * }); * - * const keys = await keylessService.getOrCreateKeys(request); - * if (keys) { - * console.log('Publishable Key:', keys.publishableKey); - * } + * const keys = keylessService.readKeys(); * ``` */ export function createKeylessService(options: KeylessServiceOptions): KeylessService { const { storage, api, framework, frameworkVersion } = options; - let hasLoggedKeylessMessage = false; const source = createSource(framework); const safeParseConfig = (): AccountlessApplication | undefined => { @@ -213,30 +157,6 @@ export function createKeylessService(options: KeylessServiceOptions): KeylessSer }; return { - async getOrCreateKeys(): Promise { - // Check for existing config first - const existingConfig = safeParseConfig(); - if (existingConfig?.publishableKey && existingConfig?.secretKey) { - return existingConfig; - } - - if (!api.createAccountlessApplication) { - return null; - } - - // Create metadata headers - const headers = createMetadataHeaders(framework, frameworkVersion); - - // Create new keys via the API - const accountlessApplication = await api.createAccountlessApplication(headers, source); - - if (accountlessApplication) { - storage.write(JSON.stringify(accountlessApplication)); - } - - return accountlessApplication; - }, - readKeys(): AccountlessApplication | undefined { return safeParseConfig(); }, @@ -249,56 +169,5 @@ export function createKeylessService(options: KeylessServiceOptions): KeylessSer const headers = createMetadataHeaders(framework, frameworkVersion); return api.completeOnboarding(headers, source); }, - - logKeylessMessage(claimUrl: string): void { - if (!hasLoggedKeylessMessage) { - hasLoggedKeylessMessage = true; - console.log(`[Clerk]: Running in keyless mode. Claim your keys at: ${claimUrl}`); - } - }, - - async resolveKeysWithKeylessFallback( - configuredPublishableKey: string | undefined, - configuredSecretKey: string | undefined, - ): Promise { - let publishableKey = configuredPublishableKey; - let secretKey = configuredSecretKey; - let claimUrl: string | undefined; - let apiKeysUrl: string | undefined; - - try { - const locallyStoredKeys = safeParseConfig(); - - // Check if running with claimed keys (configured keys match locally stored keyless keys) - const runningWithClaimedKeys = - Boolean(configuredPublishableKey) && configuredPublishableKey === locallyStoredKeys?.publishableKey; - - if (runningWithClaimedKeys && locallyStoredKeys) { - await completeClaimedOnboarding(locallyStoredKeys.publishableKey, this); - return { publishableKey, secretKey, claimUrl, apiKeysUrl }; - } - - // In keyless mode, try to read/create keys from the file system - if (!publishableKey && !secretKey) { - const keylessApp: AccountlessApplication | null = await this.getOrCreateKeys(); - - if (keylessApp) { - publishableKey = keylessApp.publishableKey; - secretKey = keylessApp.secretKey; - claimUrl = keylessApp.claimUrl; - apiKeysUrl = keylessApp.apiKeysUrl; - - clerkDevelopmentCache?.log({ - cacheKey: keylessApp.publishableKey, - msg: createKeylessModeMessage(keylessApp), - }); - } - } - } catch { - // noop - fall through to return whatever keys we have - } - - return { publishableKey, secretKey, claimUrl, apiKeysUrl }; - }, }; } diff --git a/packages/shared/src/keyless/types.ts b/packages/shared/src/keyless/types.ts index 18b13e1e57a..294bbc42926 100644 --- a/packages/shared/src/keyless/types.ts +++ b/packages/shared/src/keyless/types.ts @@ -12,8 +12,3 @@ export interface AccountlessApplication { readonly claimUrl: string; readonly apiKeysUrl: string; } - -/** - * Public-facing keyless application data (without secret key). - */ -export type PublicKeylessApplication = Omit;