Skip to content

Commit 5995193

Browse files
committed
feat(astro): Add support for keyless mode
1 parent 833b325 commit 5995193

9 files changed

Lines changed: 211 additions & 4 deletions

File tree

packages/astro/src/env.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ interface InternalEnv {
2020
readonly PUBLIC_CLERK_SIGN_UP_URL?: string;
2121
readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string;
2222
readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string;
23+
readonly PUBLIC_CLERK_KEYLESS_CLAIM_URL?: string;
24+
readonly PUBLIC_CLERK_KEYLESS_API_KEYS_URL?: string;
25+
readonly PUBLIC_CLERK_KEYLESS_DISABLED?: string;
2326
}
2427

2528
interface ImportMeta {

packages/astro/src/integration/create-integration.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { AstroIntegration } from 'astro';
33
import { envField } from 'astro/config';
44

55
import { name as packageName, version as packageVersion } from '../../package.json';
6+
import { resolveKeysWithKeylessFallback } from '../server/keyless/utils';
67
import type { AstroClerkIntegrationParams } from '../types';
78
import { vitePluginAstroConfig } from './vite-plugin-astro-config';
89

@@ -26,17 +27,36 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
2627
return {
2728
name: '@clerk/astro/integration',
2829
hooks: {
29-
'astro:config:setup': ({ config, injectScript, updateConfig, logger, command }) => {
30+
'astro:config:setup': async ({ config, injectScript, updateConfig, logger, command }) => {
3031
if (['server', 'hybrid'].includes(config.output) && !config.adapter) {
3132
logger.error('Missing adapter, please update your Astro config to use one.');
3233
}
3334

35+
const envPublishableKey = process.env.PUBLIC_CLERK_PUBLISHABLE_KEY;
36+
const envSecretKey = process.env.CLERK_SECRET_KEY;
37+
38+
const isDev = command === 'dev';
39+
let resolvedKeys = {
40+
publishableKey: envPublishableKey,
41+
secretKey: envSecretKey,
42+
claimUrl: undefined as string | undefined,
43+
apiKeysUrl: undefined as string | undefined,
44+
};
45+
46+
if (isDev) {
47+
try {
48+
resolvedKeys = await resolveKeysWithKeylessFallback(envPublishableKey, envSecretKey);
49+
} catch {
50+
logger.warn('Keyless mode initialization failed, using configured keys');
51+
}
52+
}
53+
3454
const internalParams: ClerkOptions = {
3555
...params,
3656
sdkMetadata: {
3757
version: packageVersion,
3858
name: packageName,
39-
environment: command === 'dev' ? 'development' : 'production',
59+
environment: isDev ? 'development' : 'production',
4060
},
4161
};
4262

@@ -58,6 +78,10 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
5878
...buildEnvVarFromOption(clerkJSUrl, 'PUBLIC_CLERK_JS_URL'),
5979
...buildEnvVarFromOption(clerkJSVersion, 'PUBLIC_CLERK_JS_VERSION'),
6080
...buildEnvVarFromOption(prefetchUI === false ? 'false' : undefined, 'PUBLIC_CLERK_PREFETCH_UI'),
81+
...buildEnvVarFromOption(resolvedKeys.publishableKey, 'PUBLIC_CLERK_PUBLISHABLE_KEY'),
82+
...buildEnvVarFromOption(resolvedKeys.secretKey, 'CLERK_SECRET_KEY'),
83+
...buildEnvVarFromOption(resolvedKeys.claimUrl, 'PUBLIC_CLERK_KEYLESS_CLAIM_URL'),
84+
...buildEnvVarFromOption(resolvedKeys.apiKeysUrl, 'PUBLIC_CLERK_KEYLESS_API_KEYS_URL'),
6185
},
6286

6387
ssr: {
@@ -157,7 +181,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
157181

158182
function createClerkEnvSchema() {
159183
return {
160-
PUBLIC_CLERK_PUBLISHABLE_KEY: envField.string({ context: 'client', access: 'public' }),
184+
PUBLIC_CLERK_PUBLISHABLE_KEY: envField.string({ context: 'client', access: 'public', optional: true }),
161185
PUBLIC_CLERK_SIGN_IN_URL: envField.string({ context: 'client', access: 'public', optional: true }),
162186
PUBLIC_CLERK_SIGN_UP_URL: envField.string({ context: 'client', access: 'public', optional: true }),
163187
PUBLIC_CLERK_IS_SATELLITE: envField.boolean({ context: 'client', access: 'public', optional: true }),
@@ -169,7 +193,20 @@ function createClerkEnvSchema() {
169193
PUBLIC_CLERK_UI_URL: envField.string({ context: 'client', access: 'public', optional: true, url: true }),
170194
PUBLIC_CLERK_TELEMETRY_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }),
171195
PUBLIC_CLERK_TELEMETRY_DEBUG: envField.boolean({ context: 'client', access: 'public', optional: true }),
172-
CLERK_SECRET_KEY: envField.string({ context: 'server', access: 'secret' }),
196+
PUBLIC_CLERK_KEYLESS_CLAIM_URL: envField.string({
197+
context: 'client',
198+
access: 'public',
199+
optional: true,
200+
url: true,
201+
}),
202+
PUBLIC_CLERK_KEYLESS_API_KEYS_URL: envField.string({
203+
context: 'client',
204+
access: 'public',
205+
optional: true,
206+
url: true,
207+
}),
208+
PUBLIC_CLERK_KEYLESS_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }),
209+
CLERK_SECRET_KEY: envField.string({ context: 'server', access: 'secret', optional: true }),
173210
CLERK_MACHINE_SECRET_KEY: envField.string({ context: 'server', access: 'secret', optional: true }),
174211
CLERK_JWT_KEY: envField.string({ context: 'server', access: 'secret', optional: true }),
175212
};

packages/astro/src/internal/create-clerk-instance.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,17 @@ async function createClerkInstanceInternal<TUi extends Ui = Ui>(options?: AstroC
5454
$clerk.set(clerkJSInstance);
5555
}
5656

57+
const keylessClaimUrl = (options as any)?.__internal_keylessClaimUrl;
58+
const keylessApiKeysUrl = (options as any)?.__internal_keylessApiKeysUrl;
59+
5760
const clerkOptions = {
5861
routerPush: createNavigationHandler(window.history.pushState.bind(window.history)),
5962
routerReplace: createNavigationHandler(window.history.replaceState.bind(window.history)),
6063
...options,
6164
// Pass the clerk-ui constructor promise to clerk.load()
6265
clerkUICtor,
66+
...(keylessClaimUrl && { __internal_keyless_claimKeylessApplicationUrl: keylessClaimUrl }),
67+
...(keylessApiKeysUrl && { __internal_keyless_copyInstanceKeysUrl: keylessApiKeysUrl }),
6368
} as unknown as ClerkOptions;
6469

6570
initOptions = clerkOptions;

packages/astro/src/internal/merge-env-vars-with-params.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish
5656
disabled: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DISABLED),
5757
debug: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DEBUG),
5858
},
59+
__internal_keylessClaimUrl: import.meta.env.PUBLIC_CLERK_KEYLESS_CLAIM_URL,
60+
__internal_keylessApiKeysUrl: import.meta.env.PUBLIC_CLERK_KEYLESS_API_KEYS_URL,
5961
...rest,
6062
};
6163
};

packages/astro/src/server/get-safe-env.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ function getSafeEnv(context: ContextOrLocals) {
3838
apiUrl: getContextEnvVar('CLERK_API_URL', context),
3939
telemetryDisabled: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DISABLED', context)),
4040
telemetryDebug: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DEBUG', context)),
41+
keylessClaimUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_CLAIM_URL', context),
42+
keylessApiKeysUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_API_KEYS_URL', context),
4143
};
4244
}
4345

@@ -55,6 +57,8 @@ function getClientSafeEnv(context: ContextOrLocals) {
5557
proxyUrl: getContextEnvVar('PUBLIC_CLERK_PROXY_URL', context),
5658
signInUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_IN_URL', context),
5759
signUpUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_UP_URL', context),
60+
keylessClaimUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_CLAIM_URL', context),
61+
keylessApiKeysUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_API_KEYS_URL', context),
5862
};
5963
}
6064

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { KeylessStorage } from '@clerk/shared/keyless';
2+
3+
export type { KeylessStorage };
4+
5+
export interface FileStorageOptions {
6+
cwd?: () => string;
7+
}
8+
9+
export async function createFileStorage(options: FileStorageOptions = {}): Promise<KeylessStorage> {
10+
const { cwd = () => process.cwd() } = options;
11+
12+
const [{ default: fs }, { default: path }] = await Promise.all([import('node:fs'), import('node:path')]);
13+
14+
const { createNodeFileStorage } = await import('@clerk/shared/keyless');
15+
16+
return createNodeFileStorage(fs, path, {
17+
cwd,
18+
frameworkPackageName: '@clerk/astro',
19+
});
20+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { createKeylessService } from '@clerk/shared/keyless';
2+
import type { APIContext } from 'astro';
3+
4+
import { clerkClient } from '../clerk-client';
5+
import { createFileStorage } from './file-storage.js';
6+
7+
let keylessServiceInstance: ReturnType<typeof createKeylessService> | null = null;
8+
9+
export async function keyless() {
10+
if (!keylessServiceInstance) {
11+
const storage = await createFileStorage();
12+
13+
keylessServiceInstance = createKeylessService({
14+
storage,
15+
api: {
16+
async createAccountlessApplication(requestHeaders?: Headers) {
17+
try {
18+
const mockContext = {
19+
locals: { runtime: { env: {} } },
20+
} as unknown as APIContext;
21+
22+
return await clerkClient(mockContext).__experimental_accountlessApplications.createAccountlessApplication({
23+
requestHeaders,
24+
});
25+
} catch {
26+
return null;
27+
}
28+
},
29+
async completeOnboarding(requestHeaders?: Headers) {
30+
try {
31+
const mockContext = {
32+
locals: { runtime: { env: {} } },
33+
} as unknown as APIContext;
34+
35+
return await clerkClient(
36+
mockContext,
37+
).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({
38+
requestHeaders,
39+
});
40+
} catch {
41+
return null;
42+
}
43+
},
44+
},
45+
framework: '@clerk/astro',
46+
frameworkVersion: PACKAGE_VERSION,
47+
});
48+
}
49+
50+
return keylessServiceInstance;
51+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { AccountlessApplication } from '@clerk/shared/keyless';
2+
import { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } from '@clerk/shared/keyless';
3+
4+
import { canUseKeyless } from '../../utils/feature-flags';
5+
import { keyless } from './index';
6+
7+
export interface KeylessResult {
8+
publishableKey: string | undefined;
9+
secretKey: string | undefined;
10+
claimUrl: string | undefined;
11+
apiKeysUrl: string | undefined;
12+
}
13+
14+
export async function resolveKeysWithKeylessFallback(
15+
configuredPublishableKey: string | undefined,
16+
configuredSecretKey: string | undefined,
17+
): Promise<KeylessResult> {
18+
let publishableKey = configuredPublishableKey;
19+
let secretKey = configuredSecretKey;
20+
let claimUrl: string | undefined;
21+
let apiKeysUrl: string | undefined;
22+
23+
if (!canUseKeyless) {
24+
return { publishableKey, secretKey, claimUrl, apiKeysUrl };
25+
}
26+
27+
const keylessService = await keyless();
28+
const locallyStoredKeys = keylessService.readKeys();
29+
30+
const runningWithClaimedKeys =
31+
Boolean(configuredPublishableKey) && configuredPublishableKey === locallyStoredKeys?.publishableKey;
32+
33+
if (runningWithClaimedKeys && locallyStoredKeys) {
34+
try {
35+
await clerkDevelopmentCache?.run(() => keylessService.completeOnboarding(), {
36+
cacheKey: `${locallyStoredKeys.publishableKey}_complete`,
37+
onSuccessStale: 24 * 60 * 60 * 1000,
38+
});
39+
} catch {
40+
// noop
41+
}
42+
43+
clerkDevelopmentCache?.log({
44+
cacheKey: `${locallyStoredKeys.publishableKey}_claimed`,
45+
msg: createConfirmationMessage(),
46+
});
47+
48+
return { publishableKey, secretKey, claimUrl, apiKeysUrl };
49+
}
50+
51+
if (!publishableKey && !secretKey) {
52+
const keylessApp: AccountlessApplication | null = await keylessService.getOrCreateKeys();
53+
54+
if (keylessApp) {
55+
publishableKey = keylessApp.publishableKey;
56+
secretKey = keylessApp.secretKey;
57+
claimUrl = keylessApp.claimUrl;
58+
apiKeysUrl = keylessApp.apiKeysUrl;
59+
60+
clerkDevelopmentCache?.log({
61+
cacheKey: keylessApp.publishableKey,
62+
msg: createKeylessModeMessage(keylessApp),
63+
});
64+
}
65+
}
66+
67+
return { publishableKey, secretKey, claimUrl, apiKeysUrl };
68+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { isTruthy } from '@clerk/shared/underscore';
2+
import { isDevelopmentEnvironment } from '@clerk/shared/utils';
3+
4+
function hasFileSystemSupport(): boolean {
5+
if (typeof process === 'undefined' || !process?.versions?.node) {
6+
return false;
7+
}
8+
if (typeof window !== 'undefined') {
9+
return false;
10+
}
11+
return true;
12+
}
13+
14+
const KEYLESS_DISABLED =
15+
isTruthy(import.meta.env.PUBLIC_CLERK_KEYLESS_DISABLED) || isTruthy(import.meta.env.CLERK_KEYLESS_DISABLED) || false;
16+
17+
export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED && hasFileSystemSupport();

0 commit comments

Comments
 (0)