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}
+
+{/if}
+
+{#if stepPayload?.callbacks}
+
+{: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