@nexartis/sentinel-sdk 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,21 +9,43 @@
9
9
  */
10
10
  let isRefreshing = false;
11
11
  let refreshPromise = null;
12
- async function refreshAccessToken() {
12
+ function composeSignals(signals) {
13
+ const live = signals.filter((s) => s);
14
+ if (live.length === 0) {
15
+ return new AbortController().signal;
16
+ }
17
+ if (live.length === 1) {
18
+ return live[0];
19
+ }
20
+ if (typeof AbortSignal.any === 'function') {
21
+ return AbortSignal.any(live);
22
+ }
23
+ const controller = new AbortController();
24
+ const onAbort = () => {
25
+ const reason = live.find((s) => s.aborted)?.reason;
26
+ if (!controller.signal.aborted)
27
+ controller.abort(reason);
28
+ };
29
+ for (const signal of live) {
30
+ if (signal.aborted) {
31
+ onAbort();
32
+ return controller.signal;
33
+ }
34
+ signal.addEventListener('abort', onAbort, { once: true });
35
+ }
36
+ return controller.signal;
37
+ }
38
+ async function refreshAccessToken(callerSignal) {
13
39
  if (isRefreshing && refreshPromise) {
14
40
  return refreshPromise;
15
41
  }
16
42
  isRefreshing = true;
17
43
  refreshPromise = (async () => {
18
44
  try {
19
- // The refresh sub-call is intentionally NOT wrapped in the caller's
20
- // timeoutMs. It has its own well-behaved backend contract and we do
21
- // not want a mid-refresh abort to leave the client with partially
22
- // rotated tokens. If refresh itself hangs pathologically the
23
- // browser/network stack will eventually fail it.
24
45
  const response = await fetch('/api/auth/refresh', {
25
46
  method: 'POST',
26
- credentials: 'include'
47
+ credentials: 'include',
48
+ signal: callerSignal ?? undefined
27
49
  });
28
50
  if (response.ok) {
29
51
  const result = await response.json();
@@ -65,7 +87,7 @@ function buildAttemptSignal(callerSignal, timeoutMs) {
65
87
  if (!callerSignal) {
66
88
  return timeoutSignal;
67
89
  }
68
- return AbortSignal.any([callerSignal, timeoutSignal]);
90
+ return composeSignals([callerSignal, timeoutSignal]);
69
91
  }
70
92
  /**
71
93
  * Authenticated fetch wrapper with automatic token refresh.
@@ -84,7 +106,7 @@ export async function authFetch(url, options) {
84
106
  signal: buildAttemptSignal(callerSignal, effectiveTimeout)
85
107
  });
86
108
  if (response.status === 401) {
87
- const refreshed = await refreshAccessToken();
109
+ const refreshed = await refreshAccessToken(callerSignal);
88
110
  if (refreshed) {
89
111
  // Attempt #2 — brand new timeout budget. Do NOT reuse attempt #1's
90
112
  // signal (that budget may already be nearly exhausted); build a
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sveltekit/client/index.ts"],"names":[],"mappings":"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","sourcesContent":["// src/sveltekit/client/index.ts\n\n/**\n * Authenticated fetch wrapper with automatic token refresh.\n *\n * Use for:\n * - Multipart/chunked file uploads\n * - Sequential API calls with delays\n * - Any scenario where token might expire between requests\n */\n\nlet isRefreshing = false;\nlet refreshPromise: Promise<boolean> | null = null;\n\nasync function refreshAccessToken(): Promise<boolean> {\n\tif (isRefreshing && refreshPromise) {\n\t\treturn refreshPromise;\n\t}\n\n\tisRefreshing = true;\n\trefreshPromise = (async () => {\n\t\ttry {\n\t\t\t// The refresh sub-call is intentionally NOT wrapped in the caller's\n\t\t\t// timeoutMs. It has its own well-behaved backend contract and we do\n\t\t\t// not want a mid-refresh abort to leave the client with partially\n\t\t\t// rotated tokens. If refresh itself hangs pathologically the\n\t\t\t// browser/network stack will eventually fail it.\n\t\t\tconst response = await fetch('/api/auth/refresh', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tcredentials: 'include'\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst result = await response.json();\n\t\t\t\treturn result.success === true;\n\t\t\t}\n\n\t\t\treturn false;\n\t\t} catch (error) {\n\t\t\tconsole.error('Token refresh error:', error);\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tisRefreshing = false;\n\t\t\trefreshPromise = null;\n\t\t}\n\t})();\n\n\treturn refreshPromise;\n}\n\n/**\n * Extended options for {@link authFetch} / {@link authFetchJson}.\n *\n * Adds a per-attempt request timeout on top of the standard `RequestInit`.\n * The timeout is applied INDEPENDENTLY to the initial request and to the\n * post-401-refresh retry, so a slow first attempt does not steal budget from\n * the retry.\n */\nexport interface AuthFetchOptions extends RequestInit {\n\t/**\n\t * Per-attempt timeout in milliseconds.\n\t *\n\t * - Defaults to `60_000` (60 s) when omitted.\n\t * - `0` or `Infinity` disables the internal timeout entirely (caller's\n\t * own `signal` still applies).\n\t * - The timeout is applied SEPARATELY to attempt #1 and to the post-401\n\t * refresh retry — each gets a fresh budget.\n\t * - Composed with `options.signal` via `AbortSignal.any([...])` so caller\n\t * cancellation still works and a tighter caller signal wins.\n\t * - Does NOT apply to the internal `/api/auth/refresh` sub-call.\n\t */\n\ttimeoutMs?: number;\n}\n\n/** Default per-attempt timeout when `timeoutMs` is not provided. */\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\n/**\n * Build the effective per-attempt AbortSignal for a single fetch attempt.\n *\n * - When `timeoutMs` disables the timeout, we pass through the caller signal\n * (or `undefined`).\n * - When timeout is enabled and the caller supplied a signal, we compose them\n * with `AbortSignal.any` so whichever aborts first wins.\n *\n * Node engine is >=20 so `AbortSignal.any` / `AbortSignal.timeout` are\n * available; modern browsers (Chrome 116+, Firefox 124+, Safari 17.4+)\n * also ship them.\n */\nfunction buildAttemptSignal(\n\tcallerSignal: AbortSignal | null | undefined,\n\ttimeoutMs: number\n): AbortSignal | undefined {\n\tconst timeoutDisabled = timeoutMs === 0 || !Number.isFinite(timeoutMs);\n\tif (timeoutDisabled) {\n\t\treturn callerSignal ?? undefined;\n\t}\n\tconst timeoutSignal = AbortSignal.timeout(timeoutMs);\n\tif (!callerSignal) {\n\t\treturn timeoutSignal;\n\t}\n\treturn AbortSignal.any([callerSignal, timeoutSignal]);\n}\n\n/**\n * Authenticated fetch wrapper with automatic token refresh.\n *\n * Applies a per-attempt timeout (default 60 s) to attempt #1 and to the\n * post-401-refresh retry. Each attempt gets its OWN fresh timeout budget so\n * a slow first attempt cannot starve the retry. See {@link AuthFetchOptions}.\n */\nexport async function authFetch(\n\turl: string | URL,\n\toptions?: AuthFetchOptions\n): Promise<Response> {\n\tconst { timeoutMs, signal: callerSignal, ...restOptions } = options ?? {};\n\tconst effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n\t// Attempt #1 — fresh timeout budget composed with caller signal.\n\tlet response = await fetch(url, {\n\t\t...restOptions,\n\t\tcredentials: 'include',\n\t\tsignal: buildAttemptSignal(callerSignal, effectiveTimeout)\n\t});\n\n\tif (response.status === 401) {\n\t\tconst refreshed = await refreshAccessToken();\n\n\t\tif (refreshed) {\n\t\t\t// Attempt #2 — brand new timeout budget. Do NOT reuse attempt #1's\n\t\t\t// signal (that budget may already be nearly exhausted); build a\n\t\t\t// fresh one instead. The caller's signal is still honored via\n\t\t\t// AbortSignal.any so external cancellation continues to work.\n\t\t\tresponse = await fetch(url, {\n\t\t\t\t...restOptions,\n\t\t\t\tcredentials: 'include',\n\t\t\t\tsignal: buildAttemptSignal(callerSignal, effectiveTimeout)\n\t\t\t});\n\t\t}\n\t\t// If refresh failed, do NOT hard-redirect to /auth.\n\t\t// With Sentinel's rotating refresh tokens, concurrent requests can\n\t\t// race: one wins the refresh (R1→R2) while the others fail because\n\t\t// R1 is already invalidated. A hard redirect would log the user out\n\t\t// even though the browser already has fresh cookies from the winning\n\t\t// response. Instead, return the original 401 response and let the\n\t\t// caller handle it — polling callers will silently retry on the next\n\t\t// interval with the updated cookies, and SvelteKit's protected-route\n\t\t// redirect handles truly expired sessions.\n\t}\n\n\treturn response;\n}\n\n/**\n * Authenticated fetch wrapper with JSON response parsing\n */\nexport async function authFetchJson<T = any>(\n\turl: string | URL,\n\toptions?: AuthFetchOptions\n): Promise<T> {\n\tconst response = await authFetch(url, options);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`HTTP error! status: ${response.status}`);\n\t}\n\n\treturn response.json();\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sveltekit/client/index.ts"],"names":[],"mappings":"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","sourcesContent":["// src/sveltekit/client/index.ts\n\n/**\n * Authenticated fetch wrapper with automatic token refresh.\n *\n * Use for:\n * - Multipart/chunked file uploads\n * - Sequential API calls with delays\n * - Any scenario where token might expire between requests\n */\n\nlet isRefreshing = false;\nlet refreshPromise: Promise<boolean> | null = null;\n\nfunction composeSignals(signals: AbortSignal[]): AbortSignal {\n\tconst live = signals.filter((s) => s);\n\tif (live.length === 0) {\n\t\treturn new AbortController().signal;\n\t}\n\tif (live.length === 1) {\n\t\treturn live[0];\n\t}\n\tif (typeof AbortSignal.any === 'function') {\n\t\treturn AbortSignal.any(live);\n\t}\n\tconst controller = new AbortController();\n\tconst onAbort = () => {\n\t\tconst reason = live.find((s) => s.aborted)?.reason;\n\t\tif (!controller.signal.aborted) controller.abort(reason);\n\t};\n\tfor (const signal of live) {\n\t\tif (signal.aborted) {\n\t\t\tonAbort();\n\t\t\treturn controller.signal;\n\t\t}\n\t\tsignal.addEventListener('abort', onAbort, { once: true });\n\t}\n\treturn controller.signal;\n}\n\nasync function refreshAccessToken(callerSignal?: AbortSignal | null): Promise<boolean> {\n\tif (isRefreshing && refreshPromise) {\n\t\treturn refreshPromise;\n\t}\n\n\tisRefreshing = true;\n\trefreshPromise = (async () => {\n\t\ttry {\n\t\t\tconst response = await fetch('/api/auth/refresh', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tcredentials: 'include',\n\t\t\t\tsignal: callerSignal ?? undefined\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst result = await response.json();\n\t\t\t\treturn result.success === true;\n\t\t\t}\n\n\t\t\treturn false;\n\t\t} catch (error) {\n\t\t\tconsole.error('Token refresh error:', error);\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tisRefreshing = false;\n\t\t\trefreshPromise = null;\n\t\t}\n\t})();\n\n\treturn refreshPromise;\n}\n\n/**\n * Extended options for {@link authFetch} / {@link authFetchJson}.\n *\n * Adds a per-attempt request timeout on top of the standard `RequestInit`.\n * The timeout is applied INDEPENDENTLY to the initial request and to the\n * post-401-refresh retry, so a slow first attempt does not steal budget from\n * the retry.\n */\nexport interface AuthFetchOptions extends RequestInit {\n\t/**\n\t * Per-attempt timeout in milliseconds.\n\t *\n\t * - Defaults to `60_000` (60 s) when omitted.\n\t * - `0` or `Infinity` disables the internal timeout entirely (caller's\n\t * own `signal` still applies).\n\t * - The timeout is applied SEPARATELY to attempt #1 and to the post-401\n\t * refresh retry — each gets a fresh budget.\n\t * - Composed with `options.signal` via `AbortSignal.any([...])` so caller\n\t * cancellation still works and a tighter caller signal wins.\n\t * - Does NOT apply to the internal `/api/auth/refresh` sub-call.\n\t */\n\ttimeoutMs?: number;\n}\n\n/** Default per-attempt timeout when `timeoutMs` is not provided. */\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\n/**\n * Build the effective per-attempt AbortSignal for a single fetch attempt.\n *\n * - When `timeoutMs` disables the timeout, we pass through the caller signal\n * (or `undefined`).\n * - When timeout is enabled and the caller supplied a signal, we compose them\n * with `AbortSignal.any` so whichever aborts first wins.\n *\n * Node engine is >=20 so `AbortSignal.any` / `AbortSignal.timeout` are\n * available; modern browsers (Chrome 116+, Firefox 124+, Safari 17.4+)\n * also ship them.\n */\nfunction buildAttemptSignal(\n\tcallerSignal: AbortSignal | null | undefined,\n\ttimeoutMs: number\n): AbortSignal | undefined {\n\tconst timeoutDisabled = timeoutMs === 0 || !Number.isFinite(timeoutMs);\n\tif (timeoutDisabled) {\n\t\treturn callerSignal ?? undefined;\n\t}\n\tconst timeoutSignal = AbortSignal.timeout(timeoutMs);\n\tif (!callerSignal) {\n\t\treturn timeoutSignal;\n\t}\n\treturn composeSignals([callerSignal, timeoutSignal]);\n}\n\n/**\n * Authenticated fetch wrapper with automatic token refresh.\n *\n * Applies a per-attempt timeout (default 60 s) to attempt #1 and to the\n * post-401-refresh retry. Each attempt gets its OWN fresh timeout budget so\n * a slow first attempt cannot starve the retry. See {@link AuthFetchOptions}.\n */\nexport async function authFetch(\n\turl: string | URL,\n\toptions?: AuthFetchOptions\n): Promise<Response> {\n\tconst { timeoutMs, signal: callerSignal, ...restOptions } = options ?? {};\n\tconst effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n\t// Attempt #1 — fresh timeout budget composed with caller signal.\n\tlet response = await fetch(url, {\n\t\t...restOptions,\n\t\tcredentials: 'include',\n\t\tsignal: buildAttemptSignal(callerSignal, effectiveTimeout)\n\t});\n\n\tif (response.status === 401) {\n\t\tconst refreshed = await refreshAccessToken(callerSignal);\n\n\t\tif (refreshed) {\n\t\t\t// Attempt #2 — brand new timeout budget. Do NOT reuse attempt #1's\n\t\t\t// signal (that budget may already be nearly exhausted); build a\n\t\t\t// fresh one instead. The caller's signal is still honored via\n\t\t\t// AbortSignal.any so external cancellation continues to work.\n\t\t\tresponse = await fetch(url, {\n\t\t\t\t...restOptions,\n\t\t\t\tcredentials: 'include',\n\t\t\t\tsignal: buildAttemptSignal(callerSignal, effectiveTimeout)\n\t\t\t});\n\t\t}\n\t\t// If refresh failed, do NOT hard-redirect to /auth.\n\t\t// With Sentinel's rotating refresh tokens, concurrent requests can\n\t\t// race: one wins the refresh (R1→R2) while the others fail because\n\t\t// R1 is already invalidated. A hard redirect would log the user out\n\t\t// even though the browser already has fresh cookies from the winning\n\t\t// response. Instead, return the original 401 response and let the\n\t\t// caller handle it — polling callers will silently retry on the next\n\t\t// interval with the updated cookies, and SvelteKit's protected-route\n\t\t// redirect handles truly expired sessions.\n\t}\n\n\treturn response;\n}\n\n/**\n * Authenticated fetch wrapper with JSON response parsing\n */\nexport async function authFetchJson<T = any>(\n\turl: string | URL,\n\toptions?: AuthFetchOptions\n): Promise<T> {\n\tconst response = await authFetch(url, options);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`HTTP error! status: ${response.status}`);\n\t}\n\n\treturn response.json();\n}\n"]}
@@ -1,5 +1,7 @@
1
1
  import type { Handle } from '@sveltejs/kit';
2
2
  import type { SentinelHandleOptions, UserLocals } from '../core/types.js';
3
+ /** Test-only: reset the once-per-isolate skip-verify warning/refusal latches. */
4
+ export declare function __resetSkipVerifyWarningForTests(): void;
3
5
  /** Locals shape set by the Sentinel auth middleware. */
4
6
  export interface SentinelLocals {
5
7
  user: UserLocals | null;
@@ -9,8 +11,9 @@ export interface SentinelLocals {
9
11
  * Creates a SvelteKit handle function with Sentinel authentication built-in.
10
12
  *
11
13
  * Features:
12
- * - Local JWT decode on every request (no per-request API call)
13
- * - API-validated token refresh when expired or structurally invalid
14
+ * - JWKS signature verification of the access token on every request
15
+ * (Ed25519/EdDSA, cached public keys — no per-request API call)
16
+ * - API-validated token refresh when expired or failing verification
14
17
  * - Local expiration check (saves API calls)
15
18
  * - Protected route redirection
16
19
  * - Sets event.locals.user for all routes
@@ -1 +1 @@
1
- {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../../src/sveltekit/handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAgB,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAgC,MAAM,kBAAkB,CAAC;AAMxG,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CA0DhF"}
1
+ {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../../src/sveltekit/handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAgB,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAgC,MAAM,kBAAkB,CAAC;AAyCxG,iFAAiF;AACjF,wBAAgB,gCAAgC,IAAI,IAAI,CAGvD;AAED,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CA0EhF"}
@@ -1,13 +1,46 @@
1
1
  import { SentinelClient } from '../core/client.js';
2
2
  import { isTokenExpired, decodeTokenLocally } from '../core/auth-utils.js';
3
+ import { createJwksVerifier } from '../core/jwks.js';
3
4
  import { createLogger } from '../core/logger.js';
4
5
  import { COOKIE_NAMES, COOKIE_OPTIONS } from './cookies.js';
6
+ /**
7
+ * DEVELOPMENT-ONLY escape hatch. When set to "1" (or "true") the handle skips
8
+ * JWKS signature verification and trusts the decoded token payload, which is
9
+ * the pre-1.8.0 behaviour and the exact hole the admin-gate audit flagged.
10
+ * Never set this in production: when NODE_ENV=production the flag is REFUSED
11
+ * (construction throws for the programmatic option; per-request the flag is
12
+ * ignored with a loud classified error) and verification always runs.
13
+ */
14
+ const SKIP_VERIFY_ENV = 'SENTINEL_SKIP_JWKS_VERIFY';
15
+ let skipVerifyWarningEmitted = false;
16
+ function warnSkipVerifyOnce(log) {
17
+ if (skipVerifyWarningEmitted)
18
+ return;
19
+ skipVerifyWarningEmitted = true;
20
+ log.warn(`${SKIP_VERIFY_ENV} is enabled — JWT signatures are NOT being verified. ` +
21
+ 'Access-token payloads are trusted unverified. Development use only.');
22
+ }
23
+ let skipVerifyRefusalEmitted = false;
24
+ function refuseSkipVerifyInProduction(log) {
25
+ if (skipVerifyRefusalEmitted)
26
+ return;
27
+ skipVerifyRefusalEmitted = true;
28
+ log.error(`${SKIP_VERIFY_ENV} refused in production (NODE_ENV=production) — ` +
29
+ 'refusing to disable JWKS signature verification. ' +
30
+ 'Unset the flag or run with NODE_ENV=development.');
31
+ }
32
+ /** Test-only: reset the once-per-isolate skip-verify warning/refusal latches. */
33
+ export function __resetSkipVerifyWarningForTests() {
34
+ skipVerifyWarningEmitted = false;
35
+ skipVerifyRefusalEmitted = false;
36
+ }
5
37
  /**
6
38
  * Creates a SvelteKit handle function with Sentinel authentication built-in.
7
39
  *
8
40
  * Features:
9
- * - Local JWT decode on every request (no per-request API call)
10
- * - API-validated token refresh when expired or structurally invalid
41
+ * - JWKS signature verification of the access token on every request
42
+ * (Ed25519/EdDSA, cached public keys — no per-request API call)
43
+ * - API-validated token refresh when expired or failing verification
11
44
  * - Local expiration check (saves API calls)
12
45
  * - Protected route redirection
13
46
  * - Sets event.locals.user for all routes
@@ -16,6 +49,13 @@ import { COOKIE_NAMES, COOKIE_OPTIONS } from './cookies.js';
16
49
  export function createSentinelHandle(options = {}) {
17
50
  const { protectedRoutes = [], loginRedirect = '/auth' } = options;
18
51
  const log = options.logger ?? createLogger('sentinel-handle');
52
+ // Fail fast at construction when the unsafe escape hatch is requested in production.
53
+ if (options.skipJwksVerify === true &&
54
+ typeof process !== 'undefined' &&
55
+ process.env?.NODE_ENV === 'production') {
56
+ throw new Error(`${SKIP_VERIFY_ENV}: skipJwksVerify option refused in production ` +
57
+ '(NODE_ENV=production) — JWKS signature verification cannot be disabled.');
58
+ }
19
59
  return async ({ event, resolve }) => {
20
60
  // Initialize locals with typed shape
21
61
  const locals = event.locals;
@@ -23,25 +63,29 @@ export function createSentinelHandle(options = {}) {
23
63
  locals.accessToken = null;
24
64
  // Get config from environment or options (with request URL fallback)
25
65
  const config = getConfig(options, event.platform, event.request.url);
66
+ const skipVerify = isJwksVerifyDisabled(options, event.platform, log);
67
+ if (skipVerify)
68
+ warnSkipVerifyOnce(log);
69
+ const verifier = skipVerify ? null : createVerifier(options, config, event, log);
26
70
  const accessToken = event.cookies.get(COOKIE_NAMES.ACCESS_TOKEN);
27
71
  const refreshToken = event.cookies.get(COOKIE_NAMES.REFRESH_TOKEN);
28
72
  if (accessToken) {
29
73
  if (!isTokenExpired(accessToken)) {
30
- const decoded = decodeTokenLocally(accessToken);
74
+ const decoded = await resolveVerifiedToken(accessToken, verifier, log);
31
75
  if (decoded) {
32
76
  locals.user = createUserLocals(decoded);
33
77
  locals.accessToken = accessToken;
34
78
  }
35
79
  else {
36
- await tryRefreshToken(event, config, refreshToken, log);
80
+ await tryRefreshToken(event, config, refreshToken, log, verifier);
37
81
  }
38
82
  }
39
83
  else if (refreshToken) {
40
- await tryRefreshToken(event, config, refreshToken, log);
84
+ await tryRefreshToken(event, config, refreshToken, log, verifier);
41
85
  }
42
86
  }
43
87
  else if (refreshToken) {
44
- await tryRefreshToken(event, config, refreshToken, log);
88
+ await tryRefreshToken(event, config, refreshToken, log, verifier);
45
89
  }
46
90
  // Protected routes check — redirect to login with return URL
47
91
  const isProtectedRoute = protectedRoutes.some(route => event.url.pathname.startsWith(route));
@@ -61,6 +105,73 @@ export function createSentinelHandle(options = {}) {
61
105
  return response;
62
106
  };
63
107
  }
108
+ /**
109
+ * Verify an access token's signature against the auth server's JWKS and
110
+ * return the decoded payload only when the signature checks out.
111
+ * With `verifier === null` (explicit dev-only opt-out) falls back to the
112
+ * legacy unverified local decode. Never throws.
113
+ */
114
+ async function resolveVerifiedToken(accessToken, verifier, log) {
115
+ if (!verifier) {
116
+ return decodeTokenLocally(accessToken);
117
+ }
118
+ const outcome = await verifier.verify(accessToken);
119
+ if (outcome.status === 'valid') {
120
+ return outcome.payload;
121
+ }
122
+ log.warn('Access token rejected by signature verification', {
123
+ status: outcome.status,
124
+ reason: outcome.reason
125
+ });
126
+ return null;
127
+ }
128
+ function isProductionEnvironment(platform) {
129
+ const platformEnv = platform?.env;
130
+ const fromPlatform = platformEnv?.NODE_ENV;
131
+ const fromProcess = typeof process !== 'undefined' ? process.env?.NODE_ENV : undefined;
132
+ return (fromPlatform ?? fromProcess) === 'production';
133
+ }
134
+ function isJwksVerifyDisabled(options, platform, log) {
135
+ if (options.skipJwksVerify === true) {
136
+ if (isProductionEnvironment(platform)) {
137
+ refuseSkipVerifyInProduction(log);
138
+ return false;
139
+ }
140
+ return true;
141
+ }
142
+ const platformEnv = platform?.env;
143
+ const fromPlatform = platformEnv?.[SKIP_VERIFY_ENV];
144
+ const fromProcess = typeof process !== 'undefined' ? process.env?.[SKIP_VERIFY_ENV] : undefined;
145
+ const value = fromPlatform ?? fromProcess;
146
+ const requested = value === '1' || value === 'true';
147
+ if (requested && isProductionEnvironment(platform)) {
148
+ refuseSkipVerifyInProduction(log);
149
+ return false;
150
+ }
151
+ return requested;
152
+ }
153
+ function createVerifier(options, config, event, log) {
154
+ const platformEnv = event.platform?.env;
155
+ const jwksUrl = options.jwksUrl || platformEnv?.SENTINEL_JWKS_URL || deriveJwksUrl(config.apiUrl);
156
+ return createJwksVerifier({
157
+ jwksUrl,
158
+ fetchFn: event.fetch,
159
+ cacheTtlMs: options.jwksCacheTtlMs,
160
+ logger: log
161
+ });
162
+ }
163
+ /**
164
+ * Derive the JWKS URL from the Sentinel API URL. getConfig normalises apiUrl
165
+ * to `<origin>/api`, so stripping the `/api` suffix yields the origin that
166
+ * serves `/.well-known/jwks.json`.
167
+ */
168
+ function deriveJwksUrl(apiUrl) {
169
+ const origin = apiUrl.endsWith('/api') ? apiUrl.slice(0, -'/api'.length) : apiUrl;
170
+ let trimmed = origin;
171
+ while (trimmed.endsWith('/'))
172
+ trimmed = trimmed.slice(0, -1);
173
+ return `${trimmed}/.well-known/jwks.json`;
174
+ }
64
175
  function getConfig(options, platform, requestUrl) {
65
176
  let apiUrl = options.apiUrl || platform?.env?.SENTINEL_API_URL || '';
66
177
  let baseUrl = options.baseUrl || platform?.env?.VITE_BASE_URL || '';
@@ -112,7 +223,7 @@ function createUserLocals(decoded) {
112
223
  roles: decoded.roles || []
113
224
  };
114
225
  }
115
- async function tryRefreshToken(event, config, refreshToken, log) {
226
+ async function tryRefreshToken(event, config, refreshToken, log, verifier) {
116
227
  if (!refreshToken)
117
228
  return;
118
229
  try {
@@ -125,9 +236,11 @@ async function tryRefreshToken(event, config, refreshToken, log) {
125
236
  if (result.refresh_token) {
126
237
  event.cookies.set(COOKIE_NAMES.REFRESH_TOKEN, result.refresh_token, COOKIE_OPTIONS.REFRESH_TOKEN_LONG);
127
238
  }
128
- // Local decode — no second API call. The token was just issued by
129
- // the Sentinel backend during refresh, so it's already valid.
130
- const decoded = decodeTokenLocally(result.access_token);
239
+ // Verify the freshly-issued token's signature before trusting its
240
+ // payload (same JWKS path as the cookie flow). The cookie is kept —
241
+ // it was just minted by the backend over TLS — but locals.user is
242
+ // only set when verification passes; the next request retries.
243
+ const decoded = await resolveVerifiedToken(result.access_token, verifier, log);
131
244
  if (decoded) {
132
245
  const locals = event.locals;
133
246
  locals.user = createUserLocals(decoded);
@@ -1 +1 @@
1
- {"version":3,"file":"handle.js","sourceRoot":"","sources":["../../src/sveltekit/handle.ts"],"names":[],"mappings":"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","sourcesContent":["// src/sveltekit/handle.ts\nimport type { Handle, RequestEvent } from '@sveltejs/kit';\nimport type { SentinelHandleOptions, UserLocals, SentinelConfig, DecodedToken } from '../core/types.js';\nimport { SentinelClient } from '../core/client.js';\nimport { isTokenExpired, decodeTokenLocally } from '../core/auth-utils.js';\nimport { createLogger, type SentinelLogger } from '../core/logger.js';\nimport { COOKIE_NAMES, COOKIE_OPTIONS } from './cookies.js';\n\n/** Locals shape set by the Sentinel auth middleware. */\nexport interface SentinelLocals {\n\tuser: UserLocals | null;\n\taccessToken: string | null;\n}\n\n/**\n * Creates a SvelteKit handle function with Sentinel authentication built-in.\n *\n * Features:\n * - Local JWT decode on every request (no per-request API call)\n * - API-validated token refresh when expired or structurally invalid\n * - Local expiration check (saves API calls)\n * - Protected route redirection\n * - Sets event.locals.user for all routes\n * - Never clears cookies (only explicit logout does)\n */\nexport function createSentinelHandle(options: SentinelHandleOptions = {}): Handle {\n\tconst {\n\t\tprotectedRoutes = [],\n\t\tloginRedirect = '/auth'\n\t} = options;\n\tconst log = options.logger ?? createLogger('sentinel-handle');\n\n\treturn async ({ event, resolve }) => {\n\t\t// Initialize locals with typed shape\n\t\tconst locals = event.locals as SentinelLocals;\n\t\tlocals.user = null;\n\t\tlocals.accessToken = null;\n\n\t\t// Get config from environment or options (with request URL fallback)\n\t\tconst config = getConfig(options, event.platform, event.request.url);\n\n\t\tconst accessToken = event.cookies.get(COOKIE_NAMES.ACCESS_TOKEN);\n\t\tconst refreshToken = event.cookies.get(COOKIE_NAMES.REFRESH_TOKEN);\n\n\t\tif (accessToken) {\n\t\t\tif (!isTokenExpired(accessToken)) {\n\t\t\t\tconst decoded = decodeTokenLocally(accessToken);\n\t\t\t\tif (decoded) {\n\t\t\t\t\tlocals.user = createUserLocals(decoded);\n\t\t\t\t\tlocals.accessToken = accessToken;\n\t\t\t\t} else {\n\t\t\t\t\tawait tryRefreshToken(event, config, refreshToken, log);\n\t\t\t\t}\n\t\t\t} else if (refreshToken) {\n\t\t\t\tawait tryRefreshToken(event, config, refreshToken, log);\n\t\t\t}\n\t\t} else if (refreshToken) {\n\t\t\tawait tryRefreshToken(event, config, refreshToken, log);\n\t\t}\n\n\t\t// Protected routes check — redirect to login with return URL\n\t\tconst isProtectedRoute = protectedRoutes.some(route =>\n\t\t\tevent.url.pathname.startsWith(route)\n\t\t);\n\n\t\tif (isProtectedRoute && !locals.user) {\n\t\t\tconst returnPath = event.url.pathname + event.url.search;\n\t\t\tconst redirectUrl = `${loginRedirect}?redirect=${encodeURIComponent(returnPath)}`;\n\t\t\treturn new Response('Redirect', {\n\t\t\t\tstatus: 302,\n\t\t\t\theaders: { Location: redirectUrl }\n\t\t\t});\n\t\t}\n\n\t\tconst response = await resolve(event);\n\n\t\t// Add security headers\n\t\tresponse.headers.set('X-Frame-Options', 'DENY');\n\t\tresponse.headers.set('X-Content-Type-Options', 'nosniff');\n\t\tresponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');\n\n\t\treturn response;\n\t};\n}\n\nfunction getConfig(options: SentinelHandleOptions, platform: Readonly<App.Platform> | undefined, requestUrl?: string): SentinelConfig {\n\tlet apiUrl = options.apiUrl || (platform as Record<string, any>)?.env?.SENTINEL_API_URL || '';\n\tlet baseUrl = options.baseUrl || (platform as Record<string, any>)?.env?.VITE_BASE_URL || '';\n\n\tif (!apiUrl) {\n\t\tthrow new Error('SENTINEL_API_URL must be configured');\n\t}\n\n\t// Normalize: strip trailing slashes, then append /api if not already present\n\twhile (apiUrl.endsWith('/')) apiUrl = apiUrl.slice(0, -1);\n\tif (!apiUrl.endsWith('/api')) {\n\t\tapiUrl = apiUrl + '/api';\n\t}\n\n\t// ARCH-003 fix: Derive baseUrl from request URL when not configured.\n\t// Workers for Platforms deployments don't know their URL at build time.\n\tif (!baseUrl && requestUrl) {\n\t\ttry {\n\t\t\tconst url = new URL(requestUrl);\n\t\t\tconst isLoopback = url.hostname === 'localhost'\n\t\t\t\t|| url.hostname === '127.0.0.1'\n\t\t\t\t|| url.hostname === '0.0.0.0'\n\t\t\t\t|| url.hostname === '::1';\n\n\t\t\tlet protocol = url.protocol;\n\t\t\tlet forcedHttps = false;\n\t\t\tif (!isLoopback && protocol !== 'https:') {\n\t\t\t\tprotocol = 'https:';\n\t\t\t\tforcedHttps = true;\n\t\t\t}\n\n\t\t\t// Build URL with hostname (not host) to avoid carrying\n\t\t\t// default-for-original-scheme ports across a protocol switch.\n\t\t\t// Drop port entirely when forcing HTTPS (Workers serve on standard ports).\n\t\t\tconst host = url.hostname.includes(':') ? `[${url.hostname}]` : url.hostname;\n\t\t\tconst portSuffix = (!forcedHttps && url.port) ? `:${url.port}` : '';\n\t\t\tbaseUrl = `${protocol}//${host}${portSuffix}`;\n\t\t} catch {\n\t\t\t// If URL parsing fails, baseUrl stays empty\n\t\t}\n\t}\n\n\tif (!baseUrl) {\n\t\tthrow new Error('VITE_BASE_URL must be configured (or provide a valid request URL for runtime detection)');\n\t}\n\n\treturn { apiUrl, baseUrl };\n}\n\nfunction createUserLocals(decoded: DecodedToken): UserLocals {\n\treturn {\n\t\tisAuthenticated: true,\n\t\tid: decoded.sub,\n\t\temail: decoded.email,\n\t\troles: decoded.roles || []\n\t};\n}\n\nasync function tryRefreshToken(event: RequestEvent, config: SentinelConfig, refreshToken: string | undefined, log: SentinelLogger): Promise<void> {\n\tif (!refreshToken) return;\n\n\ttry {\n\t\tconst sentinel = new SentinelClient(config, event.fetch);\n\t\tconst result = await sentinel.refreshToken(refreshToken);\n\n\t\tif (result.code === 0 || result.success) {\n\t\t\tif (result.access_token) {\n\t\t\t\tevent.cookies.set(COOKIE_NAMES.ACCESS_TOKEN, result.access_token, COOKIE_OPTIONS.ACCESS_TOKEN);\n\n\t\t\t\t// Handle refresh token rotation\n\t\t\t\tif (result.refresh_token) {\n\t\t\t\t\tevent.cookies.set(COOKIE_NAMES.REFRESH_TOKEN, result.refresh_token, COOKIE_OPTIONS.REFRESH_TOKEN_LONG);\n\t\t\t\t}\n\n\t\t\t\t// Local decode — no second API call. The token was just issued by\n\t\t\t\t// the Sentinel backend during refresh, so it's already valid.\n\t\t\t\tconst decoded = decodeTokenLocally(result.access_token);\n\t\t\t\tif (decoded) {\n\t\t\t\t\tconst locals = event.locals as SentinelLocals;\n\t\t\t\t\tlocals.user = createUserLocals(decoded);\n\t\t\t\t\tlocals.accessToken = result.access_token;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tlog.warn('Token refresh failed - continuing without user');\n\t\t}\n\t} catch (error) {\n\t\tlog.error('Token refresh error', { error: String(error) });\n\t}\n}\n\n"]}
1
+ {"version":3,"file":"handle.js","sourceRoot":"","sources":["../../src/sveltekit/handle.ts"],"names":[],"mappings":"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","sourcesContent":["// src/sveltekit/handle.ts\nimport type { Handle, RequestEvent } from '@sveltejs/kit';\nimport type { SentinelHandleOptions, UserLocals, SentinelConfig, DecodedToken } from '../core/types.js';\nimport { SentinelClient } from '../core/client.js';\nimport { isTokenExpired, decodeTokenLocally } from '../core/auth-utils.js';\nimport { createJwksVerifier, type JwksVerifier } from '../core/jwks.js';\nimport { createLogger, type SentinelLogger } from '../core/logger.js';\nimport { COOKIE_NAMES, COOKIE_OPTIONS } from './cookies.js';\n\n// Node fallback for the dev-only skip flag (mirrors src/sveltekit/utils.ts).\ndeclare const process: any;\n\n/**\n * DEVELOPMENT-ONLY escape hatch. When set to \"1\" (or \"true\") the handle skips\n * JWKS signature verification and trusts the decoded token payload, which is\n * the pre-1.8.0 behaviour and the exact hole the admin-gate audit flagged.\n * Never set this in production: when NODE_ENV=production the flag is REFUSED\n * (construction throws for the programmatic option; per-request the flag is\n * ignored with a loud classified error) and verification always runs.\n */\nconst SKIP_VERIFY_ENV = 'SENTINEL_SKIP_JWKS_VERIFY';\n\nlet skipVerifyWarningEmitted = false;\nfunction warnSkipVerifyOnce(log: SentinelLogger): void {\n\tif (skipVerifyWarningEmitted) return;\n\tskipVerifyWarningEmitted = true;\n\tlog.warn(\n\t\t`${SKIP_VERIFY_ENV} is enabled — JWT signatures are NOT being verified. ` +\n\t\t\t'Access-token payloads are trusted unverified. Development use only.'\n\t);\n}\n\nlet skipVerifyRefusalEmitted = false;\nfunction refuseSkipVerifyInProduction(log: SentinelLogger): void {\n\tif (skipVerifyRefusalEmitted) return;\n\tskipVerifyRefusalEmitted = true;\n\tlog.error(\n\t\t`${SKIP_VERIFY_ENV} refused in production (NODE_ENV=production) — ` +\n\t\t\t'refusing to disable JWKS signature verification. ' +\n\t\t\t'Unset the flag or run with NODE_ENV=development.'\n\t);\n}\n\n/** Test-only: reset the once-per-isolate skip-verify warning/refusal latches. */\nexport function __resetSkipVerifyWarningForTests(): void {\n\tskipVerifyWarningEmitted = false;\n\tskipVerifyRefusalEmitted = false;\n}\n\n/** Locals shape set by the Sentinel auth middleware. */\nexport interface SentinelLocals {\n\tuser: UserLocals | null;\n\taccessToken: string | null;\n}\n\n/**\n * Creates a SvelteKit handle function with Sentinel authentication built-in.\n *\n * Features:\n * - JWKS signature verification of the access token on every request\n * (Ed25519/EdDSA, cached public keys — no per-request API call)\n * - API-validated token refresh when expired or failing verification\n * - Local expiration check (saves API calls)\n * - Protected route redirection\n * - Sets event.locals.user for all routes\n * - Never clears cookies (only explicit logout does)\n */\nexport function createSentinelHandle(options: SentinelHandleOptions = {}): Handle {\n\tconst {\n\t\tprotectedRoutes = [],\n\t\tloginRedirect = '/auth'\n\t} = options;\n\tconst log = options.logger ?? createLogger('sentinel-handle');\n\n\t// Fail fast at construction when the unsafe escape hatch is requested in production.\n\tif (\n\t\toptions.skipJwksVerify === true &&\n\t\ttypeof process !== 'undefined' &&\n\t\tprocess.env?.NODE_ENV === 'production'\n\t) {\n\t\tthrow new Error(\n\t\t\t`${SKIP_VERIFY_ENV}: skipJwksVerify option refused in production ` +\n\t\t\t\t'(NODE_ENV=production) — JWKS signature verification cannot be disabled.'\n\t\t);\n\t}\n\n\treturn async ({ event, resolve }) => {\n\t\t// Initialize locals with typed shape\n\t\tconst locals = event.locals as SentinelLocals;\n\t\tlocals.user = null;\n\t\tlocals.accessToken = null;\n\n\t\t// Get config from environment or options (with request URL fallback)\n\t\tconst config = getConfig(options, event.platform, event.request.url);\n\n\t\tconst skipVerify = isJwksVerifyDisabled(options, event.platform, log);\n\t\tif (skipVerify) warnSkipVerifyOnce(log);\n\t\tconst verifier = skipVerify ? null : createVerifier(options, config, event, log);\n\n\t\tconst accessToken = event.cookies.get(COOKIE_NAMES.ACCESS_TOKEN);\n\t\tconst refreshToken = event.cookies.get(COOKIE_NAMES.REFRESH_TOKEN);\n\n\t\tif (accessToken) {\n\t\t\tif (!isTokenExpired(accessToken)) {\n\t\t\t\tconst decoded = await resolveVerifiedToken(accessToken, verifier, log);\n\t\t\t\tif (decoded) {\n\t\t\t\t\tlocals.user = createUserLocals(decoded);\n\t\t\t\t\tlocals.accessToken = accessToken;\n\t\t\t\t} else {\n\t\t\t\t\tawait tryRefreshToken(event, config, refreshToken, log, verifier);\n\t\t\t\t}\n\t\t\t} else if (refreshToken) {\n\t\t\t\tawait tryRefreshToken(event, config, refreshToken, log, verifier);\n\t\t\t}\n\t\t} else if (refreshToken) {\n\t\t\tawait tryRefreshToken(event, config, refreshToken, log, verifier);\n\t\t}\n\n\t\t// Protected routes check — redirect to login with return URL\n\t\tconst isProtectedRoute = protectedRoutes.some(route =>\n\t\t\tevent.url.pathname.startsWith(route)\n\t\t);\n\n\t\tif (isProtectedRoute && !locals.user) {\n\t\t\tconst returnPath = event.url.pathname + event.url.search;\n\t\t\tconst redirectUrl = `${loginRedirect}?redirect=${encodeURIComponent(returnPath)}`;\n\t\t\treturn new Response('Redirect', {\n\t\t\t\tstatus: 302,\n\t\t\t\theaders: { Location: redirectUrl }\n\t\t\t});\n\t\t}\n\n\t\tconst response = await resolve(event);\n\n\t\t// Add security headers\n\t\tresponse.headers.set('X-Frame-Options', 'DENY');\n\t\tresponse.headers.set('X-Content-Type-Options', 'nosniff');\n\t\tresponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');\n\n\t\treturn response;\n\t};\n}\n\n/**\n * Verify an access token's signature against the auth server's JWKS and\n * return the decoded payload only when the signature checks out.\n * With `verifier === null` (explicit dev-only opt-out) falls back to the\n * legacy unverified local decode. Never throws.\n */\nasync function resolveVerifiedToken(\n\taccessToken: string,\n\tverifier: JwksVerifier | null,\n\tlog: SentinelLogger\n): Promise<DecodedToken | null> {\n\tif (!verifier) {\n\t\treturn decodeTokenLocally(accessToken);\n\t}\n\tconst outcome = await verifier.verify(accessToken);\n\tif (outcome.status === 'valid') {\n\t\treturn outcome.payload;\n\t}\n\tlog.warn('Access token rejected by signature verification', {\n\t\tstatus: outcome.status,\n\t\treason: outcome.reason\n\t});\n\treturn null;\n}\n\nfunction isProductionEnvironment(platform: Readonly<App.Platform> | undefined): boolean {\n\tconst platformEnv = (platform as Record<string, any>)?.env as Record<string, string> | undefined;\n\tconst fromPlatform = platformEnv?.NODE_ENV;\n\tconst fromProcess = typeof process !== 'undefined' ? process.env?.NODE_ENV : undefined;\n\treturn (fromPlatform ?? fromProcess) === 'production';\n}\n\nfunction isJwksVerifyDisabled(\n\toptions: SentinelHandleOptions,\n\tplatform: Readonly<App.Platform> | undefined,\n\tlog: SentinelLogger\n): boolean {\n\tif (options.skipJwksVerify === true) {\n\t\tif (isProductionEnvironment(platform)) {\n\t\t\trefuseSkipVerifyInProduction(log);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\tconst platformEnv = (platform as Record<string, any>)?.env as Record<string, string> | undefined;\n\tconst fromPlatform = platformEnv?.[SKIP_VERIFY_ENV];\n\tconst fromProcess = typeof process !== 'undefined' ? process.env?.[SKIP_VERIFY_ENV] : undefined;\n\tconst value = fromPlatform ?? fromProcess;\n\tconst requested = value === '1' || value === 'true';\n\tif (requested && isProductionEnvironment(platform)) {\n\t\trefuseSkipVerifyInProduction(log);\n\t\treturn false;\n\t}\n\treturn requested;\n}\n\nfunction createVerifier(\n\toptions: SentinelHandleOptions,\n\tconfig: SentinelConfig,\n\tevent: RequestEvent,\n\tlog: SentinelLogger\n): JwksVerifier {\n\tconst platformEnv = (event.platform as Record<string, any>)?.env as Record<string, string> | undefined;\n\tconst jwksUrl = options.jwksUrl || platformEnv?.SENTINEL_JWKS_URL || deriveJwksUrl(config.apiUrl);\n\treturn createJwksVerifier({\n\t\tjwksUrl,\n\t\tfetchFn: event.fetch,\n\t\tcacheTtlMs: options.jwksCacheTtlMs,\n\t\tlogger: log\n\t});\n}\n\n/**\n * Derive the JWKS URL from the Sentinel API URL. getConfig normalises apiUrl\n * to `<origin>/api`, so stripping the `/api` suffix yields the origin that\n * serves `/.well-known/jwks.json`.\n */\nfunction deriveJwksUrl(apiUrl: string): string {\n\tconst origin = apiUrl.endsWith('/api') ? apiUrl.slice(0, -'/api'.length) : apiUrl;\n\tlet trimmed = origin;\n\twhile (trimmed.endsWith('/')) trimmed = trimmed.slice(0, -1);\n\treturn `${trimmed}/.well-known/jwks.json`;\n}\n\nfunction getConfig(options: SentinelHandleOptions, platform: Readonly<App.Platform> | undefined, requestUrl?: string): SentinelConfig {\n\tlet apiUrl = options.apiUrl || (platform as Record<string, any>)?.env?.SENTINEL_API_URL || '';\n\tlet baseUrl = options.baseUrl || (platform as Record<string, any>)?.env?.VITE_BASE_URL || '';\n\n\tif (!apiUrl) {\n\t\tthrow new Error('SENTINEL_API_URL must be configured');\n\t}\n\n\t// Normalize: strip trailing slashes, then append /api if not already present\n\twhile (apiUrl.endsWith('/')) apiUrl = apiUrl.slice(0, -1);\n\tif (!apiUrl.endsWith('/api')) {\n\t\tapiUrl = apiUrl + '/api';\n\t}\n\n\t// ARCH-003 fix: Derive baseUrl from request URL when not configured.\n\t// Workers for Platforms deployments don't know their URL at build time.\n\tif (!baseUrl && requestUrl) {\n\t\ttry {\n\t\t\tconst url = new URL(requestUrl);\n\t\t\tconst isLoopback = url.hostname === 'localhost'\n\t\t\t\t|| url.hostname === '127.0.0.1'\n\t\t\t\t|| url.hostname === '0.0.0.0'\n\t\t\t\t|| url.hostname === '::1';\n\n\t\t\tlet protocol = url.protocol;\n\t\t\tlet forcedHttps = false;\n\t\t\tif (!isLoopback && protocol !== 'https:') {\n\t\t\t\tprotocol = 'https:';\n\t\t\t\tforcedHttps = true;\n\t\t\t}\n\n\t\t\t// Build URL with hostname (not host) to avoid carrying\n\t\t\t// default-for-original-scheme ports across a protocol switch.\n\t\t\t// Drop port entirely when forcing HTTPS (Workers serve on standard ports).\n\t\t\tconst host = url.hostname.includes(':') ? `[${url.hostname}]` : url.hostname;\n\t\t\tconst portSuffix = (!forcedHttps && url.port) ? `:${url.port}` : '';\n\t\t\tbaseUrl = `${protocol}//${host}${portSuffix}`;\n\t\t} catch {\n\t\t\t// If URL parsing fails, baseUrl stays empty\n\t\t}\n\t}\n\n\tif (!baseUrl) {\n\t\tthrow new Error('VITE_BASE_URL must be configured (or provide a valid request URL for runtime detection)');\n\t}\n\n\treturn { apiUrl, baseUrl };\n}\n\nfunction createUserLocals(decoded: DecodedToken): UserLocals {\n\treturn {\n\t\tisAuthenticated: true,\n\t\tid: decoded.sub,\n\t\temail: decoded.email,\n\t\troles: decoded.roles || []\n\t};\n}\n\nasync function tryRefreshToken(event: RequestEvent, config: SentinelConfig, refreshToken: string | undefined, log: SentinelLogger, verifier: JwksVerifier | null): Promise<void> {\n\tif (!refreshToken) return;\n\n\ttry {\n\t\tconst sentinel = new SentinelClient(config, event.fetch);\n\t\tconst result = await sentinel.refreshToken(refreshToken);\n\n\t\tif (result.code === 0 || result.success) {\n\t\t\tif (result.access_token) {\n\t\t\t\tevent.cookies.set(COOKIE_NAMES.ACCESS_TOKEN, result.access_token, COOKIE_OPTIONS.ACCESS_TOKEN);\n\n\t\t\t\t// Handle refresh token rotation\n\t\t\t\tif (result.refresh_token) {\n\t\t\t\t\tevent.cookies.set(COOKIE_NAMES.REFRESH_TOKEN, result.refresh_token, COOKIE_OPTIONS.REFRESH_TOKEN_LONG);\n\t\t\t\t}\n\n\t\t\t\t// Verify the freshly-issued token's signature before trusting its\n\t\t\t\t// payload (same JWKS path as the cookie flow). The cookie is kept —\n\t\t\t\t// it was just minted by the backend over TLS — but locals.user is\n\t\t\t\t// only set when verification passes; the next request retries.\n\t\t\t\tconst decoded = await resolveVerifiedToken(result.access_token, verifier, log);\n\t\t\t\tif (decoded) {\n\t\t\t\t\tconst locals = event.locals as SentinelLocals;\n\t\t\t\t\tlocals.user = createUserLocals(decoded);\n\t\t\t\t\tlocals.accessToken = result.access_token;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tlog.warn('Token refresh failed - continuing without user');\n\t\t}\n\t} catch (error) {\n\t\tlog.error('Token refresh error', { error: String(error) });\n\t}\n}\n\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sveltekit/handlers/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,eAAe,CAAC;AAElE,OAAO,KAAK,EACX,kBAAkB,EAGlB,MAAM,qBAAqB,CAAC;AA0hB7B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,kBAAuB,GAAG;IAAE,GAAG,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,CA8ClH"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sveltekit/handlers/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,eAAe,CAAC;AAElE,OAAO,KAAK,EACX,kBAAkB,EAGlB,MAAM,qBAAqB,CAAC;AAgiB7B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,kBAAuB,GAAG;IAAE,GAAG,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,CA8ClH"}
@@ -178,7 +178,13 @@ async function handlePoll(event, resolved) {
178
178
  ...loginCookieOptions,
179
179
  maxAge: 60 // 1 minute
180
180
  });
181
- return json({ status: 'completed', redirectTo: '/api/auth/claim' });
181
+ return json({
182
+ status: 'completed',
183
+ redirectTo: '/api/auth/claim',
184
+ ...(result.redirect_context != null
185
+ ? { redirect_context: result.redirect_context }
186
+ : {})
187
+ });
182
188
  }
183
189
  else if (result.status === 'pending') {
184
190
  return json({ status: 'pending' });