diff --git a/.gitignore b/.gitignore index b7ab27b789..6fbacabae0 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,9 @@ test-output .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md +# SvelteKit +.svelte-kit + # Gemini local knowledge base files GEMINI.md **/GEMINI.md diff --git a/e2e/oidc-app/src/utils/oidc-app.ts b/e2e/oidc-app/src/utils/oidc-app.ts index 69289580a0..fd1c0fd5c3 100644 --- a/e2e/oidc-app/src/utils/oidc-app.ts +++ b/e2e/oidc-app/src/utils/oidc-app.ts @@ -88,14 +88,14 @@ export async function oidcApp({ config, urlParams }) { }); document.getElementById('login-redirect').addEventListener('click', async () => { - const authorizeUrl = await oidcClient.authorize.url(); - if (typeof authorizeUrl !== 'string' && 'error' in authorizeUrl) { - console.error('Authorization URL Error:', authorizeUrl); - displayError(authorizeUrl); + const authorizeResult = await oidcClient.authorize.url(); + if ('error' in authorizeResult) { + console.error('Authorization URL Error:', authorizeResult); + displayError(authorizeResult); return; } else { - console.log('Authorization URL:', authorizeUrl); - window.location.assign(authorizeUrl); + console.log('Authorization URL:', authorizeResult.url); + window.location.assign(authorizeResult.url); } }); diff --git a/e2e/svelte-app/package.json b/e2e/svelte-app/package.json new file mode 100644 index 0000000000..295b6fe9a8 --- /dev/null +++ b/e2e/svelte-app/package.json @@ -0,0 +1,26 @@ +{ + "name": "@forgerock/svelte-app", + "version": "1.0.0", + "private": true, + "description": "SvelteKit SSR proof of concept for Journey Client", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@forgerock/journey-client": "workspace:*", + "@forgerock/oidc-client": "workspace:*" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.21.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "catalog:vite" + }, + "nx": { + "tags": ["scope:e2e"] + } +} diff --git a/e2e/svelte-app/src/app.html b/e2e/svelte-app/src/app.html new file mode 100644 index 0000000000..4c87a53dd4 --- /dev/null +++ b/e2e/svelte-app/src/app.html @@ -0,0 +1,12 @@ + + + + + + Journey Client SSR PoC + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/e2e/svelte-app/src/lib/config.ts b/e2e/svelte-app/src/lib/config.ts new file mode 100644 index 0000000000..adb1c91f07 --- /dev/null +++ b/e2e/svelte-app/src/lib/config.ts @@ -0,0 +1,17 @@ +/** + * Server configuration for the SSR proof of concept. + * Points at the AM mock API running on localhost:9443. + */ +export const WELLKNOWN_URL = + 'http://localhost:9443/am/oauth2/realms/root/.well-known/openid-configuration'; + +export const CLIENT_ID = 'SvelteSSRClient'; +export const REDIRECT_URI = 'http://localhost:5174/callback'; +export const SCOPE = 'openid profile'; + +/** No-op storage adapter for server-side usage where browser storage is unavailable. */ +export const noopStorage = { + get: async () => null, + set: async () => {}, + remove: async () => {}, +}; diff --git a/e2e/svelte-app/src/routes/+layout.svelte b/e2e/svelte-app/src/routes/+layout.svelte new file mode 100644 index 0000000000..60c87da46f --- /dev/null +++ b/e2e/svelte-app/src/routes/+layout.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children()} +
+ + diff --git a/e2e/svelte-app/src/routes/+page.server.ts b/e2e/svelte-app/src/routes/+page.server.ts new file mode 100644 index 0000000000..f22f29c470 --- /dev/null +++ b/e2e/svelte-app/src/routes/+page.server.ts @@ -0,0 +1,92 @@ +import { redirect } from '@sveltejs/kit'; +import { journey } from '@forgerock/journey-client'; +import { oidc } from '@forgerock/oidc-client'; +import { WELLKNOWN_URL, CLIENT_ID, REDIRECT_URI, SCOPE, noopStorage } from '$lib/config.js'; +import type { PageServerLoad, Actions } from './$types'; + +/** + * Server-side load function. + * + * Initializes the journey client with noop storage (no sessionStorage on server) + * and calls start() to fetch the first authentication step. The raw step payload + * is serialized and passed to the client for SSR rendering. + */ +export const load: PageServerLoad = async () => { + try { + const client = await journey({ + config: { + serverConfig: { wellknown: WELLKNOWN_URL }, + storage: { type: 'custom', name: 'journey-step', custom: noopStorage }, + }, + }); + + const result = await client.start(); + + if ('payload' in result) { + return { + stepPayload: result.payload, + error: null, + }; + } + + return { + stepPayload: null, + error: 'error' in result ? result : { error: 'unexpected', message: 'Unexpected result' }, + }; + } catch (e) { + return { + stepPayload: null, + error: { + error: 'server_init_failed', + message: e instanceof Error ? e.message : 'Failed to initialize journey client on server', + }, + }; + } +}; + +/** + * Form actions — the authorize action generates a PKCE authorize URL on the server, + * stores the verifier in a cookie, and redirects the browser to the authorize endpoint. + */ +export const actions: Actions = { + authorize: async ({ cookies }) => { + const client = await oidc({ + config: { + serverConfig: { wellknown: WELLKNOWN_URL }, + clientId: CLIENT_ID, + redirectUri: REDIRECT_URI, + scope: SCOPE, + responseType: 'code', + }, + storage: { type: 'custom', name: CLIENT_ID, custom: noopStorage }, + }); + + if (!client || 'error' in client) { + return { error: 'Failed to initialize OIDC client' }; + } + + // Generate authorize URL with PKCE — returns { url, verifier, state } + const result = await client.authorize.url(); + + if ('error' in result) { + return { error: result.error }; + } + + // Store PKCE verifier + state in an httpOnly cookie for the callback route + cookies.set('pkce_verifier', result.verifier, { + path: '/', + httpOnly: true, + sameSite: 'lax', + maxAge: 300, // 5 minutes + }); + cookies.set('pkce_state', result.state, { + path: '/', + httpOnly: true, + sameSite: 'lax', + maxAge: 300, + }); + + // Redirect browser to authorization endpoint + redirect(303, result.url); + }, +}; diff --git a/e2e/svelte-app/src/routes/+page.svelte b/e2e/svelte-app/src/routes/+page.svelte new file mode 100644 index 0000000000..28b99a5582 --- /dev/null +++ b/e2e/svelte-app/src/routes/+page.svelte @@ -0,0 +1,219 @@ + + +

Journey Client SSR PoC

+ +{#if serverError} +
+

Error: {serverError.message}

+

This is expected if the AM mock API is not running on port 9443.

+
+{:else if success} +
+

Journey complete!

+

Authentication succeeded. Now exchange for tokens via server-side PKCE:

+
+ +
+
+{:else if failure} +
+

{failure}

+
+{/if} + +{#if stepPayload?.callbacks} +
+

{stepPayload.header ?? 'Sign In'}

+ {#if stepPayload.description} +

{stepPayload.description}

+ {/if} + + {#each stepPayload.callbacks as cb (cb._id)} + {#if isTextInput(cb)} + + {:else if isPassword(cb)} + + {:else} +

Unsupported callback: {cb.type}

+ {/if} + {/each} + + +
+{:else if !serverError && !success} +

Loading...

+{/if} + + diff --git a/e2e/svelte-app/src/routes/callback/+page.server.ts b/e2e/svelte-app/src/routes/callback/+page.server.ts new file mode 100644 index 0000000000..8957d349d4 --- /dev/null +++ b/e2e/svelte-app/src/routes/callback/+page.server.ts @@ -0,0 +1,105 @@ +import { oidc } from '@forgerock/oidc-client'; +import { WELLKNOWN_URL, CLIENT_ID, REDIRECT_URI, SCOPE, noopStorage } from '$lib/config.js'; +import type { PageServerLoad } from './$types'; + +/** + * Callback route — handles the redirect from the authorization server. + * + * Reads the authorization code and state from the URL, retrieves the PKCE + * verifier from the cookie (set during authorize), and exchanges for tokens + * entirely on the server. The browser never sees the verifier or tokens directly. + */ +export const load: PageServerLoad = async ({ url, cookies }) => { + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const error = url.searchParams.get('error'); + const errorDescription = url.searchParams.get('error_description'); + + if (error) { + return { + tokens: null, + error: { error, message: errorDescription ?? 'Authorization failed' }, + }; + } + + if (!code || !state) { + return { + tokens: null, + error: { error: 'missing_params', message: 'Missing code or state in callback URL' }, + }; + } + + // Retrieve PKCE values from httpOnly cookies + const verifier = cookies.get('pkce_verifier'); + const pkceState = cookies.get('pkce_state'); + + // Clean up cookies + cookies.delete('pkce_verifier', { path: '/' }); + cookies.delete('pkce_state', { path: '/' }); + + if (!verifier || !pkceState) { + return { + tokens: null, + error: { error: 'missing_pkce', message: 'PKCE verifier or state not found in cookies' }, + }; + } + + if (pkceState !== state) { + return { + tokens: null, + error: { error: 'state_mismatch', message: 'State parameter does not match' }, + }; + } + + try { + const client = await oidc({ + config: { + serverConfig: { wellknown: WELLKNOWN_URL }, + clientId: CLIENT_ID, + redirectUri: REDIRECT_URI, + scope: SCOPE, + responseType: 'code', + }, + storage: { type: 'custom', name: CLIENT_ID, custom: noopStorage }, + }); + + if (!client || 'error' in client) { + return { + tokens: null, + error: { error: 'oidc_init_failed', message: 'Failed to initialize OIDC client' }, + }; + } + + // Exchange code for tokens, providing PKCE values directly (no sessionStorage) + const tokens = await client.token.exchange(code, state, { + pkceValues: { verifier, state: pkceState }, + }); + + if ('error' in tokens) { + return { + tokens: null, + error: { + error: tokens.error, + message: 'message' in tokens ? tokens.message : 'Token exchange failed', + }, + }; + } + + return { + tokens: { + accessToken: tokens.accessToken, + idToken: tokens.idToken, + ...(tokens.refreshToken && { refreshToken: tokens.refreshToken }), + }, + error: null, + }; + } catch (e) { + return { + tokens: null, + error: { + error: 'exchange_failed', + message: e instanceof Error ? e.message : 'Token exchange failed', + }, + }; + } +}; diff --git a/e2e/svelte-app/src/routes/callback/+page.svelte b/e2e/svelte-app/src/routes/callback/+page.svelte new file mode 100644 index 0000000000..ddefe9b51f --- /dev/null +++ b/e2e/svelte-app/src/routes/callback/+page.svelte @@ -0,0 +1,63 @@ + + +

OIDC Callback

+ +{#if data.error} +
+

Error

+

{data.error.error}: {data.error.message}

+ Back to login +
+{:else if data.tokens} +
+

Tokens received!

+
+
Access Token
+
{data.tokens.accessToken.slice(0, 20)}...
+
ID Token
+
{data.tokens.idToken.slice(0, 20)}...
+ {#if data.tokens.refreshToken} +
Refresh Token
+
{data.tokens.refreshToken.slice(0, 20)}...
+ {/if} +
+ Back to login +
+{:else} +

Processing...

+{/if} + + diff --git a/e2e/svelte-app/svelte.config.js b/e2e/svelte-app/svelte.config.js new file mode 100644 index 0000000000..2f86665436 --- /dev/null +++ b/e2e/svelte-app/svelte.config.js @@ -0,0 +1,10 @@ +import adapter from '@sveltejs/adapter-auto'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter(), + }, +}; + +export default config; diff --git a/e2e/svelte-app/tsconfig.json b/e2e/svelte-app/tsconfig.json new file mode 100644 index 0000000000..c748ab2058 --- /dev/null +++ b/e2e/svelte-app/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + }, + "references": [ + { + "path": "../../packages/oidc-client" + }, + { + "path": "../../packages/journey-client" + } + ] +} diff --git a/e2e/svelte-app/vite.config.ts b/e2e/svelte-app/vite.config.ts new file mode 100644 index 0000000000..2e920e4aa4 --- /dev/null +++ b/e2e/svelte-app/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], +}); diff --git a/nx.json b/nx.json index d2f5805f60..7a112dd147 100644 --- a/nx.json +++ b/nx.json @@ -131,7 +131,8 @@ "buildDepsTargetName": "vite:build-deps", "watchDepsTargetName": "vite:watch-deps" }, - "include": ["packages/**/**/*", "e2e/**/**/*", "tools/**/**/*"] + "include": ["packages/**/**/*", "e2e/**/**/*", "tools/**/**/*"], + "exclude": ["e2e/svelte-app/**/*"] }, { "plugin": "@nx/js/typescript", @@ -151,7 +152,8 @@ "options": { "testTargetName": "nxTest" }, - "include": ["packages/**/**/*", "e2e/**/**/*", "tools/**/**/*"] + "include": ["packages/**/**/*", "e2e/**/**/*", "tools/**/**/*"], + "exclude": ["e2e/svelte-app/**/*"] } ], "parallel": 1, diff --git a/packages/davinci-client/src/lib/davinci.api.ts b/packages/davinci-client/src/lib/davinci.api.ts index 8f47cc2f84..003d95184d 100644 --- a/packages/davinci-client/src/lib/davinci.api.ts +++ b/packages/davinci-client/src/lib/davinci.api.ts @@ -262,7 +262,7 @@ export const davinciApi = createApi({ } try { - const authorizeUrl = await createAuthorizeUrl(authorizeEndpoint, { + const authorizeResult = await createAuthorizeUrl(authorizeEndpoint, { clientId: state?.config?.clientId, login: 'redirect', // TODO: improve this in SDK to be more semantic redirectUri: state?.config?.redirectUri, @@ -270,7 +270,7 @@ export const davinciApi = createApi({ responseMode: 'pi.flow', scope: state?.config?.scope, }); - const url = new URL(authorizeUrl); + const url = new URL(authorizeResult.url); const existingParams = url.searchParams; if (options?.query) { diff --git a/packages/journey-client/src/index.ts b/packages/journey-client/src/index.ts index 04d5c83093..33e2481c5c 100644 --- a/packages/journey-client/src/index.ts +++ b/packages/journey-client/src/index.ts @@ -6,6 +6,7 @@ */ export * from './lib/client.store.js'; +export { createJourneyObject } from './lib/journey.utils.js'; // Re-export types from internal packages that consumers need export { callbackType } from '@forgerock/sdk-types'; diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index cbd85480f2..f68cebb748 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -148,10 +148,9 @@ export async function journey({ throw new Error(message); } - const stepStorage = createStorage<{ step: Step }>({ - type: 'sessionStorage', - name: 'journey-step', - }); + const stepStorage = createStorage<{ step: Step }>( + config.storage ?? { type: 'sessionStorage', name: 'journey-step' }, + ); const self: JourneyClient = { start: async (options?: StartParam) => { @@ -200,6 +199,11 @@ export async function journey({ if (isGenericError(err)) { log.warn('Failed to persist step before redirect', err); } + if (typeof window === 'undefined') { + throw new Error( + 'redirect() requires a browser environment. Extract the redirect URL from the RedirectCallback for server-side redirection.', + ); + } window.location.assign(redirectUrl); }, diff --git a/packages/journey-client/src/lib/config.types.ts b/packages/journey-client/src/lib/config.types.ts index 25eaeaa888..5f1e664371 100644 --- a/packages/journey-client/src/lib/config.types.ts +++ b/packages/journey-client/src/lib/config.types.ts @@ -6,6 +6,7 @@ */ import type { AsyncLegacyConfigOptions, GenericError } from '@forgerock/sdk-types'; +import type { StorageConfig } from '@forgerock/storage'; import type { ResolvedServerConfig } from './wellknown.utils.js'; /** @@ -40,6 +41,8 @@ export interface JourneyServerConfig { */ export interface JourneyClientConfig extends AsyncLegacyConfigOptions { serverConfig: JourneyServerConfig; + /** Storage configuration for step persistence during redirects. Defaults to sessionStorage in browsers. */ + storage?: StorageConfig; } /** diff --git a/packages/oidc-client/src/lib/authorize.request.utils.ts b/packages/oidc-client/src/lib/authorize.request.utils.ts index 96557e22ed..851292ed6e 100644 --- a/packages/oidc-client/src/lib/authorize.request.utils.ts +++ b/packages/oidc-client/src/lib/authorize.request.utils.ts @@ -4,7 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { createAuthorizeUrl } from '@forgerock/sdk-oidc'; +import { createAuthorizeUrl, type AuthorizeUrlResult } from '@forgerock/sdk-oidc'; import { Micro } from 'effect'; import type { WellknownResponse, GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; @@ -49,7 +49,6 @@ export function buildAuthorizeOptionsµ( * @description Creates an error response with new Authorize URL for the authorization request. * @param { error: string; error_description: string } res - The error response from the authorization request. * @param {WellknownResponse} wellknown- The well-known configuration for the OIDC server. - * @param { OidcConfig } config- The OIDC client configuration. * @param { GetAuthorizationUrlOptions } options- Optional parameters for the authorization request. * @returns { Micro.Micro } */ @@ -75,12 +74,12 @@ export function createAuthorizeErrorµ( } as const; }, }).pipe( - Micro.flatMap((url) => { + Micro.flatMap((result) => { return Micro.fail({ error: res.error, error_description: res.error_description, type: 'auth_error', - redirectUrl: url, + redirectUrl: result.url, } as const); }), ); @@ -89,6 +88,7 @@ export function createAuthorizeErrorµ( /** * @function createAuthorizeUrlµ * @description Creates an authorization URL and related options/config for the Authorize request. + * Stores PKCE values in sessionStorage for the background authorize flow. * @param {string} path - The path to the authorization endpoint. * @param { GetAuthorizationUrlOptions } options - Optional parameters for the authorization request. * @returns { Micro.Micro<[string, GetAuthorizationUrlOptions], AuthorizationError, never> } @@ -98,13 +98,18 @@ export function createAuthorizeUrlµ( options: GetAuthorizationUrlOptions, ): Micro.Micro<[string, GetAuthorizationUrlOptions], AuthorizationError, never> { return Micro.tryPromise({ - try: async () => [ - await createAuthorizeUrl(path, { + try: async (): Promise<[string, GetAuthorizationUrlOptions]> => { + const result = await createAuthorizeUrl(path, { ...options, prompt: 'none', - }), - options, - ], + }); + + // For the background flow, persist PKCE values in sessionStorage + // so the token exchange can retrieve them after the iframe redirect. + storePkceValues(options.clientId, result, options); + + return [result.url, options]; + }, catch: (error) => { let message = 'Error creating authorization URL'; if (error instanceof Error) { @@ -119,6 +124,26 @@ export function createAuthorizeUrlµ( }); } +/** + * Store PKCE values in sessionStorage for the background authorize flow. + * This is the browser-only path — server-side callers handle storage themselves. + */ +function storePkceValues( + clientId: string, + result: AuthorizeUrlResult, + options: GetAuthorizationUrlOptions, +): void { + const storageKey = `FR-SDK-authflow-${clientId}`; + globalThis.sessionStorage.setItem( + storageKey, + JSON.stringify({ + ...options, + state: result.state, + verifier: result.verifier, + }), + ); +} + export function handleResponseµ( response: AuthorizationSuccess | AuthorizationError, wellknown: WellknownResponse, diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index da6c3de99c..68d90b381b 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ import { logger as loggerFn } from '@forgerock/sdk-logger'; -import { createAuthorizeUrl } from '@forgerock/sdk-oidc'; +import { createAuthorizeUrl, type AuthorizeUrlResult } from '@forgerock/sdk-oidc'; import { createStorage } from '@forgerock/storage'; import { Micro } from 'effect'; import { exitIsFail, exitIsSuccess } from 'effect/Micro'; @@ -29,6 +29,7 @@ import type { RevokeSuccessResult, UserInfoResponse, } from './client.types.js'; +import type { PkceValues } from './exchange.utils.js'; import type { OauthTokens, OidcConfig } from './config.types.js'; import type { AuthorizationError, AuthorizationSuccess } from './authorize.request.types.js'; import type { TokenExchangeErrorResponse } from './exchange.types.js'; @@ -105,7 +106,9 @@ export async function oidc({ * @param {GetAuthorizationUrlOptions} options - Optional parameters to customize the authorization URL. * @returns {Promise} - Returns a promise that resolves to the authorization URL or an error. */ - url: async (options?: GetAuthorizationUrlOptions): Promise => { + url: async ( + options?: GetAuthorizationUrlOptions, + ): Promise => { const optionsWithDefaults = { clientId: config.clientId, redirectUri: config.redirectUri, @@ -179,7 +182,7 @@ export async function oidc({ exchange: async ( code: string, state: string, - options?: Partial, + options?: Partial & { pkceValues?: PkceValues }, ): Promise => { const storeState = store.getState(); const wellknown = wellknownSelector(wellknownUrl, storeState); @@ -199,6 +202,7 @@ export async function oidc({ endpoint: wellknown.token_endpoint, store, options, + pkceValues: options?.pkceValues, }).pipe( Micro.tap(async (tokens) => { await storageClient.set(tokens); diff --git a/packages/oidc-client/src/lib/exchange.request.ts b/packages/oidc-client/src/lib/exchange.request.ts index 4fb4e57110..813ae77a88 100644 --- a/packages/oidc-client/src/lib/exchange.request.ts +++ b/packages/oidc-client/src/lib/exchange.request.ts @@ -8,7 +8,12 @@ import { Micro } from 'effect'; import { logger } from '@forgerock/sdk-logger'; -import { createValuesµ, handleTokenResponseµ, validateValuesµ } from './exchange.utils.js'; +import { + createValuesµ, + handleTokenResponseµ, + validateValuesµ, + type PkceValues, +} from './exchange.utils.js'; import { oidcApi } from './oidc.api.js'; import type { ClientStore } from './client.types.js'; @@ -24,6 +29,8 @@ interface BuildTokenExchangeµParams { state: string; store: ClientStore; options?: Partial; + /** Provide PKCE values directly (SSR) instead of reading from sessionStorage. */ + pkceValues?: PkceValues; } export function buildTokenExchangeµ({ @@ -34,8 +41,9 @@ export function buildTokenExchangeµ({ state, store, options, + pkceValues, }: BuildTokenExchangeµParams): Micro.Micro { - return createValuesµ(code, config, state, endpoint, options).pipe( + return createValuesµ(code, config, state, endpoint, options, pkceValues).pipe( Micro.flatMap((options) => validateValuesµ(options)), Micro.tap((options) => log.debug('Token exchange values created', options)), Micro.tapError((options) => diff --git a/packages/oidc-client/src/lib/exchange.utils.ts b/packages/oidc-client/src/lib/exchange.utils.ts index b3314a6ab4..9ca70d1a36 100644 --- a/packages/oidc-client/src/lib/exchange.utils.ts +++ b/packages/oidc-client/src/lib/exchange.utils.ts @@ -16,15 +16,32 @@ import type { TokenExchangeResponse, TokenRequestOptions } from './exchange.type import type { TokenExchangeErrorResponse } from './exchange.types.js'; import type { OidcConfig } from './config.types.js'; +/** Options for providing PKCE values directly instead of reading sessionStorage. */ +export interface PkceValues { + verifier: string; + state: string; +} + export function createValuesµ( code: string, config: OidcConfig, state: string, endpoint: string, options?: Partial, + pkceValues?: PkceValues, ) { return Micro.sync(() => { - const storedValues = getStoredAuthUrlValues(config.clientId, options?.prefix); + // If PKCE values are provided directly, use them (SSR path). + // Otherwise, fall back to reading from sessionStorage (browser path). + const storedValues: GetAuthorizationUrlOptions = pkceValues + ? { + ...pkceValues, + clientId: config.clientId, + redirectUri: config.redirectUri, + responseType: 'code', + scope: config.scope || 'openid', + } + : getStoredAuthUrlValues(config.clientId, options?.prefix); return { code, diff --git a/packages/oidc-client/src/types.ts b/packages/oidc-client/src/types.ts index 10c8b28405..45d5d4f258 100644 --- a/packages/oidc-client/src/types.ts +++ b/packages/oidc-client/src/types.ts @@ -16,3 +16,5 @@ export type { export type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; export type { CustomLogger, LogLevel } from '@forgerock/sdk-logger'; export type { StorageConfig } from '@forgerock/storage'; +export type { AuthorizeUrlResult } from '@forgerock/sdk-oidc'; +export type { PkceValues } from './lib/exchange.utils.js'; diff --git a/packages/sdk-effects/oidc/src/lib/authorize.effects.ts b/packages/sdk-effects/oidc/src/lib/authorize.effects.ts index 4881f2dd34..a2f794697f 100644 --- a/packages/sdk-effects/oidc/src/lib/authorize.effects.ts +++ b/packages/sdk-effects/oidc/src/lib/authorize.effects.ts @@ -10,26 +10,34 @@ */ import { createChallenge } from '@forgerock/sdk-utilities'; -import { generateAndStoreAuthUrlValues } from './state-pkce.effects.js'; +import { generateAuthUrlValues } from './state-pkce.effects.js'; import type { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; +/** Result of creating an authorization URL with PKCE. */ +export interface AuthorizeUrlResult { + /** The fully-formed authorization URL to redirect to. */ + url: string; + /** The PKCE verifier — caller must persist this for token exchange. */ + verifier: string; + /** The state parameter — caller must persist this for CSRF validation. */ + state: string; +} + /** - * @function createAuthorizeUrl - Create authorization URL for initial call to DaVinci - * @param baseUrl {string} - * @param options {GetAuthorizationUrlOptions} - * @returns {Promise} - the authorization URL + * Creates an authorization URL with PKCE parameters. + * + * Returns the URL along with the verifier and state values. The caller is + * responsible for persisting verifier/state (in sessionStorage, a cookie, + * a server-side session, etc.) so they can be provided during token exchange. */ export async function createAuthorizeUrl( authorizeUrl: string, options: GetAuthorizationUrlOptions, -): Promise { - /** - * Generate state and verifier for PKCE - */ +): Promise { const baseUrl = new URL(authorizeUrl).origin; - const [authorizeUrlOptions, storeOptions] = generateAndStoreAuthUrlValues({ + const authorizeUrlOptions = generateAuthUrlValues({ clientId: options.clientId, serverConfig: { baseUrl }, responseType: options.responseType, @@ -54,7 +62,9 @@ export async function createAuthorizeUrl( const url = new URL(`${authorizeUrl}?${requestParams.toString()}`); - storeOptions(); - - return url.toString(); + return { + url: url.toString(), + verifier: authorizeUrlOptions.verifier, + state: authorizeUrlOptions.state, + }; } diff --git a/packages/sdk-effects/oidc/src/lib/authorize.test.ts b/packages/sdk-effects/oidc/src/lib/authorize.test.ts index 6dc03c43a7..2695ec4892 100644 --- a/packages/sdk-effects/oidc/src/lib/authorize.test.ts +++ b/packages/sdk-effects/oidc/src/lib/authorize.test.ts @@ -8,7 +8,6 @@ import type { GenerateAndStoreAuthUrlValues } from '@forgerock/sdk-types'; import { describe, expect, it, beforeEach } from 'vitest'; import { createAuthorizeUrl } from './authorize.effects.js'; -import { getStorageKey } from './state-pkce.effects.js'; const mockSessionStorage = (() => { let store: { [key: string]: string } = {}; @@ -45,8 +44,8 @@ describe('createAuthorizeUrl', () => { const baseUrl = 'https://auth.example.com/authorize'; it('should create a valid authorization URL with all required parameters', async () => { - const url = await createAuthorizeUrl(baseUrl, mockOptions); - const parsedUrl = new URL(url); + const result = await createAuthorizeUrl(baseUrl, mockOptions); + const parsedUrl = new URL(result.url); // Check the base URL expect(parsedUrl.origin + parsedUrl.pathname).toBe(baseUrl); @@ -67,6 +66,21 @@ describe('createAuthorizeUrl', () => { expect(params.code_challenge).toBeDefined(); }); + it('should return verifier and state alongside the URL', async () => { + const result = await createAuthorizeUrl(baseUrl, mockOptions); + + expect(result.verifier).toBeDefined(); + expect(result.state).toBeDefined(); + expect(typeof result.verifier).toBe('string'); + expect(typeof result.state).toBe('string'); + expect(result.verifier.length).toBeGreaterThan(0); + expect(result.state.length).toBeGreaterThan(0); + + // State in URL should match the returned state + const parsedUrl = new URL(result.url); + expect(parsedUrl.searchParams.get('state')).toBe(result.state); + }); + it('should include optional parameters when provided', async () => { const prompt = 'login'; const responseMode = 'pi.flow'; @@ -76,8 +90,8 @@ describe('createAuthorizeUrl', () => { responseMode, }; - const url = await createAuthorizeUrl(baseUrl, optionsWithOptionals); - const params = new URL(url).searchParams; + const result = await createAuthorizeUrl(baseUrl, optionsWithOptionals); + const params = new URL(result.url).searchParams; expect(params.get('prompt')).toBe(prompt); expect(params.get('response_mode')).toBe(responseMode); @@ -94,8 +108,8 @@ describe('createAuthorizeUrl', () => { }, }; - const url = await createAuthorizeUrl(baseUrl, optionsWithOptionals); - const params = new URL(url).searchParams; + const result = await createAuthorizeUrl(baseUrl, optionsWithOptionals); + const params = new URL(result.url).searchParams; expect(params.get('queryA')).toBe(queryA); expect(params.get('queryB')).toBe(queryB); @@ -110,8 +124,8 @@ describe('createAuthorizeUrl', () => { }, }; - const url = await createAuthorizeUrl(baseUrl, optionsWithConflict); - const params = new URL(url).searchParams; + const result = await createAuthorizeUrl(baseUrl, optionsWithConflict); + const params = new URL(result.url).searchParams; // Standard param should override query param expect(params.get('client_id')).toBe(mockOptions.clientId); @@ -119,20 +133,9 @@ describe('createAuthorizeUrl', () => { expect(params.get('custom_param')).toBe('value'); }); - it('should store the authorize options in session storage', async () => { + it('should NOT store values in session storage (caller responsibility)', async () => { await createAuthorizeUrl(baseUrl, mockOptions); - const storageKey = getStorageKey(mockOptions.clientId); - const storedData = sessionStorage.getItem(storageKey); - - const parsedOptions = JSON.parse(storedData as string); - const serverUrl = new URL(baseUrl).origin; - - expect(storedData).toBeDefined(); - expect(parsedOptions).toMatchObject({ - ...mockOptions, - serverConfig: { baseUrl: serverUrl }, - }); - expect(parsedOptions).toHaveProperty('state'); - expect(parsedOptions).toHaveProperty('verifier'); + // createAuthorizeUrl no longer writes to sessionStorage — that's the caller's job + expect(sessionStorage.getItem(`FR-SDK-authflow-${mockOptions.clientId}`)).toBeNull(); }); }); diff --git a/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts b/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts index f24df9fdfb..de35bb5a80 100644 --- a/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts +++ b/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts @@ -16,44 +16,55 @@ export function getStorageKey(clientId: string, prefix?: string) { return `${prefix || 'FR-SDK'}-authflow-${clientId}`; } +/** The PKCE + state values generated for an authorization request. */ +export interface AuthUrlValues extends GetAuthorizationUrlOptions { + state: string; + verifier: string; +} + /** - * Generate and store PKCE values for later use - * @param { string } storageKey - Key to store authorization options in sessionStorage - * @param {GenerateAndStoreAuthUrlValues} options - Options for generating PKCE values - * @returns { state: string, verifier: string, GetAuthorizationUrlOptions } + * Pure PKCE generation — no storage side effects. + * Returns the authorize URL options with generated state and verifier. + * The caller is responsible for persisting these values for token exchange. */ - -export function generateAndStoreAuthUrlValues( - options: GenerateAndStoreAuthUrlValues, -): readonly [GetAuthorizationUrlOptions & { state: string; verifier: string }, () => void] { +export function generateAuthUrlValues(options: GenerateAndStoreAuthUrlValues): AuthUrlValues { const verifier = createVerifier(); const state = createState(); - const storageKey = getStorageKey(options.clientId, options.prefix); - const authorizeUrlOptions = { + return { ...options, state, verifier, }; +} + +/** + * @deprecated Use `generateAuthUrlValues` and handle storage yourself. + * Generate PKCE values and return a closure to store them in sessionStorage. + */ +export function generateAndStoreAuthUrlValues( + options: GenerateAndStoreAuthUrlValues, +): readonly [AuthUrlValues, () => void] { + const authorizeUrlOptions = generateAuthUrlValues(options); + const storageKey = getStorageKey(options.clientId, options.prefix); return [ authorizeUrlOptions, - () => sessionStorage.setItem(storageKey, JSON.stringify(authorizeUrlOptions)), + () => globalThis.sessionStorage.setItem(storageKey, JSON.stringify(authorizeUrlOptions)), ] as const; } /** - * @function getStoredAuthUrlValues - Retrieve stored authorization options from sessionStorage - * @param { string } storageKey - Key to retrieve stored values from sessionStorage - * @returns { GetAuthorizationUrlOptions } + * @deprecated Use caller-provided stored values instead. + * Retrieve stored authorization options from sessionStorage. */ export function getStoredAuthUrlValues( clientId: string, prefix?: string, ): GetAuthorizationUrlOptions { const storageKey = getStorageKey(clientId, prefix); - const storedString = sessionStorage.getItem(storageKey); - sessionStorage.removeItem(storageKey); + const storedString = globalThis.sessionStorage.getItem(storageKey); + globalThis.sessionStorage.removeItem(storageKey); try { return JSON.parse(storedString as string); diff --git a/packages/sdk-effects/storage/src/lib/storage.effects.ts b/packages/sdk-effects/storage/src/lib/storage.effects.ts index 64706f07f4..0e5b7e78ce 100644 --- a/packages/sdk-effects/storage/src/lib/storage.effects.ts +++ b/packages/sdk-effects/storage/src/lib/storage.effects.ts @@ -27,6 +27,21 @@ export interface CustomStorageConfig { custom: CustomStorageObject; } +/** + * Lazily access browser storage globals. Using `globalThis` property access + * instead of bare `sessionStorage`/`localStorage` avoids ReferenceError + * in Node.js/SSR environments — property access returns `undefined` rather than throwing. + */ +function getBrowserStorage(type: 'localStorage' | 'sessionStorage'): Storage { + const storage = type === 'localStorage' ? globalThis.localStorage : globalThis.sessionStorage; + if (!storage) { + throw new Error( + `${type} is not available in this environment. Use type: 'custom' for server-side usage.`, + ); + } + return storage; +} + function createStorageError( storeType: 'localStorage' | 'sessionStorage' | 'custom', action: 'Storing' | 'Retrieving' | 'Removing' | 'Parsing', @@ -56,10 +71,6 @@ function createStorageError( export function createStorage(config: StorageConfig): StorageClient { const { type: storeType, prefix = 'pic', name } = config; const key = `${prefix}-${name}`; - const storageTypes = { - sessionStorage, - localStorage, - }; if (storeType === 'custom' && !('custom' in config)) { throw new Error('Custom storage configuration must include a custom storage object'); @@ -89,7 +100,7 @@ export function createStorage(config: StorageConfig): StorageClient(config: StorageConfig): StorageClient(config: StorageConfig): StorageClient=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 || ^6.0.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + typescript: + optional: true + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1': + resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^5.0.0 + svelte: ^5.0.0 + vite: ^6.0.0 + + '@sveltejs/vite-plugin-svelte@5.1.1': + resolution: {integrity: sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.0.0 + '@swc-node/core@1.14.1': resolution: {integrity: sha512-jrt5GUaZUU6cmMS+WTJEvGvaB6j1YNKPHPzC2PUi2BjaFbtxURHj6641Az6xN7b665hNniAIdvjxWcRml5yCnw==} engines: {node: '>= 10'} @@ -3059,6 +3125,9 @@ packages: '@types/conventional-commits-parser@5.0.2': resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -3137,6 +3206,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -3685,6 +3757,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -3771,6 +3847,10 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + b4a@1.7.3: resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} peerDependencies: @@ -4084,6 +4164,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -4215,6 +4299,10 @@ packages: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -4528,6 +4616,9 @@ packages: peerDependencies: typescript: ^5.4.4 + devalue@5.7.1: + resolution: {integrity: sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==} + dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} @@ -4825,6 +4916,9 @@ packages: jiti: optional: true + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4842,6 +4936,9 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} + esrap@2.2.4: + resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -5665,6 +5762,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -6017,6 +6117,10 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -6052,6 +6156,9 @@ packages: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -7218,6 +7325,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -7519,6 +7629,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte@5.55.2: + resolution: {integrity: sha512-z41M/hi0ZPTzrwVKLvB/R1/Oo08gL1uIib8HZ+FncqxxtY9MLb01emg2fqk+WLZ/lNrrtNDFh7BZLDxAHvMgLw==} + engines: {node: '>=18'} + swc-loader@0.2.6: resolution: {integrity: sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg==} peerDependencies: @@ -8024,6 +8138,14 @@ packages: yaml: optional: true + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + vitest-canvas-mock@1.1.3: resolution: {integrity: sha512-zlKJR776Qgd+bcACPh0Pq5MG3xWq+CdkACKY/wX4Jyija0BSz8LH3aCCgwFKYFwtm565+050YFEGG9Ki0gE/Hw==} peerDependencies: @@ -8249,6 +8371,9 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + zod-package-json@1.2.0: resolution: {integrity: sha512-tamtgPM3MkP+obfO2dLr/G+nYoYkpJKmuHdYEy6IXRKfLybruoJ5NUj0lM0LxwOpC9PpoGLbll1ecoeyj43Wsg==} engines: {node: '>=20'} @@ -10866,6 +10991,57 @@ snapshots: '@standard-schema/utils@0.3.0': {} + '@sveltejs/acorn-typescript@1.0.9(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@sveltejs/adapter-auto@6.1.1(@sveltejs/kit@2.57.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.55.2)(typescript@5.9.3)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)))': + dependencies: + '@sveltejs/kit': 2.57.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.55.2)(typescript@5.9.3)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)) + + '@sveltejs/kit@2.57.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.55.2)(typescript@5.9.3)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@standard-schema/spec': 1.0.0 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.7.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.1.0 + sirv: 3.0.2 + svelte: 5.55.2 + vite: 7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + typescript: 5.9.3 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)) + debug: 4.4.3 + svelte: 5.55.2 + vite: 7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.55.2)(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)) + debug: 4.4.3 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.21 + svelte: 5.55.2 + vite: 7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.3(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)) + transitivePeerDependencies: + - supports-color + '@swc-node/core@1.14.1(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)': dependencies: '@swc/core': 1.11.21(@swc/helpers@0.5.17) @@ -11054,6 +11230,8 @@ snapshots: dependencies: '@types/node': 24.9.2 + '@types/cookie@0.6.0': {} + '@types/deep-eql@4.0.2': {} '@types/eslint-scope@3.7.7': @@ -11140,6 +11318,8 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/trusted-types@2.0.7': {} + '@types/unist@3.0.3': {} '@types/yargs-parser@21.0.3': {} @@ -11896,6 +12076,8 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.1: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -11996,6 +12178,8 @@ snapshots: transitivePeerDependencies: - debug + axobject-query@4.1.0: {} + b4a@1.7.3: {} babel-jest@30.2.0(@babel/core@7.28.5): @@ -12352,6 +12536,8 @@ snapshots: clone@1.0.4: {} + clsx@2.1.1: {} + co@4.6.0: {} collect-v8-coverage@1.0.3: {} @@ -12482,6 +12668,8 @@ snapshots: cookie-signature@1.2.2: {} + cookie@0.6.0: {} + cookie@0.7.1: {} cookie@0.7.2: {} @@ -12766,6 +12954,8 @@ snapshots: transitivePeerDependencies: - supports-color + devalue@5.7.1: {} + dezalgo@1.0.4: dependencies: asap: 2.0.6 @@ -13184,6 +13374,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.2: {} + espree@10.4.0: dependencies: acorn: 8.15.0 @@ -13202,6 +13394,11 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@2.2.4: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@typescript-eslint/types': 8.46.3 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -14158,6 +14355,10 @@ snapshots: is-promise@4.0.0: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -14696,6 +14897,8 @@ snapshots: kind-of@6.0.3: {} + kleur@4.1.5: {} + leven@3.1.0: {} levn@0.4.1: @@ -14739,6 +14942,8 @@ snapshots: loader-runner@4.3.1: {} + locate-character@3.0.0: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -15991,6 +16196,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@3.1.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -16334,6 +16541,25 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte@5.55.2: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) + '@types/estree': 1.0.8 + '@types/trusted-types': 2.0.7 + acorn: 8.15.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.7.1 + esm-env: 1.2.2 + esrap: 2.2.4 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + swc-loader@0.2.6(@swc/core@1.11.21(@swc/helpers@0.5.17))(webpack@5.102.1(@swc/core@1.11.21(@swc/helpers@0.5.17))): dependencies: '@swc/core': 1.11.21(@swc/helpers@0.5.17) @@ -16882,6 +17108,10 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vitefu@1.1.3(vite@7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1)): + optionalDependencies: + vite: 7.3.1(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.1) + vitest-canvas-mock@1.1.3(vitest@3.2.4): dependencies: cssfontparser: 1.2.1 @@ -17185,6 +17415,8 @@ snapshots: yoctocolors-cjs@2.1.3: {} + zimmerframe@1.1.4: {} + zod-package-json@1.2.0: dependencies: zod: 3.25.76