Skip to content

Commit 4773d0a

Browse files
authored
feat(nextjs): Mark keyless onboarding as complete when stored keys match explicit keys (#4971)
1 parent 83f1456 commit 4773d0a

File tree

8 files changed

+92
-13
lines changed

8 files changed

+92
-13
lines changed

.changeset/fuzzy-ghosts-kneel.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/backend': patch
3+
'@clerk/nextjs': patch
4+
---
5+
6+
Mark keyless onboarding as complete when stored keys match explicit keys

packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { joinPaths } from '../../util/path';
12
import type { AccountlessApplication } from '../resources/AccountlessApplication';
23
import { AbstractAPI } from './AbstractApi';
34

@@ -10,4 +11,11 @@ export class AccountlessApplicationAPI extends AbstractAPI {
1011
path: basePath,
1112
});
1213
}
14+
15+
public async completeAccountlessApplicationOnboarding() {
16+
return this.request<AccountlessApplication>({
17+
method: 'POST',
18+
path: joinPaths(basePath, 'complete'),
19+
});
20+
}
1321
}

packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { useRevalidateEnvironment } from './use-revalidate-environment';
1515
type KeylessPromptProps = {
1616
claimUrl: string;
1717
copyKeysUrl: string;
18-
onDismiss: (() => Promise<unknown>) | undefined;
18+
onDismiss: (() => Promise<unknown>) | undefined | null;
1919
};
2020

2121
const buttonIdentifierPrefix = `--clerk-keyless-prompt`;

packages/nextjs/src/app-router/keyless-actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ export async function createOrReadKeylessAction(): Promise<null | Omit<Accountle
4343
return null;
4444
}
4545

46-
const { keylessLogger, createKeylessModeMessage } = await import('../server/keyless-log-cache.js');
46+
const { clerkDevelopmentCache, createKeylessModeMessage } = await import('../server/keyless-log-cache.js');
4747

4848
/**
4949
* Notify developers.
5050
*/
51-
keylessLogger?.log({
51+
clerkDevelopmentCache?.log({
5252
cacheKey: result.publishableKey,
5353
msg: createKeylessModeMessage(result),
5454
});

packages/nextjs/src/app-router/server/ClerkProvider.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react';
44

55
import { PromisifiedAuthProvider } from '../../client-boundary/PromisifiedAuthProvider';
66
import { getDynamicAuthData } from '../../server/buildClerkProps';
7+
import { createClerkClientWithOptions } from '../../server/createClerkClient';
78
import type { NextClerkProviderProps } from '../../types';
89
import { canUseKeyless } from '../../utils/feature-flags';
910
import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv';
@@ -96,7 +97,7 @@ export async function ClerkProvider(
9697
const newOrReadKeys = await import('../../server/keyless-node.js')
9798
.then(mod => mod.createOrReadKeyless())
9899
.catch(() => null);
99-
const { keylessLogger, createConfirmationMessage, createKeylessModeMessage } = await import(
100+
const { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } = await import(
100101
'../../server/keyless-log-cache.js'
101102
);
102103

@@ -108,7 +109,8 @@ export async function ClerkProvider(
108109
publishableKey: newOrReadKeys.publishableKey,
109110
__internal_keyless_claimKeylessApplicationUrl: newOrReadKeys.claimUrl,
110111
__internal_keyless_copyInstanceKeysUrl: newOrReadKeys.apiKeysUrl,
111-
__internal_keyless_dismissPrompt: runningWithClaimedKeys ? deleteKeylessAction : undefined,
112+
// Explicitly use `null` instead of `undefined` here to avoid persisting `deleteKeylessAction` during merging of options.
113+
__internal_keyless_dismissPrompt: runningWithClaimedKeys ? deleteKeylessAction : null,
112114
})}
113115
nonce={await generateNonce()}
114116
initialState={await generateStatePromise()}
@@ -118,10 +120,37 @@ export async function ClerkProvider(
118120
);
119121

120122
if (runningWithClaimedKeys) {
123+
try {
124+
const secretKey = await import('../../server/keyless-node.js').then(
125+
mod => mod.safeParseClerkFile()?.secretKey,
126+
);
127+
if (!secretKey) {
128+
// we will ignore it later
129+
throw new Error(secretKey);
130+
}
131+
const client = createClerkClientWithOptions({
132+
secretKey,
133+
});
134+
135+
/**
136+
* Notifying the dashboard the should runs once. We are controlling this behaviour by caching the result of the request.
137+
* If the request fails, it will be considered stale after 10 minutes, otherwise it is cached for 24 hours.
138+
*/
139+
await clerkDevelopmentCache?.run(
140+
() => client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding(),
141+
{
142+
cacheKey: `${newOrReadKeys.publishableKey}_complete`,
143+
onSuccessStale: 24 * 60 * 60 * 1000, // 24 hours
144+
},
145+
);
146+
} catch {
147+
// ignore
148+
}
149+
121150
/**
122151
* Notify developers.
123152
*/
124-
keylessLogger?.log({
153+
clerkDevelopmentCache?.log({
125154
cacheKey: `${newOrReadKeys.publishableKey}_claimed`,
126155
msg: createConfirmationMessage(),
127156
});
@@ -145,7 +174,7 @@ export async function ClerkProvider(
145174
/**
146175
* Notify developers.
147176
*/
148-
keylessLogger?.log({
177+
clerkDevelopmentCache?.log({
149178
cacheKey: newOrReadKeys.publishableKey,
150179
msg: createKeylessModeMessage({ ...newOrReadKeys, claimUrl: claimUrl.href }),
151180
});

packages/nextjs/src/global.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,20 @@ declare namespace globalThis {
4444
// eslint-disable-next-line no-var
4545
var __clerk_internal_keyless_logger:
4646
| {
47-
__cache: Map<string, { expiresAt: number }>;
47+
__cache: Map<string, { expiresAt: number; data?: unknown }>;
4848
log: (param: { cacheKey: string; msg: string }) => void;
49+
run: (
50+
callback: () => Promise<unknown>,
51+
{
52+
cacheKey,
53+
onSuccessStale,
54+
onErrorStale,
55+
}: {
56+
cacheKey: string;
57+
onSuccessStale?: number;
58+
onErrorStale?: number;
59+
},
60+
) => Promise<unknown>;
4961
}
5062
| undefined;
5163
}

packages/nextjs/src/server/keyless-log-cache.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ import { isDevelopmentEnvironment } from '@clerk/shared/utils';
33
// 10 minutes in milliseconds
44
const THROTTLE_DURATION_MS = 10 * 60 * 1000;
55

6-
function createClerkDevLogger() {
6+
function createClerkDevCache() {
77
if (!isDevelopmentEnvironment()) {
88
return;
99
}
1010

1111
if (!global.__clerk_internal_keyless_logger) {
1212
global.__clerk_internal_keyless_logger = {
13-
__cache: new Map<string, { expiresAt: number }>(),
13+
__cache: new Map<string, { expiresAt: number; data?: unknown }>(),
1414

15-
log: function ({ cacheKey, msg }: { cacheKey: string; msg: string }) {
15+
log: function ({ cacheKey, msg }) {
1616
if (this.__cache.has(cacheKey) && Date.now() < (this.__cache.get(cacheKey)?.expiresAt || 0)) {
1717
return;
1818
}
@@ -23,6 +23,30 @@ function createClerkDevLogger() {
2323
expiresAt: Date.now() + THROTTLE_DURATION_MS,
2424
});
2525
},
26+
run: async function (
27+
callback,
28+
{ cacheKey, onSuccessStale = THROTTLE_DURATION_MS, onErrorStale = THROTTLE_DURATION_MS },
29+
) {
30+
if (this.__cache.has(cacheKey) && Date.now() < (this.__cache.get(cacheKey)?.expiresAt || 0)) {
31+
return this.__cache.get(cacheKey)?.data;
32+
}
33+
34+
try {
35+
const result = await callback();
36+
37+
this.__cache.set(cacheKey, {
38+
expiresAt: Date.now() + onSuccessStale,
39+
data: result,
40+
});
41+
return result;
42+
} catch (e) {
43+
this.__cache.set(cacheKey, {
44+
expiresAt: Date.now() + onErrorStale,
45+
});
46+
47+
throw e;
48+
}
49+
},
2650
};
2751
}
2852

@@ -37,4 +61,4 @@ export const createConfirmationMessage = () => {
3761
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`;
3862
};
3963

40-
export const keylessLogger = createClerkDevLogger();
64+
export const clerkDevelopmentCache = createClerkDevCache();

packages/types/src/clerk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ export type ClerkOptions = ClerkOptionsNavigation &
780780
* Pass a function that will trigger the unmounting of the Keyless Prompt.
781781
* It should cause the values of `__internal_claimKeylessApplicationUrl` and `__internal_copyInstanceKeysUrl` to become undefined.
782782
*/
783-
__internal_keyless_dismissPrompt?: () => Promise<void>;
783+
__internal_keyless_dismissPrompt?: (() => Promise<void>) | null;
784784
};
785785

786786
export interface NavigateOptions {

0 commit comments

Comments
 (0)