diff --git a/integration/testUtils/keylessHelpers.ts b/integration/testUtils/keylessHelpers.ts new file mode 100644 index 00000000000..4c941e64f62 --- /dev/null +++ b/integration/testUtils/keylessHelpers.ts @@ -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 => { + 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 }); + }); +}; diff --git a/integration/tests/astro/keyless.test.ts b/integration/tests/astro/keyless.test.ts new file mode 100644 index 00000000000..85cd4e0c17b --- /dev/null +++ b/integration/tests/astro/keyless.test.ts @@ -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(); + }); +}); diff --git a/packages/astro/package.json b/packages/astro/package.json index 24a5fd9641b..74e6c4422b8 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -96,7 +96,8 @@ }, "devDependencies": { "@clerk/ui": "workspace:^", - "astro": "^5.15.9" + "astro": "^5.15.9", + "vite": "^7.1.0" }, "peerDependencies": { "astro": "^4.15.0 || ^5.0.0" diff --git a/packages/astro/src/env.d.ts b/packages/astro/src/env.d.ts index 1240941e1e2..e1778007d82 100644 --- a/packages/astro/src/env.d.ts +++ b/packages/astro/src/env.d.ts @@ -20,6 +20,7 @@ interface InternalEnv { readonly PUBLIC_CLERK_SIGN_UP_URL?: string; readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string; readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string; + readonly PUBLIC_CLERK_KEYLESS_DISABLED?: string; } interface ImportMeta { @@ -29,6 +30,9 @@ interface ImportMeta { declare namespace App { interface Locals { runtime: { env: InternalEnv }; + keylessClaimUrl?: string; + keylessApiKeysUrl?: string; + keylessPublishableKey?: string; } } diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index fc19a8371c3..c213e9653b8 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -31,12 +31,19 @@ function createIntegration() logger.error('Missing adapter, please update your Astro config to use one.'); } + const isDev = command === 'dev'; + + // Read keys from process.env for vite.define injection + // Note: Keyless mode is now handled by middleware per-request, not here + const envPublishableKey = process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; + const envSecretKey = process.env.CLERK_SECRET_KEY; + const internalParams: ClerkOptions = { ...params, sdkMetadata: { version: packageVersion, name: packageName, - environment: command === 'dev' ? 'development' : 'production', + environment: isDev ? 'development' : 'production', }, }; @@ -58,6 +65,9 @@ function createIntegration() ...buildEnvVarFromOption(clerkJSUrl, 'PUBLIC_CLERK_JS_URL'), ...buildEnvVarFromOption(clerkJSVersion, 'PUBLIC_CLERK_JS_VERSION'), ...buildEnvVarFromOption(prefetchUI === false ? 'false' : undefined, 'PUBLIC_CLERK_PREFETCH_UI'), + ...buildEnvVarFromOption(envPublishableKey, 'PUBLIC_CLERK_PUBLISHABLE_KEY'), + ...buildEnvVarFromOption(envSecretKey, 'CLERK_SECRET_KEY'), + // Keyless URLs are now handled by middleware, not vite.define }, ssr: { @@ -157,7 +167,7 @@ function createIntegration() function createClerkEnvSchema() { return { - PUBLIC_CLERK_PUBLISHABLE_KEY: envField.string({ context: 'client', access: 'public' }), + PUBLIC_CLERK_PUBLISHABLE_KEY: envField.string({ context: 'client', access: 'public', optional: true }), PUBLIC_CLERK_SIGN_IN_URL: envField.string({ context: 'client', access: 'public', optional: true }), PUBLIC_CLERK_SIGN_UP_URL: envField.string({ context: 'client', access: 'public', optional: true }), PUBLIC_CLERK_IS_SATELLITE: envField.boolean({ context: 'client', access: 'public', optional: true }), @@ -169,7 +179,9 @@ function createClerkEnvSchema() { PUBLIC_CLERK_UI_URL: envField.string({ context: 'client', access: 'public', optional: true, url: true }), PUBLIC_CLERK_TELEMETRY_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }), PUBLIC_CLERK_TELEMETRY_DEBUG: envField.boolean({ context: 'client', access: 'public', optional: true }), - CLERK_SECRET_KEY: envField.string({ context: 'server', access: 'secret' }), + // Note: KEYLESS_CLAIM_URL and KEYLESS_API_KEYS_URL are dynamically resolved by middleware, not user-configurable + PUBLIC_CLERK_KEYLESS_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }), + CLERK_SECRET_KEY: envField.string({ context: 'server', access: 'secret', optional: true }), CLERK_MACHINE_SECRET_KEY: envField.string({ context: 'server', access: 'secret', optional: true }), CLERK_JWT_KEY: envField.string({ context: 'server', access: 'secret', optional: true }), }; diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts index 675e405382c..0eb4a71b582 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -54,12 +54,17 @@ async function createClerkInstanceInternal(options?: AstroC $clerk.set(clerkJSInstance); } + const keylessClaimUrl = (options as any)?.__internal_keylessClaimUrl; + const keylessApiKeysUrl = (options as any)?.__internal_keylessApiKeysUrl; + const clerkOptions = { routerPush: createNavigationHandler(window.history.pushState.bind(window.history)), routerReplace: createNavigationHandler(window.history.replaceState.bind(window.history)), ...options, // Pass the clerk-ui constructor promise to clerk.load() clerkUICtor, + ...(keylessClaimUrl && { __internal_keyless_claimKeylessApplicationUrl: keylessClaimUrl }), + ...(keylessApiKeysUrl && { __internal_keyless_copyInstanceKeysUrl: keylessApiKeysUrl }), } as unknown as ClerkOptions; initOptions = clerkOptions; diff --git a/packages/astro/src/internal/merge-env-vars-with-params.ts b/packages/astro/src/internal/merge-env-vars-with-params.ts index 1cac532cc6f..edc4b360f2b 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -56,6 +56,10 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish disabled: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DISABLED), debug: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DEBUG), }, + // Read from params (server-injected via __CLERK_ASTRO_SAFE_VARS__) + // These are dynamically resolved by middleware, not from env vars + __internal_keylessClaimUrl: (params as any)?.keylessClaimUrl, + __internal_keylessApiKeysUrl: (params as any)?.keylessApiKeysUrl, ...rest, }; }; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index fa0500f2197..72d7792b175 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -24,10 +24,12 @@ import type { APIContext } from 'astro'; 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 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { 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 + } + } + 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 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { 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. diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts index 9d17c86321d..2c679edc59e 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -21,11 +21,14 @@ function getContextEnvVar(envVarName: keyof InternalEnv, contextOrLocals: Contex * @internal */ function getSafeEnv(context: ContextOrLocals) { + const locals = 'locals' in context ? context.locals : context; + return { domain: getContextEnvVar('PUBLIC_CLERK_DOMAIN', context), isSatellite: getContextEnvVar('PUBLIC_CLERK_IS_SATELLITE', context) === 'true', proxyUrl: getContextEnvVar('PUBLIC_CLERK_PROXY_URL', context), - pk: getContextEnvVar('PUBLIC_CLERK_PUBLISHABLE_KEY', context), + // Use keyless publishable key if available, otherwise read from env + pk: locals.keylessPublishableKey || getContextEnvVar('PUBLIC_CLERK_PUBLISHABLE_KEY', context), sk: getContextEnvVar('CLERK_SECRET_KEY', context), machineSecretKey: getContextEnvVar('CLERK_MACHINE_SECRET_KEY', context), signInUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_IN_URL', context), @@ -38,6 +41,9 @@ function getSafeEnv(context: ContextOrLocals) { apiUrl: getContextEnvVar('CLERK_API_URL', context), telemetryDisabled: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DISABLED', context)), telemetryDebug: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DEBUG', context)), + // Read from locals (set by middleware) instead of env vars + keylessClaimUrl: locals.keylessClaimUrl, + keylessApiKeysUrl: locals.keylessApiKeysUrl, }; } @@ -49,12 +55,19 @@ function getSafeEnv(context: ContextOrLocals) { * This is a way to get around it. */ function getClientSafeEnv(context: ContextOrLocals) { + const locals = ('locals' in context ? context.locals : context) as any; + return { domain: getContextEnvVar('PUBLIC_CLERK_DOMAIN', context), isSatellite: getContextEnvVar('PUBLIC_CLERK_IS_SATELLITE', context) === 'true', proxyUrl: getContextEnvVar('PUBLIC_CLERK_PROXY_URL', context), signInUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_IN_URL', context), signUpUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_UP_URL', context), + // In keyless mode, pass the resolved publishable key to client + publishableKey: locals.keylessPublishableKey || getContextEnvVar('PUBLIC_CLERK_PUBLISHABLE_KEY', context), + // Read from locals (set by middleware) instead of env vars + keylessClaimUrl: locals.keylessClaimUrl, + keylessApiKeysUrl: locals.keylessApiKeysUrl, }; } diff --git a/packages/astro/src/server/keyless/file-storage.ts b/packages/astro/src/server/keyless/file-storage.ts new file mode 100644 index 00000000000..0df8973418d --- /dev/null +++ b/packages/astro/src/server/keyless/file-storage.ts @@ -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 { + 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', + }); +} diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts new file mode 100644 index 00000000000..af2035bb94b --- /dev/null +++ b/packages/astro/src/server/keyless/index.ts @@ -0,0 +1,50 @@ +import { createClerkClient } from '@clerk/backend'; +import { createKeylessService } from '@clerk/shared/keyless'; + +import { createFileStorage } from './file-storage.js'; + +let keylessServiceInstance: ReturnType | null = null; + +export async function keyless() { + if (!keylessServiceInstance) { + const storage = await createFileStorage(); + + keylessServiceInstance = createKeylessService({ + storage, + api: { + async createAccountlessApplication(requestHeaders?: Headers) { + try { + const client = createClerkClient({ + secretKey: process.env.CLERK_SECRET_KEY, + publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, + apiUrl: process.env.CLERK_API_URL, + }); + return await client.__experimental_accountlessApplications.createAccountlessApplication({ + requestHeaders, + }); + } catch { + return null; + } + }, + async completeOnboarding(requestHeaders?: Headers) { + try { + const client = createClerkClient({ + secretKey: process.env.CLERK_SECRET_KEY, + publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, + apiUrl: process.env.CLERK_API_URL, + }); + return await client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + requestHeaders, + }); + } catch { + return null; + } + }, + }, + framework: 'astro', + frameworkVersion: PACKAGE_VERSION, + }); + } + + return keylessServiceInstance; +} diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts new file mode 100644 index 00000000000..92ac8b888c1 --- /dev/null +++ b/packages/astro/src/server/keyless/utils.ts @@ -0,0 +1,76 @@ +import type { AccountlessApplication } from '@clerk/shared/keyless'; +import { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } from '@clerk/shared/keyless'; + +import { canUseKeyless } from '../../utils/feature-flags'; +import { keyless } from './index'; + +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. + */ +export async function resolveKeysWithKeylessFallback( + configuredPublishableKey: string | undefined, + configuredSecretKey: string | undefined, +): Promise { + let publishableKey = configuredPublishableKey; + let secretKey = configuredSecretKey; + let claimUrl: string | undefined; + let apiKeysUrl: string | undefined; + + if (!canUseKeyless) { + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; + } + + // If both keys are explicitly configured, skip all keyless logic + if (publishableKey && secretKey) { + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; + } + + const keylessService = await keyless(); + const locallyStoredKeys = keylessService.readKeys(); + + const runningWithClaimedKeys = + Boolean(configuredPublishableKey) && configuredPublishableKey === locallyStoredKeys?.publishableKey; + + if (runningWithClaimedKeys && locallyStoredKeys) { + try { + await clerkDevelopmentCache?.run(() => keylessService.completeOnboarding(), { + cacheKey: `${locallyStoredKeys.publishableKey}_complete`, + onSuccessStale: 24 * 60 * 60 * 1000, + }); + } catch { + // noop + } + + clerkDevelopmentCache?.log({ + cacheKey: `${locallyStoredKeys.publishableKey}_claimed`, + msg: createConfirmationMessage(), + }); + + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; + } + + 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), + }); + } + } + + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; +} diff --git a/packages/astro/src/utils/feature-flags.ts b/packages/astro/src/utils/feature-flags.ts new file mode 100644 index 00000000000..70856d6609e --- /dev/null +++ b/packages/astro/src/utils/feature-flags.ts @@ -0,0 +1,17 @@ +import { isTruthy } from '@clerk/shared/underscore'; +import { isDevelopmentEnvironment } from '@clerk/shared/utils'; + +function hasFileSystemSupport(): boolean { + if (typeof process === 'undefined' || !process?.versions?.node) { + return false; + } + if (typeof window !== 'undefined') { + return false; + } + return true; +} + +const KEYLESS_DISABLED = + isTruthy(process.env.PUBLIC_CLERK_KEYLESS_DISABLED) || isTruthy(process.env.CLERK_KEYLESS_DISABLED) || false; + +export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED && hasFileSystemSupport(); diff --git a/packages/astro/tsup.config.ts b/packages/astro/tsup.config.ts index ade6b396caa..80849d46a6c 100644 --- a/packages/astro/tsup.config.ts +++ b/packages/astro/tsup.config.ts @@ -26,6 +26,14 @@ export default defineConfig(() => { bundle: true, sourcemap: true, format: ['esm'], - external: ['astro', 'react', 'react-dom', 'node:async_hooks', '#async-local-storage', 'astro:transitions/client'], + external: [ + 'astro', + 'react', + 'react-dom', + 'node:async_hooks', + '#async-local-storage', + 'astro:transitions/client', + 'vite', + ], }; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adb035f97a6..4a28034e8b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -388,6 +388,9 @@ importers: astro: specifier: ^5.15.9 version: 5.17.1(@types/node@22.19.0)(db0@0.3.4)(idb-keyval@6.2.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.1)(terser@5.44.1)(tsx@4.20.6)(typescript@5.8.3)(yaml@2.8.1) + vite: + specifier: ^7.1.0 + version: 7.2.2(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) packages/backend: dependencies: