diff --git a/.changeset/fuzzy-ghosts-kneel.md b/.changeset/fuzzy-ghosts-kneel.md new file mode 100644 index 00000000000..7f348c1ba6c --- /dev/null +++ b/.changeset/fuzzy-ghosts-kneel.md @@ -0,0 +1,6 @@ +--- +'@clerk/backend': patch +'@clerk/nextjs': patch +--- + +Mark keyless onboarding as complete when stored keys match explicit keys diff --git a/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts b/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts index 784ce061f94..fca8c1b3c64 100644 --- a/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts +++ b/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts @@ -1,3 +1,4 @@ +import { joinPaths } from '../../util/path'; import type { AccountlessApplication } from '../resources/AccountlessApplication'; import { AbstractAPI } from './AbstractApi'; @@ -10,4 +11,11 @@ export class AccountlessApplicationAPI extends AbstractAPI { path: basePath, }); } + + public async completeAccountlessApplicationOnboarding() { + return this.request({ + method: 'POST', + path: joinPaths(basePath, 'complete'), + }); + } } diff --git a/packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx b/packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx index 4bf08202262..b8f33c2137a 100644 --- a/packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx +++ b/packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx @@ -15,7 +15,7 @@ import { useRevalidateEnvironment } from './use-revalidate-environment'; type KeylessPromptProps = { claimUrl: string; copyKeysUrl: string; - onDismiss: (() => Promise) | undefined; + onDismiss: (() => Promise) | undefined | null; }; const buttonIdentifierPrefix = `--clerk-keyless-prompt`; diff --git a/packages/nextjs/src/app-router/keyless-actions.ts b/packages/nextjs/src/app-router/keyless-actions.ts index cd4ff10e23d..920751126c1 100644 --- a/packages/nextjs/src/app-router/keyless-actions.ts +++ b/packages/nextjs/src/app-router/keyless-actions.ts @@ -43,12 +43,12 @@ export async function createOrReadKeylessAction(): Promise mod.createOrReadKeyless()) .catch(() => null); - const { keylessLogger, createConfirmationMessage, createKeylessModeMessage } = await import( + const { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } = await import( '../../server/keyless-log-cache.js' ); @@ -108,7 +109,8 @@ export async function ClerkProvider( publishableKey: newOrReadKeys.publishableKey, __internal_keyless_claimKeylessApplicationUrl: newOrReadKeys.claimUrl, __internal_keyless_copyInstanceKeysUrl: newOrReadKeys.apiKeysUrl, - __internal_keyless_dismissPrompt: runningWithClaimedKeys ? deleteKeylessAction : undefined, + // Explicitly use `null` instead of `undefined` here to avoid persisting `deleteKeylessAction` during merging of options. + __internal_keyless_dismissPrompt: runningWithClaimedKeys ? deleteKeylessAction : null, })} nonce={await generateNonce()} initialState={await generateStatePromise()} @@ -118,10 +120,37 @@ export async function ClerkProvider( ); if (runningWithClaimedKeys) { + try { + const secretKey = await import('../../server/keyless-node.js').then( + mod => mod.safeParseClerkFile()?.secretKey, + ); + if (!secretKey) { + // we will ignore it later + throw new Error(secretKey); + } + const client = createClerkClientWithOptions({ + secretKey, + }); + + /** + * Notifying the dashboard the should runs 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( + () => client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding(), + { + cacheKey: `${newOrReadKeys.publishableKey}_complete`, + onSuccessStale: 24 * 60 * 60 * 1000, // 24 hours + }, + ); + } catch { + // ignore + } + /** * Notify developers. */ - keylessLogger?.log({ + clerkDevelopmentCache?.log({ cacheKey: `${newOrReadKeys.publishableKey}_claimed`, msg: createConfirmationMessage(), }); @@ -145,7 +174,7 @@ export async function ClerkProvider( /** * Notify developers. */ - keylessLogger?.log({ + clerkDevelopmentCache?.log({ cacheKey: newOrReadKeys.publishableKey, msg: createKeylessModeMessage({ ...newOrReadKeys, claimUrl: claimUrl.href }), }); diff --git a/packages/nextjs/src/global.d.ts b/packages/nextjs/src/global.d.ts index 60216cda94c..e9672e5b939 100644 --- a/packages/nextjs/src/global.d.ts +++ b/packages/nextjs/src/global.d.ts @@ -44,8 +44,20 @@ declare namespace globalThis { // eslint-disable-next-line no-var var __clerk_internal_keyless_logger: | { - __cache: Map; + __cache: Map; log: (param: { cacheKey: string; msg: string }) => void; + run: ( + callback: () => Promise, + { + cacheKey, + onSuccessStale, + onErrorStale, + }: { + cacheKey: string; + onSuccessStale?: number; + onErrorStale?: number; + }, + ) => Promise; } | undefined; } diff --git a/packages/nextjs/src/server/keyless-log-cache.ts b/packages/nextjs/src/server/keyless-log-cache.ts index 0f9a6536275..5a0624227a6 100644 --- a/packages/nextjs/src/server/keyless-log-cache.ts +++ b/packages/nextjs/src/server/keyless-log-cache.ts @@ -3,16 +3,16 @@ import { isDevelopmentEnvironment } from '@clerk/shared/utils'; // 10 minutes in milliseconds const THROTTLE_DURATION_MS = 10 * 60 * 1000; -function createClerkDevLogger() { +function createClerkDevCache() { if (!isDevelopmentEnvironment()) { return; } if (!global.__clerk_internal_keyless_logger) { global.__clerk_internal_keyless_logger = { - __cache: new Map(), + __cache: new Map(), - log: function ({ cacheKey, msg }: { cacheKey: string; msg: string }) { + log: function ({ cacheKey, msg }) { if (this.__cache.has(cacheKey) && Date.now() < (this.__cache.get(cacheKey)?.expiresAt || 0)) { return; } @@ -23,6 +23,30 @@ function createClerkDevLogger() { expiresAt: Date.now() + THROTTLE_DURATION_MS, }); }, + run: async function ( + callback, + { cacheKey, onSuccessStale = THROTTLE_DURATION_MS, onErrorStale = THROTTLE_DURATION_MS }, + ) { + if (this.__cache.has(cacheKey) && Date.now() < (this.__cache.get(cacheKey)?.expiresAt || 0)) { + return this.__cache.get(cacheKey)?.data; + } + + try { + const result = await callback(); + + this.__cache.set(cacheKey, { + expiresAt: Date.now() + onSuccessStale, + data: result, + }); + return result; + } catch (e) { + this.__cache.set(cacheKey, { + expiresAt: Date.now() + onErrorStale, + }); + + throw e; + } + }, }; } @@ -37,4 +61,4 @@ export const createConfirmationMessage = () => { return `\n\x1b[35m\n[Clerk]:\x1b[0m Your application is running with your claimed keys.\nYou can safely remove the \x1b[35m.clerk/\x1b[0m from your project.\n`; }; -export const keylessLogger = createClerkDevLogger(); +export const clerkDevelopmentCache = createClerkDevCache(); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 3bfd1740a55..f0cee60c6ac 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -780,7 +780,7 @@ export type ClerkOptions = ClerkOptionsNavigation & * Pass a function that will trigger the unmounting of the Keyless Prompt. * It should cause the values of `__internal_claimKeylessApplicationUrl` and `__internal_copyInstanceKeysUrl` to become undefined. */ - __internal_keyless_dismissPrompt?: () => Promise; + __internal_keyless_dismissPrompt?: (() => Promise) | null; }; export interface NavigateOptions {