@eventuras/fides-auth 0.14.0 → 0.15.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.
Files changed (99) hide show
  1. package/README.md +225 -33
  2. package/dist/activity-tracker.d.ts +1 -1
  3. package/dist/activity-tracker.d.ts.map +1 -1
  4. package/dist/activity-tracker.js +1 -2
  5. package/dist/cookies.d.ts +6 -0
  6. package/dist/cookies.d.ts.map +1 -1
  7. package/dist/cookies.js +8 -3
  8. package/dist/cookies.js.map +1 -1
  9. package/dist/heartbeat.d.ts +1 -1
  10. package/dist/heartbeat.js +1 -2
  11. package/dist/id-token.d.ts +31 -0
  12. package/dist/id-token.d.ts.map +1 -0
  13. package/dist/id-token.js +60 -0
  14. package/dist/id-token.js.map +1 -0
  15. package/dist/index.js +3 -3
  16. package/dist/logger.js +1 -2
  17. package/dist/oauth-browser.d.ts +13 -10
  18. package/dist/oauth-browser.d.ts.map +1 -1
  19. package/dist/oauth-browser.js +4 -2
  20. package/dist/oauth-browser.js.map +1 -1
  21. package/dist/oauth-config.d.ts +70 -0
  22. package/dist/oauth-config.d.ts.map +1 -0
  23. package/dist/oauth-logging.d.ts +6 -5
  24. package/dist/oauth-logging.d.ts.map +1 -1
  25. package/dist/oauth-logging.js +5 -5
  26. package/dist/oauth-logging.js.map +1 -1
  27. package/dist/oauth.d.ts +48 -46
  28. package/dist/oauth.d.ts.map +1 -1
  29. package/dist/oauth.js +58 -29
  30. package/dist/oauth.js.map +1 -1
  31. package/dist/oidc-discovery.d.ts +13 -4
  32. package/dist/oidc-discovery.d.ts.map +1 -1
  33. package/dist/oidc-discovery.js +82 -15
  34. package/dist/oidc-discovery.js.map +1 -1
  35. package/dist/pkce.d.ts +10 -0
  36. package/dist/pkce.d.ts.map +1 -0
  37. package/dist/pkce.js +23 -0
  38. package/dist/pkce.js.map +1 -0
  39. package/dist/providers/vipps/client.d.ts +4 -3
  40. package/dist/providers/vipps/client.d.ts.map +1 -1
  41. package/dist/providers/vipps/index.js +10 -10
  42. package/dist/providers/vipps/index.js.map +1 -1
  43. package/dist/providers/vipps/types.d.ts +10 -10
  44. package/dist/providers/vipps/types.d.ts.map +1 -1
  45. package/dist/rate-limit.d.ts.map +1 -1
  46. package/dist/rate-limit.js +1 -2
  47. package/dist/server/authorized-fetch.d.ts +78 -0
  48. package/dist/server/authorized-fetch.d.ts.map +1 -0
  49. package/dist/server/cookie-store.d.ts +1 -1
  50. package/dist/server/heartbeat-handler.d.ts +7 -5
  51. package/dist/server/heartbeat-handler.d.ts.map +1 -1
  52. package/dist/server/index.d.ts +4 -0
  53. package/dist/server/index.d.ts.map +1 -1
  54. package/dist/server/index.js +744 -94
  55. package/dist/server/index.js.map +1 -1
  56. package/dist/server/login-transaction.d.ts +55 -0
  57. package/dist/server/login-transaction.d.ts.map +1 -0
  58. package/dist/server/oidc-callback.d.ts +14 -5
  59. package/dist/server/oidc-callback.d.ts.map +1 -1
  60. package/dist/server/oidc-login.d.ts +15 -6
  61. package/dist/server/oidc-login.d.ts.map +1 -1
  62. package/dist/server/oidc-logout.d.ts +7 -5
  63. package/dist/server/oidc-logout.d.ts.map +1 -1
  64. package/dist/server/session-persistence.d.ts +102 -0
  65. package/dist/server/session-persistence.d.ts.map +1 -0
  66. package/dist/server/session-store.d.ts +39 -0
  67. package/dist/server/session-store.d.ts.map +1 -0
  68. package/dist/server/session.d.ts +38 -31
  69. package/dist/server/session.d.ts.map +1 -1
  70. package/dist/session-cookies.d.ts +2 -1
  71. package/dist/session-cookies.d.ts.map +1 -1
  72. package/dist/session-cookies.js +4 -3
  73. package/dist/session-cookies.js.map +1 -1
  74. package/dist/session-events.d.ts +16 -9
  75. package/dist/session-events.d.ts.map +1 -1
  76. package/dist/session-events.js +4 -3
  77. package/dist/session-events.js.map +1 -1
  78. package/dist/session-refresh.d.ts +2 -2
  79. package/dist/session-refresh.d.ts.map +1 -1
  80. package/dist/session-refresh.js +1 -2
  81. package/dist/session-refresh.js.map +1 -1
  82. package/dist/session-validation.d.ts.map +1 -1
  83. package/dist/session-validation.js +9 -6
  84. package/dist/session-validation.js.map +1 -1
  85. package/dist/silent-login.d.ts +14 -5
  86. package/dist/silent-login.d.ts.map +1 -1
  87. package/dist/silent-login.js +3 -3
  88. package/dist/silent-login.js.map +1 -1
  89. package/dist/types.d.ts +4 -1
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/utils.d.ts +1 -15
  92. package/dist/utils.d.ts.map +1 -1
  93. package/dist/utils.js +2 -24
  94. package/dist/utils.js.map +1 -1
  95. package/docs/discovery-cache.md +7 -3
  96. package/docs/rp-initiated-logout.md +6 -3
  97. package/docs/server-sessions.md +154 -0
  98. package/docs/session-events.md +17 -16
  99. package/package.json +18 -10
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/server/session.ts","../../src/server/oidc-login.ts","../../src/server/oidc-callback.ts","../../src/server/oidc-logout.ts","../../src/server/heartbeat-handler.ts"],"sourcesContent":["// server/session.ts\n//\n// Framework-agnostic session persistence over a {@link CookieStore}. The split\n// encode/decode lives in `../session-cookies`; these helpers wire it to a cookie\n// store, apply the per-cookie size guard, and own the refresh flow — with no\n// dependency on any framework's cookie API.\n//\n// This is also where the session lifecycle events in `../session-events` are\n// emitted: it is the one layer that sees both the session and the bytes it\n// costs, so `session.created` / `session.refreshed` are raised from here rather\n// than from each caller.\n\nimport {\n ACCESS_TOKEN_COOKIE_NAME,\n COOKIE_INFO_BYTES,\n ID_TOKEN_COOKIE_NAME,\n SESSION_COOKIE_NAME,\n assertCookieWithinLimit,\n defaultSessionCookieOptions,\n} from '../cookies';\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { classifyRefreshFailure, getOAuthErrorLogContext } from '../oauth-logging';\nimport { refreshSession } from '../session-refresh';\nimport {\n SESSION_EVENT,\n logSessionEvent,\n type RefreshFailureCause,\n type SessionClearedTrigger,\n type SessionRejectedReason,\n} from '../session-events';\nimport {\n decodeIdTokenCookie,\n encodeSessionCookies,\n tryDecodeSessionCookies,\n} from '../session-cookies';\nimport type { CreateSessionOptions, Session } from '../types';\nimport type { CookieStore } from './cookie-store';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:session' });\n\ntype Secret = string | Uint8Array;\n\n/** Applies the browser size guard to one cookie value; logs as it nears the limit. */\nfunction checkSize(name: string, value: string): number {\n // Fail loudly above the browser per-cookie limit (the browser would otherwise\n // drop the cookie silently, producing a broken login).\n const size = assertCookieWithinLimit(name, value);\n if (size >= COOKIE_INFO_BYTES) {\n logger.info({ cookieName: name, size }, 'Cookie approaching browser size limit');\n }\n return size;\n}\n\n/** Seconds until the access token expires, for the `expiresIn` log field. */\nfunction expiresInSeconds(session: Session): number | undefined {\n const at = session.tokens?.accessTokenExpiresAt;\n if (!at) return undefined;\n const ms = Date.parse(at);\n return Number.isFinite(ms) ? Math.round((ms - Date.now()) / 1000) : undefined;\n}\n\n/** How {@link persistSession} should report what it just wrote. */\nexport interface PersistSessionOptions {\n /**\n * Which lifecycle event this write represents. Omit to write silently — for\n * callers that emit their own event, or that persist for reasons the session\n * vocabulary doesn't cover.\n */\n event?: typeof SESSION_EVENT.CREATED | typeof SESSION_EVENT.REFRESHED;\n /** Only meaningful for {@link SESSION_EVENT.REFRESHED}: did the provider issue a new refresh token? */\n rotatedRefreshToken?: boolean;\n}\n\n/**\n * Encrypts a session and writes it across the \"session\", \"session_at\" and\n * \"session_it\" cookies, clearing the split-out cookies the session has no token for.\n *\n * @returns The encrypted JWT stored in the main \"session\" cookie.\n */\nexport async function persistSession(\n store: CookieStore,\n session: Session,\n secret: Secret,\n options: PersistSessionOptions = {},\n): Promise<string> {\n const encoded = await encodeSessionCookies(session, secret);\n\n const values: Array<[string, string | undefined]> = [\n [SESSION_COOKIE_NAME, encoded.session],\n [ACCESS_TOKEN_COOKIE_NAME, encoded.accessToken],\n [ID_TOKEN_COOKIE_NAME, encoded.idToken],\n ];\n\n // Size-check everything before touching the store. Throwing part-way through\n // would leave one user's session cookie next to another user's tokens — the\n // caller sees an error while the browser holds a working, mixed-up session.\n let cookieBytes = 0;\n for (const [name, value] of values) {\n if (value) cookieBytes += checkSize(name, value);\n }\n\n for (const [name, value] of values) {\n if (value) {\n await store.set(name, value, defaultSessionCookieOptions);\n } else {\n await store.delete(name);\n }\n }\n\n if (options.event === SESSION_EVENT.CREATED) {\n logSessionEvent(logger, {\n event: SESSION_EVENT.CREATED,\n sid: session.sid,\n hasRefreshToken: !!session.tokens?.refreshToken,\n scopes: session.scopes,\n expiresIn: expiresInSeconds(session),\n cookieBytes,\n });\n } else if (options.event === SESSION_EVENT.REFRESHED) {\n logSessionEvent(logger, {\n event: SESSION_EVENT.REFRESHED,\n sid: session.sid,\n expiresIn: expiresInSeconds(session),\n rotatedRefreshToken: options.rotatedRefreshToken ?? false,\n cookieBytes,\n });\n }\n\n return encoded.session;\n}\n\n/** Outcome of reading the session from the cookie store. */\nexport type ReadSessionResult =\n | { session: Session; reason?: undefined }\n | { session: null; reason: SessionRejectedReason };\n\n/**\n * Reads and reassembles the current session, reporting why when there isn't one.\n *\n * The reason comes from the package-wide {@link SessionRejectedReason}\n * vocabulary, so a proxy or status route can log the same word the heartbeat\n * endpoint logs. Does not log on its own — the caller owns the request context\n * and decides whether a missing session is worth a line.\n */\nexport async function tryReadSession(store: CookieStore, secret: Secret): Promise<ReadSessionResult> {\n try {\n return await tryDecodeSessionCookies(\n {\n session: (await store.get(SESSION_COOKIE_NAME)) ?? null,\n accessToken: (await store.get(ACCESS_TOKEN_COOKIE_NAME)) ?? null,\n idToken: (await store.get(ID_TOKEN_COOKIE_NAME)) ?? null,\n },\n secret,\n );\n } catch (error) {\n // Worker thread errors (e.g. from the crypto worker) should not crash the app.\n if (error instanceof Error && error.message.includes('worker')) {\n logger.error({ error }, 'Worker thread error reading session');\n } else {\n logger.error({ error }, 'Unexpected error reading session');\n }\n return { session: null, reason: 'unreadable_session' };\n }\n}\n\n/**\n * Reads and reassembles the current session from the cookie store, or null when\n * there is none. Decode/validation rules live in `decodeSessionCookies`;\n * the returned session may carry an expired access token.\n *\n * Prefer {@link tryReadSession} when you want to log *why* there is no session.\n */\nexport async function readSession(store: CookieStore, secret: Secret): Promise<Session | null> {\n return (await tryReadSession(store, secret)).session;\n}\n\n/** Outcome of a refresh attempt. */\nexport type RefreshSessionResult =\n | { ok: true; session: Session; rotatedRefreshToken: boolean }\n | {\n ok: false;\n reason: SessionRejectedReason;\n /**\n * Correlation id of the session that failed, when we got far enough to read\n * one. The line that *ends* a session is the one most worth correlating.\n */\n sid?: string;\n /**\n * Present only when a refresh was actually attempted, i.e. when `reason` is\n * `refresh_failed`. Absent means we never got as far as talking to the\n * provider, so there is no provider verdict to report.\n */\n cause?: RefreshFailureCause;\n };\n\n/**\n * Refreshes the stored session and persists the result, reporting why on failure.\n *\n * The `cause` is the point of this function: `refreshSessionInStore` collapses\n * \"the provider says this refresh token is dead\" and \"we could not reach the\n * provider\" into the same `null`, and a caller that logs the user out on both\n * ends sessions that were never actually invalid. Only `invalid_grant` is\n * terminal — see {@link classifyRefreshFailure}.\n */\nexport async function tryRefreshSessionInStore(\n store: CookieStore,\n config: OAuthConfig,\n secret: Secret,\n options: CreateSessionOptions = {},\n): Promise<RefreshSessionResult> {\n // readSession also returns sessions with expired access tokens — exactly\n // what refresh is for.\n const read = await tryReadSession(store, secret);\n const current = read.session;\n\n if (!current) {\n return { ok: false, reason: read.reason };\n }\n if (!current.tokens?.refreshToken) {\n return { ok: false, reason: 'no_refresh_token', sid: current.sid };\n }\n\n try {\n const updated = await refreshSession(current, config, options);\n if (!updated) {\n // refreshSession resolves to a session or throws; a null here means the\n // provider answered with something we could not build a session from.\n logSessionEvent(logger, {\n event: SESSION_EVENT.REFRESH_FAILED,\n sid: current.sid,\n cause: 'idp_error',\n accessTokenExpiresAt: current.tokens.accessTokenExpiresAt,\n });\n return { ok: false, reason: 'refresh_failed', cause: 'idp_error', sid: current.sid };\n }\n\n const rotatedRefreshToken = updated.tokens?.refreshToken !== current.tokens.refreshToken;\n await persistSession(store, updated, secret, {\n event: SESSION_EVENT.REFRESHED,\n rotatedRefreshToken,\n });\n\n return { ok: true, session: updated, rotatedRefreshToken };\n } catch (error) {\n const cause = classifyRefreshFailure(error);\n logSessionEvent(logger, {\n event: SESSION_EVENT.REFRESH_FAILED,\n sid: current.sid,\n cause,\n status: getOAuthErrorLogContext(error).status,\n accessTokenExpiresAt: current.tokens.accessTokenExpiresAt,\n error: getOAuthErrorLogContext(error),\n });\n return { ok: false, reason: 'refresh_failed', cause, sid: current.sid };\n }\n}\n\n/**\n * Refreshes the stored session using its refresh token and persists the result.\n * Returns the updated session, or null when there is nothing to refresh or the\n * refresh token is no longer valid.\n *\n * Prefer {@link tryRefreshSessionInStore}: this signature cannot tell a dead\n * refresh token apart from an unreachable provider, and treating the two alike\n * logs out users whose sessions are still valid.\n */\nexport async function refreshSessionInStore(\n store: CookieStore,\n config: OAuthConfig,\n secret: Secret,\n options: CreateSessionOptions = {},\n): Promise<Session | null> {\n const result = await tryRefreshSessionInStore(store, config, secret, options);\n return result.ok ? result.session : null;\n}\n\n/**\n * Reads the raw ID token independently of the session cookies — logout needs\n * the hint even when {@link readSession} returns null.\n */\nexport async function readIdToken(store: CookieStore, secret: Secret): Promise<string | undefined> {\n try {\n const raw = await store.get(ID_TOKEN_COOKIE_NAME);\n return raw ? await decodeIdTokenCookie(raw, secret) : undefined;\n } catch (error) {\n logger.error({ error }, 'Unexpected error reading ID token');\n return undefined;\n }\n}\n\n/** Context for the {@link SESSION_EVENT.CLEARED} event raised by {@link clearSession}. */\nexport interface ClearSessionOptions {\n /** Why the cookies are going away. Omit to clear without raising an event. */\n trigger?: SessionClearedTrigger;\n /** Correlation id of the session being cleared, when the caller has it. */\n sid?: string;\n}\n\n/** Deletes all session cookies. */\nexport async function clearSession(\n store: CookieStore,\n options: ClearSessionOptions = {},\n): Promise<void> {\n await store.delete(SESSION_COOKIE_NAME);\n await store.delete(ACCESS_TOKEN_COOKIE_NAME);\n await store.delete(ID_TOKEN_COOKIE_NAME);\n\n if (options.trigger) {\n logSessionEvent(logger, {\n event: SESSION_EVENT.CLEARED,\n sid: options.sid,\n trigger: options.trigger,\n });\n }\n}\n","// server/oidc-login.ts\n//\n// Framework-agnostic OIDC login initiation. Builds PKCE parameters, stores them\n// in the cookie store, and redirects to the provider's authorization endpoint.\n// Uses the standard Request/Response, a CookieStore, and an optional rate-limit\n// callback, so any server runtime can call it.\n\nimport { defaultOAuthCookieOptions } from '../cookies';\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { buildPKCEOptions, discoverAndBuildAuthorizationUrl } from '../oauth';\nimport type { CookieStore } from './cookie-store';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-login' });\n\nexport interface OidcLoginConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store used to persist the PKCE state. */\n cookies: CookieStore;\n\n /**\n * Optional rate-limit gate. When it resolves false, the handler responds 429.\n */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /**\n * Validates and sanitizes the returnTo parameter, returning a safe path or null.\n * @default Only allows relative paths starting with a single `/`.\n */\n validateReturnTo?: (rawReturnTo: string) => string | null;\n}\n\nconst defaultValidateReturnTo = (raw: string): string | null =>\n /^\\/(?!\\/)/.test(raw) ? raw : null;\n\n/**\n * Handles OIDC login initiation: rate-limit, build PKCE, persist it, and redirect\n * to the authorization endpoint.\n */\nexport async function handleOidcLogin(\n request: Request,\n config: OidcLoginConfig,\n): Promise<Response> {\n const { oauthConfig, cookies, rateLimit, validateReturnTo = defaultValidateReturnTo } = config;\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n const url = new URL(request.url);\n const rawReturnTo = url.searchParams.get('returnTo');\n const returnTo = rawReturnTo ? validateReturnTo(rawReturnTo) : null;\n\n const pkce = await buildPKCEOptions(oauthConfig);\n const authorizationUrl = await discoverAndBuildAuthorizationUrl(oauthConfig, pkce);\n\n logger.info('Redirecting to OIDC provider for login');\n\n await cookies.set('oauth_state', pkce.state, defaultOAuthCookieOptions);\n await cookies.set('oauth_code_verifier', pkce.code_verifier, defaultOAuthCookieOptions);\n if (returnTo) {\n await cookies.set('returnTo', returnTo, defaultOAuthCookieOptions);\n } else {\n // Clear any stale returnTo from an earlier, abandoned login so this one's\n // post-login redirect is deterministic rather than inheriting a past path.\n await cookies.delete('returnTo');\n }\n\n return new Response(null, {\n status: 302,\n headers: { Location: authorizationUrl.toString() },\n });\n}\n","// server/oidc-callback.ts\n//\n// Framework-agnostic OIDC callback handler. Orchestrates the OIDC concerns —\n// rate limit, PKCE cookie read, code exchange, session persistence, safe redirect\n// — over the standard Request/Response and a CookieStore, delegating the protocol\n// to the core oauth module and persistence to the server session helpers.\n\nimport { CookieTooLargeError } from '../cookies';\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport {\n buildSessionFromTokens,\n exchangeAuthorizationCode,\n validateReturnUrl,\n} from '../oauth';\nimport { SESSION_EVENT } from '../session-events';\nimport { getSessionSecret } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { persistSession } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-callback' });\n\nexport interface OidcCallbackConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Public application URL (used to reconstruct the callback URL and validate redirects). */\n applicationUrl: string;\n\n /** Cookie store holding the PKCE state and receiving the session cookies. */\n cookies: CookieStore;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Session encryption secret. Defaults to {@link getSessionSecret}. */\n secret?: string | Uint8Array;\n\n /**\n * Name of the ID token claim that contains user roles.\n * @default 'roles'\n */\n rolesClaim?: string;\n\n /**\n * Default path to redirect to after login if no returnTo cookie is set.\n * @default '/'\n */\n defaultRedirectPath?: string;\n\n /**\n * Optional validator for the returnTo path. If omitted, only same-origin paths\n * are allowed.\n */\n validateReturnTo?: (path: string) => boolean;\n}\n\n/**\n * Handles the OIDC callback: rate-limit, exchange the code for tokens, persist the\n * session across the cookie store, and redirect to a validated returnTo.\n */\nexport async function handleOidcCallback(\n request: Request,\n config: OidcCallbackConfig,\n): Promise<Response> {\n const {\n oauthConfig,\n applicationUrl,\n cookies,\n rateLimit,\n secret = getSessionSecret(),\n rolesClaim = 'roles',\n defaultRedirectPath = '/',\n validateReturnTo,\n } = config;\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n try {\n // Reconstruct the public callback URL. Behind a TLS-terminating proxy\n // applicationUrl is just the origin (\"/\"), and the token-exchange redirect_uri\n // must match the callback path used at authorize — so keep the request path.\n const currentUrl = new URL(request.url);\n const publicUrl = new URL(applicationUrl);\n publicUrl.pathname = currentUrl.pathname;\n publicUrl.search = currentUrl.search;\n\n const storedState = await cookies.get('oauth_state');\n const storedCodeVerifier = await cookies.get('oauth_code_verifier');\n\n if (!storedState || !storedCodeVerifier) {\n logger.warn('Missing state or code verifier');\n return new Response('Please restart the login process.', { status: 400 });\n }\n\n const tokens = await exchangeAuthorizationCode(\n oauthConfig,\n publicUrl,\n storedCodeVerifier,\n storedState,\n );\n\n // Build the session and persist it (split across session/session_at).\n // persistSession raises session.created — it is the only layer that knows\n // what the session cost in cookie bytes, and a login that comes back without\n // a refresh token (no offline_access) is visible there on day one.\n const session = buildSessionFromTokens(tokens, rolesClaim);\n await persistSession(cookies, session, secret, { event: SESSION_EVENT.CREATED });\n\n // Clean up PKCE & returnTo cookies (read returnTo first).\n const returnTo = await cookies.get('returnTo');\n await cookies.delete('oauth_state');\n await cookies.delete('oauth_code_verifier');\n await cookies.delete('returnTo');\n\n const redirectUrl = validateReturnUrl(\n returnTo ?? null,\n applicationUrl,\n defaultRedirectPath,\n validateReturnTo,\n );\n redirectUrl.searchParams.set('login', 'success');\n\n logger.info(\n { sid: session.sid, redirectUrl: redirectUrl.toString() },\n 'Login successful, redirecting',\n );\n\n return new Response(null, {\n status: 302,\n headers: { Location: redirectUrl.toString() },\n });\n } catch (error) {\n // The session was too large for a cookie — surface this distinctly instead of\n // hiding it in a generic 500, so it's diagnosable in production.\n if (error instanceof CookieTooLargeError) {\n logger.error(\n { error, cookieName: error.cookieName, size: error.size },\n 'Session cookie too large to store',\n );\n return new Response('The session is too large to store. Please contact support.', {\n status: 431,\n });\n }\n\n logger.error({ error }, 'OIDC callback error');\n return new Response('An unexpected error occurred. Please restart the login process.', {\n status: 500,\n });\n }\n}\n","// server/oidc-logout.ts\n//\n// Framework-agnostic RP-initiated logout. Clearing only the local session cookie\n// leaves the provider's session alive, so the next /authorize silently\n// re-authenticates — this handler ends both.\n\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { buildOidcLogoutUrl } from '../oauth';\nimport { getSessionSecret } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { clearSession, readIdToken, tryReadSession } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-logout' });\n\n/**\n * Whether the request came from the application itself. POST alone is no defence:\n * a third-party page can auto-submit a form at this endpoint, and the response\n * clears the cookies whether or not the browser attached them.\n *\n * `Sec-Fetch-Site` is authoritative where the browser sends it. Otherwise fall back\n * to `Origin`. A request with neither is not a browser form post, so it is allowed.\n */\nfunction isSameOrigin(request: Request, applicationUrl?: string): boolean {\n const site = request.headers.get('sec-fetch-site');\n if (site) {\n return site === 'same-origin' || site === 'none';\n }\n\n const origin = request.headers.get('origin');\n if (!origin) return true;\n\n try {\n return new URL(origin).origin === new URL(applicationUrl ?? request.url).origin;\n } catch {\n return false;\n }\n}\n\nexport interface OidcLogoutConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store holding the session cookies to read and then clear. */\n cookies: CookieStore;\n\n /** Post-logout redirect URI. Must be registered with the provider. */\n postLogoutRedirectUri: string;\n\n /** Session encryption secret. Defaults to {@link getSessionSecret}. */\n secret?: string | Uint8Array;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Passed through as `state`; verifying it on the way back is the caller's job. */\n state?: string;\n\n /** `logout_hint`, when the provider documents one. */\n logoutHint?: string;\n\n /** Whether to send `client_id`. Default true — see `OidcLogoutOptions`. */\n includeClientId?: boolean;\n\n /**\n * Request methods that may trigger a logout. Defaults to POST only: on GET, any\n * third-party page can force a logout by navigating the browser.\n * @default ['POST']\n */\n allowedMethods?: string[];\n\n /**\n * Public application URL, used to check the `Origin` header behind a proxy where\n * the request URL carries the internal origin. Defaults to the request's own\n * origin.\n */\n applicationUrl?: string;\n\n /**\n * Where to send the user when logout is local-only (no end_session_endpoint).\n * @default postLogoutRedirectUri\n */\n fallbackRedirectUri?: string;\n}\n\n/**\n * Clears the session cookies and redirects to the provider's end-session endpoint\n * with `id_token_hint`. Local cookies are cleared even when the provider hop is\n * unavailable, so the user is always logged out of this application.\n */\nexport async function handleOidcLogout(\n request: Request,\n config: OidcLogoutConfig,\n): Promise<Response> {\n const {\n oauthConfig,\n cookies,\n postLogoutRedirectUri,\n secret = getSessionSecret(),\n rateLimit,\n state,\n logoutHint,\n includeClientId,\n fallbackRedirectUri = postLogoutRedirectUri,\n allowedMethods = ['POST'],\n applicationUrl,\n } = config;\n\n if (!allowedMethods.includes(request.method)) {\n return new Response(null, {\n status: 405,\n headers: { Allow: allowedMethods.join(', ') },\n });\n }\n\n if (!isSameOrigin(request, applicationUrl)) {\n logger.warn(\n { origin: request.headers.get('origin'), site: request.headers.get('sec-fetch-site') },\n 'Cross-origin logout request rejected',\n );\n return new Response('Forbidden', { status: 403 });\n }\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n // Read before clearing. Deliberately not readSession: that returns null once the\n // access token has expired, which is exactly when the hint still matters.\n const idTokenHint = await readIdToken(cookies, secret);\n\n // Best-effort correlation id, so the logout line joins the rest of this\n // session's events. A session too broken to read still logs out fine.\n const sid = (await tryReadSession(cookies, secret)).session?.sid;\n\n try {\n await clearSession(cookies, { trigger: 'logout', sid });\n } catch (error) {\n // Never redirect to the provider on a half-cleared local session — the user\n // would come back looking logged in.\n logger.error({ error }, 'Failed to clear session cookies');\n return new Response('Logout failed', { status: 500 });\n }\n\n const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {\n postLogoutRedirectUri,\n idTokenHint,\n state,\n logoutHint,\n includeClientId,\n });\n\n if (!logoutUrl) {\n // Either the provider advertises no end_session_endpoint or discovery failed;\n // buildOidcLogoutUrl logs which. Local logout stands either way.\n logger.info(\n { issuer: oauthConfig.issuer },\n 'No logout URL — local session cleared, provider session may persist',\n );\n return new Response(null, { status: 302, headers: { Location: fallbackRedirectUri } });\n }\n\n logger.info({ hasIdTokenHint: !!idTokenHint }, 'Redirecting to provider for logout');\n\n return new Response(null, { status: 302, headers: { Location: logoutUrl.toString() } });\n}\n","// server/heartbeat-handler.ts\n//\n// Framework-agnostic heartbeat endpoint. Pairs with the client-side heartbeat:\n// when the user is active the client POSTs here, and this handler exchanges the\n// current refresh token for a fresh access token. Glue only — rate limit, load\n// session, refresh, respond — over the standard Request/Response and a CookieStore.\n\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { SESSION_EVENT, logSessionEvent, type SessionRejectedReason } from '../session-events';\nimport { getSessionSecret } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { tryRefreshSessionInStore } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:heartbeat' });\n\nexport interface HeartbeatHandlerConfig {\n /** OAuth/OIDC configuration used to refresh the access token. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store holding the session. */\n cookies: CookieStore;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Session encryption secret. Defaults to {@link getSessionSecret}. */\n secret?: string | Uint8Array;\n}\n\n/**\n * Handles a heartbeat request — refresh the active session if there is one.\n *\n * Returns 200 with `{ accessTokenExpiresAt }` on success, 401 with `{ reason }`\n * when the session is genuinely over, 503 when the provider could not be\n * reached, 405 for non-POST methods, and 429 when rate-limited.\n *\n * The 401/503 split matters: a 401 tells the client to log the user out, so it\n * must be reserved for the cases where the session really is finished. A network\n * blip or a provider 5xx says nothing about the session's validity — answering\n * 401 there logs out users whose sessions were fine, which is precisely the\n * \"logged out every few minutes\" symptom. The client already retries non-401\n * failures with backoff.\n */\nexport async function handleHeartbeat(\n request: Request,\n config: HeartbeatHandlerConfig,\n): Promise<Response> {\n const { oauthConfig, cookies, rateLimit, secret = getSessionSecret() } = config;\n\n if (request.method !== 'POST') {\n return new Response(null, { status: 405, headers: { Allow: 'POST' } });\n }\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Heartbeat rate limit exceeded');\n return new Response(null, { status: 429 });\n }\n\n // An expired access token is what the heartbeat recovers from, not 401 on.\n // tryRefreshSessionInStore reads the session, refreshes it, and reports which\n // of the two it failed at — so this handler never has to re-read cookies to\n // work out why.\n const result = await tryRefreshSessionInStore(cookies, oauthConfig, secret);\n\n if (result.ok) {\n logger.debug({ sid: result.session.sid }, 'Heartbeat refresh succeeded');\n return Response.json(\n { accessTokenExpiresAt: result.session.tokens?.accessTokenExpiresAt ?? null },\n {\n // Auth/session endpoint — must never be cached by the browser or any\n // intermediary, or a stale 200 could fool the client into thinking a refresh\n // succeeded without actually hitting the server.\n headers: { 'Cache-Control': 'private, no-store' },\n },\n );\n }\n\n // The provider was unreachable or broken. The session is probably still fine,\n // so keep it and let the client retry rather than ending it.\n if (result.cause === 'transport' || result.cause === 'idp_error') {\n logger.warn(\n { sid: result.sid, cause: result.cause },\n 'Heartbeat refresh could not be completed — keeping the session',\n );\n return new Response(null, { status: 503, headers: { 'Cache-Control': 'private, no-store' } });\n }\n\n // Everything left is a session that genuinely cannot continue.\n logSessionEvent(logger, {\n event: SESSION_EVENT.REJECTED,\n sid: result.sid,\n reason: result.reason,\n source: 'heartbeat',\n });\n\n return unauthorized(result.reason);\n}\n\n/**\n * 401 carrying the reason in the body.\n *\n * The browser is where the logout becomes visible, so letting the client name\n * the reason lets a consumer beacon it back with the same vocabulary the server\n * used. The values are session state, not secrets — and clients that ignore the\n * body are unaffected.\n */\nfunction unauthorized(reason: SessionRejectedReason): Response {\n return Response.json(\n { reason },\n { status: 401, headers: { 'Cache-Control': 'private, no-store' } },\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["logger","logger","logger","logger","logger","logger","logger"],"sources":["../../src/server/session-store.ts","../../src/server/session-persistence.ts","../../src/server/session.ts","../../src/server/login-transaction.ts","../../src/server/authorized-fetch.ts","../../src/server/oidc-login.ts","../../src/server/oidc-callback.ts","../../src/server/oidc-logout.ts","../../src/server/heartbeat-handler.ts"],"sourcesContent":["// server/session-store.ts\n//\n// The store contract behind server persistence: the session lives here, keyed\n// by a hash of the handle the browser holds. Database adapters (Mongo, Redis, …)\n// belong in consumers; this package ships only the in-memory one.\n\n/** One persisted session, as the store sees it. */\nexport interface StoredSession {\n /** The session, encrypted under the session secret. Opaque to the store. */\n data: string;\n /**\n * Absolute session expiry, ISO 8601. Adapters map it to a TTL; the library\n * also checks it on read, so a late TTL sweep never revives a session.\n */\n expiresAt: string;\n /** Correlation id of the session, so a record can be found from a log line. Not a secret. */\n sid?: string;\n}\n\n/**\n * Server-side session storage. Methods may be sync or async.\n *\n * Keys are hex SHA-256 digests of the handle, never the handle itself, so a\n * store dump or a logged key cannot be replayed as a cookie.\n */\nexport interface SessionStore {\n /** Returns the record, or null/undefined when there is none. May return an expired record. */\n get(key: string): StoredSession | null | undefined | Promise<StoredSession | null | undefined>;\n /** Creates or replaces the record. */\n set(key: string, record: StoredSession): void | Promise<void>;\n /** Removes the record. Deleting a missing key is not an error. */\n delete(key: string): void | Promise<void>;\n /**\n * Optional: runs `fn` while holding a lock on `key`, so refreshes of one\n * session are serialized across instances. Without it they are serialized\n * within the process only. Implementations should expire the lock if the\n * holder dies.\n */\n withLock?<T>(key: string, fn: () => Promise<T>): Promise<T>;\n}\n\n/**\n * In-memory store for tests and single-instance development. Sessions do not\n * survive a restart, and expired records are dropped on read.\n */\nexport function createMemorySessionStore(): SessionStore {\n const records = new Map<string, StoredSession>();\n const locks = new Map<string, Promise<void>>();\n\n return {\n get(key) {\n const record = records.get(key);\n if (!record) return null;\n if (!(Date.parse(record.expiresAt) > Date.now())) {\n records.delete(key);\n return null;\n }\n return record;\n },\n set(key, record) {\n records.set(key, record);\n },\n delete(key) {\n records.delete(key);\n },\n withLock(key, fn) {\n // One promise chain per key. The chain never rejects, so a failed holder\n // does not poison the next.\n const previous = locks.get(key) ?? Promise.resolve();\n const run = previous.then(fn);\n const settled = run.then(() => undefined, () => undefined);\n locks.set(key, settled);\n void settled.then(() => {\n if (locks.get(key) === settled) locks.delete(key);\n });\n return run;\n },\n };\n}\n","// server/session-persistence.ts\n//\n// Where a session lives between requests. Cookie persistence — the default —\n// encrypts it into the three split cookies. Server persistence keeps it in a\n// SessionStore and gives the browser only an opaque handle: for providers whose\n// tokens don't fit a cookie, and for the revocation and refresh serialization\n// a cookie cannot offer. `session.ts` is written against this contract and owns\n// the lifecycle events and the refresh flow.\n\nimport {\n ACCESS_TOKEN_COOKIE_NAME,\n COOKIE_INFO_BYTES,\n ID_TOKEN_COOKIE_NAME,\n SESSION_COOKIE_NAME,\n SESSION_REF_COOKIE_NAME,\n assertCookieWithinLimit,\n defaultSessionCookieOptions,\n} from '../cookies';\nimport { createLogger } from '../logger';\nimport {\n decodeIdTokenCookie,\n encodeSessionCookies,\n tryDecodeSessionCookies,\n} from '../session-cookies';\nimport type { SessionPersistenceMode, SessionRejectedReason } from '../session-events';\nimport { isSession } from '../session-validation';\nimport type { Session } from '../types';\nimport { createEncryptedJWT, decryptJWT, generateToken, sha256 } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport type { SessionStore } from './session-store';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:session-persistence' });\n\n/** A session encryption key: 32 bytes for A256GCM, as hex or raw bytes. */\nexport type SessionSecret = string | Uint8Array;\n\n/** A missing or malformed session secret: a configuration error, never a bad cookie. */\nexport class SessionSecretError extends TypeError {\n override name = 'SessionSecretError';\n}\n\n/** Throws unless `secret` is the 32 bytes A256GCM needs. Never echoes the secret. */\nexport function assertSessionSecret(secret: SessionSecret): void {\n if (!secret || secret.length === 0) {\n throw new SessionSecretError('fides-auth: session secret is missing or empty');\n }\n const valid =\n secret instanceof Uint8Array\n ? secret.length === 32\n : typeof secret === 'string' && /^[0-9a-fA-F]{64}$/.test(secret);\n if (!valid) {\n throw new SessionSecretError(\n 'fides-auth: session secret must be 32 bytes (64 hex characters or a Uint8Array)',\n );\n }\n}\n\n/** Where a handler finds the session: the cookies under `secret`, or `persistence`. One is required. */\nexport type SessionPersistenceConfig =\n | {\n /** Session encryption secret for the default cookie persistence. */\n secret: SessionSecret;\n /** Where the session lives; wins over `secret`. See {@link serverPersistence}. */\n persistence?: SessionPersistence;\n }\n | { secret?: undefined; persistence: SessionPersistence };\n\n/** Outcome of reading the session: a session, or why there isn't one. */\nexport type ReadSessionResult =\n | { session: Session; reason?: undefined }\n | { session: null; reason: SessionRejectedReason };\n\nexport interface WriteSessionOptions {\n /**\n * True when the write continues the session the request arrived with (a\n * refresh). Otherwise the write starts a new session — under server\n * persistence that mints a new handle, which is what defeats session\n * fixation on login.\n */\n continuation?: boolean;\n}\n\nexport interface WriteSessionResult {\n /** The value now in the main session cookie: the encrypted session, or the handle. */\n cookieValue: string;\n /** Bytes written across the session cookies. Absent when the session is not in cookies. */\n cookieBytes?: number;\n}\n\n/**\n * The persistence contract `session.ts` works against. Get one from\n * {@link cookiePersistence} or {@link serverPersistence}; the methods are the\n * library's own seam, not an extension point.\n */\nexport interface SessionPersistence {\n readonly mode: SessionPersistenceMode;\n write(\n cookies: CookieStore,\n session: Session,\n options?: WriteSessionOptions,\n ): Promise<WriteSessionResult>;\n read(cookies: CookieStore): Promise<ReadSessionResult>;\n /** The raw ID token whether or not the session is usable — logout needs it most when it isn't. */\n readIdToken(cookies: CookieStore): Promise<string | undefined>;\n clear(cookies: CookieStore): Promise<void>;\n /**\n * Runs `fn` with other refreshes of the same session held off. Concurrent\n * callers in this process share one result; across instances the store's\n * `withLock` serializes them. Present only where requests share state\n * server-side — in cookie mode every request holds its own copy and there\n * is nothing to coordinate.\n */\n exclusive?<T>(cookies: CookieStore, fn: () => Promise<T>): Promise<T>;\n}\n\n/** Accepts the pre-0.15 bare secret wherever a persistence is expected. */\nexport function resolvePersistence(value: SessionSecret | SessionPersistence): SessionPersistence {\n // Anything but a persistence is a secret, so an unset one reaches the check.\n return typeof value === 'object' && value !== null && !(value instanceof Uint8Array)\n ? value\n : cookiePersistence(value);\n}\n\n/** The configured `persistence`, else the bare `secret`. */\nexport function persistenceFrom(config: SessionPersistenceConfig): SessionSecret | SessionPersistence {\n if (config.secret === undefined) return config.persistence;\n return config.persistence ?? config.secret;\n}\n\nconst SPLIT_COOKIE_NAMES = [SESSION_COOKIE_NAME, ACCESS_TOKEN_COOKIE_NAME, ID_TOKEN_COOKIE_NAME];\n\n/** Deletes the `session`, `session_at` and `session_it` cookies. */\nexport async function clearSessionCookies(cookies: CookieStore): Promise<void> {\n for (const name of SPLIT_COOKIE_NAMES) {\n await cookies.delete(name);\n }\n}\n\n/** Applies the browser size guard to one cookie value; logs as it nears the limit. */\nfunction checkSize(name: string, value: string): number {\n // Fail loudly above the browser per-cookie limit (the browser would otherwise\n // drop the cookie silently, producing a broken login).\n const size = assertCookieWithinLimit(name, value);\n if (size >= COOKIE_INFO_BYTES) {\n logger.info({ cookieName: name, size }, 'Cookie approaching browser size limit');\n }\n return size;\n}\n\n/**\n * The default: the session encrypted into the `session`, `session_at` and\n * `session_it` cookies.\n */\nexport function cookiePersistence(secret: SessionSecret): SessionPersistence {\n // Checked on use, so a module may build this at import time without the secret.\n return {\n mode: 'cookie',\n\n async write(cookies, session) {\n assertSessionSecret(secret);\n const encoded = await encodeSessionCookies(session, secret);\n const values: Array<[string, string | undefined]> = [\n [SESSION_COOKIE_NAME, encoded.session],\n [ACCESS_TOKEN_COOKIE_NAME, encoded.accessToken],\n [ID_TOKEN_COOKIE_NAME, encoded.idToken],\n ];\n\n // Size-check everything before touching the store. Throwing part-way\n // through would leave one user's session cookie next to another user's\n // tokens — the caller sees an error while the browser holds a working,\n // mixed-up session.\n let cookieBytes = 0;\n for (const [name, value] of values) {\n if (value) cookieBytes += checkSize(name, value);\n }\n\n for (const [name, value] of values) {\n if (value) {\n await cookies.set(name, value, defaultSessionCookieOptions);\n } else {\n await cookies.delete(name);\n }\n }\n\n return { cookieValue: encoded.session, cookieBytes };\n },\n\n async read(cookies) {\n assertSessionSecret(secret);\n return tryDecodeSessionCookies(\n {\n session: (await cookies.get(SESSION_COOKIE_NAME)) ?? null,\n accessToken: (await cookies.get(ACCESS_TOKEN_COOKIE_NAME)) ?? null,\n idToken: (await cookies.get(ID_TOKEN_COOKIE_NAME)) ?? null,\n },\n secret,\n );\n },\n\n async readIdToken(cookies) {\n assertSessionSecret(secret);\n const raw = await cookies.get(ID_TOKEN_COOKIE_NAME);\n return raw ? decodeIdTokenCookie(raw, secret) : undefined;\n },\n\n async clear(cookies) {\n assertSessionSecret(secret);\n await clearSessionCookies(cookies);\n },\n };\n}\n\nexport interface ServerPersistenceOptions {\n /** Where sessions live. See {@link SessionStore}. */\n store: SessionStore;\n /** Encrypts each record at rest, so a store dump alone yields no tokens. */\n secret: SessionSecret;\n /**\n * Name of the cookie carrying the handle.\n * @default 'session_ref'\n */\n cookieName?: string;\n /**\n * Absolute session lifetime: the record and the handle cookie expire this\n * long after login, refreshes notwithstanding.\n * @default 30\n */\n sessionDurationDays?: number;\n}\n\n/**\n * The session in a {@link SessionStore}; the browser holds only a random\n * 256-bit handle. The store is keyed by `SHA-256(handle)`, so neither a store\n * dump nor a logged key can be replayed as a cookie, and the record is\n * encrypted under `secret`, so a dump alone yields no tokens.\n */\nexport function serverPersistence(options: ServerPersistenceOptions): SessionPersistence {\n const {\n store,\n secret,\n cookieName = SESSION_REF_COOKIE_NAME,\n sessionDurationDays = 30,\n } = options;\n // Checked on use, so a module may build this at import time without the secret.\n const maxAge = sessionDurationDays * 24 * 60 * 60;\n const inflight = new Map<string, Promise<unknown>>();\n\n /** The handle the request carries and the store key it maps to. */\n async function locate(cookies: CookieStore): Promise<{ handle: string; key: string } | null> {\n const handle = await cookies.get(cookieName);\n return handle ? { handle, key: await sha256(handle) } : null;\n }\n\n /** Drops split cookies left behind by cookie persistence, when there are any. */\n async function clearSplitCookies(cookies: CookieStore): Promise<void> {\n for (const name of SPLIT_COOKIE_NAMES) {\n if (await cookies.get(name)) await cookies.delete(name);\n }\n }\n\n async function readRecord(key: string): Promise<ReadSessionResult> {\n const record = await store.get(key);\n if (!record) {\n return { session: null, reason: 'session_not_found' };\n }\n // The adapter's TTL is a convenience; the expiry is enforced here.\n if (!(Date.parse(record.expiresAt) > Date.now())) {\n await store.delete(key);\n return { session: null, reason: 'session_not_found' };\n }\n\n let payload: unknown;\n try {\n payload = await decryptJWT(record.data, secret);\n } catch (error) {\n logger.warn({ error, sid: record.sid }, 'Stored session would not decrypt');\n return { session: null, reason: 'unreadable_session' };\n }\n if (!isSession(payload)) {\n logger.warn({ sid: record.sid }, 'Stored session has the wrong shape');\n return { session: null, reason: 'unreadable_session' };\n }\n return { session: payload };\n }\n\n return {\n mode: 'server',\n\n async write(cookies, session, { continuation = false } = {}) {\n assertSessionSecret(secret);\n // Same rule as on read, so a session is never stored that reads back as null.\n if (!isSession(session)) {\n throw new TypeError('A session needs a tokens or user object');\n }\n const current = await locate(cookies);\n await clearSplitCookies(cookies);\n const data = await createEncryptedJWT(session, secret);\n\n if (continuation && current) {\n const existing = await store.get(current.key);\n if (existing) {\n // Same handle, same absolute expiry: a refresh extends nothing.\n await store.set(current.key, { data, expiresAt: existing.expiresAt, sid: session.sid });\n } else {\n // Cleared while the refresh was in flight. The logout wins.\n logger.info({ sid: session.sid }, 'Session ended during refresh; not persisted');\n }\n return { cookieValue: current.handle };\n }\n\n // A new session gets a new handle, so a handle planted before login\n // (session fixation) never becomes a logged-in one.\n if (current) {\n await store.delete(current.key);\n }\n const handle = generateToken(32);\n const key = await sha256(handle);\n const expiresAt = new Date(Date.now() + maxAge * 1000).toISOString();\n await store.set(key, { data, expiresAt, sid: session.sid });\n await cookies.set(cookieName, handle, { ...defaultSessionCookieOptions, maxAge });\n return { cookieValue: handle };\n },\n\n async read(cookies) {\n assertSessionSecret(secret);\n const current = await locate(cookies);\n if (!current) {\n // Split cookies but no handle: a consumer that just switched modes.\n // One re-login, not corruption.\n const legacy = await cookies.get(SESSION_COOKIE_NAME);\n return { session: null, reason: legacy ? 'stale_legacy_session' : 'no_session_cookie' };\n }\n return readRecord(current.key);\n },\n\n async readIdToken(cookies) {\n assertSessionSecret(secret);\n const current = await locate(cookies);\n if (!current) return undefined;\n return (await readRecord(current.key)).session?.tokens?.idToken;\n },\n\n async clear(cookies) {\n assertSessionSecret(secret);\n // Record first: once it is gone a copied cookie is dead, whatever\n // happens to the cookie itself.\n const current = await locate(cookies);\n if (current) {\n await store.delete(current.key);\n await cookies.delete(cookieName);\n }\n await clearSplitCookies(cookies);\n },\n\n async exclusive<T>(cookies: CookieStore, fn: () => Promise<T>): Promise<T> {\n assertSessionSecret(secret);\n const current = await locate(cookies);\n if (!current) return fn();\n\n const running = inflight.get(current.key);\n if (running) return running as Promise<T>;\n\n const run = (store.withLock ? store.withLock(current.key, fn) : fn()).finally(() => {\n inflight.delete(current.key);\n });\n inflight.set(current.key, run);\n return run;\n },\n };\n}\n","// server/session.ts\n//\n// Framework-agnostic session persistence over a {@link CookieStore}. Where the\n// session lives — encrypted in the cookies, or in a server-side store behind a\n// handle — is a {@link SessionPersistence}; a bare secret means the cookies.\n// These helpers own the refresh flow and emit the lifecycle events in\n// `../session-events`: this is the one layer that sees both the session and\n// what persisting it cost, so `session.created` / `session.refreshed` are\n// raised from here rather than from each caller.\n\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth-config';\nimport { classifyRefreshFailure, getOAuthErrorLogContext } from '../oauth-logging';\nimport { refreshSession } from '../session-refresh';\nimport {\n SESSION_EVENT,\n logSessionEvent,\n type RefreshFailureCause,\n type SessionClearedTrigger,\n type SessionRejectedReason,\n} from '../session-events';\nimport type { CreateSessionOptions, Session } from '../types';\nimport type { CookieStore } from './cookie-store';\nimport {\n clearSessionCookies,\n resolvePersistence,\n SessionSecretError,\n type ReadSessionResult,\n type SessionPersistence,\n type SessionSecret,\n} from './session-persistence';\n\nexport type { ReadSessionResult } from './session-persistence';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:session' });\n\n/** Seconds until the access token expires, for the `expiresIn` log field. */\nfunction expiresInSeconds(session: Session): number | undefined {\n const at = session.tokens?.accessTokenExpiresAt;\n if (!at) return undefined;\n const ms = Date.parse(at);\n return Number.isFinite(ms) ? Math.round((ms - Date.now()) / 1000) : undefined;\n}\n\n/** How {@link persistSession} should report what it just wrote. */\nexport interface PersistSessionOptions {\n /**\n * Which lifecycle event this write represents. Omit to write silently — for\n * callers that emit their own event, or that persist for reasons the session\n * vocabulary doesn't cover. Under server persistence only\n * {@link SESSION_EVENT.REFRESHED} keeps the handle; every other write starts\n * a new session with a new one.\n */\n event?: typeof SESSION_EVENT.CREATED | typeof SESSION_EVENT.REFRESHED;\n /** Only meaningful for {@link SESSION_EVENT.REFRESHED}: did the provider issue a new refresh token? */\n rotatedRefreshToken?: boolean;\n}\n\n/**\n * Persists a session and raises its lifecycle event.\n *\n * With a secret, or {@link cookiePersistence}, the session is encrypted across\n * the \"session\", \"session_at\" and \"session_it\" cookies. With\n * {@link serverPersistence} it goes to the store and the browser gets a handle.\n *\n * @returns The value written to the main session cookie: the encrypted session,\n * or the handle. Either is a bearer secret.\n */\nexport async function persistSession(\n store: CookieStore,\n session: Session,\n persistence: SessionSecret | SessionPersistence,\n options: PersistSessionOptions = {},\n): Promise<string> {\n const target = resolvePersistence(persistence);\n const written = await target.write(store, session, {\n continuation: options.event === SESSION_EVENT.REFRESHED,\n });\n\n if (options.event === SESSION_EVENT.CREATED) {\n logSessionEvent(logger, {\n event: SESSION_EVENT.CREATED,\n sid: session.sid,\n hasRefreshToken: !!session.tokens?.refreshToken,\n scopes: session.scopes,\n expiresIn: expiresInSeconds(session),\n cookieBytes: written.cookieBytes,\n persistence: target.mode,\n });\n } else if (options.event === SESSION_EVENT.REFRESHED) {\n logSessionEvent(logger, {\n event: SESSION_EVENT.REFRESHED,\n sid: session.sid,\n expiresIn: expiresInSeconds(session),\n rotatedRefreshToken: options.rotatedRefreshToken ?? false,\n cookieBytes: written.cookieBytes,\n persistence: target.mode,\n });\n }\n\n return written.cookieValue;\n}\n\n/**\n * Reads and reassembles the current session, reporting why when there isn't one.\n *\n * The reason comes from the package-wide {@link SessionRejectedReason}\n * vocabulary, so a proxy or status route can log the same word the heartbeat\n * endpoint logs. Does not log on its own — the caller owns the request context\n * and decides whether a missing session is worth a line.\n */\nexport async function tryReadSession(\n store: CookieStore,\n persistence: SessionSecret | SessionPersistence,\n): Promise<ReadSessionResult> {\n try {\n return await resolvePersistence(persistence).read(store);\n } catch (error) {\n // A bad secret must fail loudly, not log everyone out.\n if (error instanceof SessionSecretError) throw error;\n // Worker thread errors (e.g. from the crypto worker) should not crash the app.\n if (error instanceof Error && error.message.includes('worker')) {\n logger.error({ error }, 'Worker thread error reading session');\n } else {\n logger.error({ error }, 'Unexpected error reading session');\n }\n return { session: null, reason: 'unreadable_session' };\n }\n}\n\n/**\n * Reads and reassembles the current session, or null when there is none. The\n * returned session may carry an expired access token.\n *\n * Prefer {@link tryReadSession} when you want to log *why* there is no session.\n */\nexport async function readSession(\n store: CookieStore,\n persistence: SessionSecret | SessionPersistence,\n): Promise<Session | null> {\n return (await tryReadSession(store, persistence)).session;\n}\n\n/** Outcome of a refresh attempt. */\nexport type RefreshSessionResult =\n | { ok: true; session: Session; rotatedRefreshToken: boolean }\n | {\n ok: false;\n reason: SessionRejectedReason;\n /**\n * Correlation id of the session that failed, when we got far enough to read\n * one. The line that *ends* a session is the one most worth correlating.\n */\n sid?: string;\n /**\n * Present only when a refresh was actually attempted, i.e. when `reason` is\n * `refresh_failed`. Absent means we never got as far as talking to the\n * provider, so there is no provider verdict to report.\n */\n cause?: RefreshFailureCause;\n };\n\n/**\n * Refreshes the stored session and persists the result, reporting why on failure.\n *\n * The `cause` is the point of this function: `refreshSessionInStore` collapses\n * \"the provider says this refresh token is dead\" and \"we could not reach the\n * provider\" into the same `null`, and a caller that logs the user out on both\n * ends sessions that were never actually invalid. Only `invalid_grant` is\n * terminal — see {@link classifyRefreshFailure}.\n *\n * Under server persistence, refreshes of one session are serialized: concurrent\n * callers get the first refresh's result rather than each spending the refresh\n * token, which with rotation would leave all but one holding a dead one.\n */\nexport async function tryRefreshSessionInStore(\n store: CookieStore,\n config: OAuthConfig,\n persistence: SessionSecret | SessionPersistence,\n options: CreateSessionOptions = {},\n): Promise<RefreshSessionResult> {\n const target = resolvePersistence(persistence);\n\n // Sessions with expired access tokens are returned too — exactly what\n // refresh is for.\n const read = await tryReadSession(store, target);\n const current = read.session;\n if (!current) {\n return { ok: false, reason: read.reason };\n }\n if (!current.tokens?.refreshToken) {\n return { ok: false, reason: 'no_refresh_token', sid: current.sid };\n }\n\n if (!target.exclusive) {\n return refreshNow(store, target, config, current, options);\n }\n\n return target.exclusive(store, async () => {\n // Re-read under the lock: another request may have refreshed, or ended,\n // this session while we waited for it.\n const latest = await tryReadSession(store, target);\n if (!latest.session) {\n return { ok: false, reason: latest.reason, sid: current.sid };\n }\n if (refreshedSince(current, latest.session)) {\n return alreadyRefreshed(current, latest.session);\n }\n return refreshNow(store, target, config, latest.session, options);\n });\n}\n\n/** Whether `after` carries tokens `before` did not — someone refreshed in between. */\nfunction refreshedSince(before: Session, after: Session): boolean {\n return (\n after.tokens?.accessToken !== before.tokens?.accessToken ||\n after.tokens?.refreshToken !== before.tokens?.refreshToken\n );\n}\n\nfunction alreadyRefreshed(before: Session, after: Session): RefreshSessionResult {\n logger.debug({ sid: after.sid }, 'Session already refreshed by a concurrent request');\n return {\n ok: true,\n session: after,\n rotatedRefreshToken: after.tokens?.refreshToken !== before.tokens?.refreshToken,\n };\n}\n\n/** The refresh itself: exchange, persist, classify. */\nasync function refreshNow(\n store: CookieStore,\n target: SessionPersistence,\n config: OAuthConfig,\n current: Session,\n options: CreateSessionOptions,\n): Promise<RefreshSessionResult> {\n if (!current.tokens?.refreshToken) {\n return { ok: false, reason: 'no_refresh_token', sid: current.sid };\n }\n\n try {\n const updated = await refreshSession(current, config, options);\n if (!updated) {\n // refreshSession resolves to a session or throws; a null here means the\n // provider answered with something we could not build a session from.\n logSessionEvent(logger, {\n event: SESSION_EVENT.REFRESH_FAILED,\n sid: current.sid,\n cause: 'idp_error',\n accessTokenExpiresAt: current.tokens.accessTokenExpiresAt,\n });\n return { ok: false, reason: 'refresh_failed', cause: 'idp_error', sid: current.sid };\n }\n\n const rotatedRefreshToken = updated.tokens?.refreshToken !== current.tokens.refreshToken;\n await persistSession(store, updated, target, {\n event: SESSION_EVENT.REFRESHED,\n rotatedRefreshToken,\n });\n\n return { ok: true, session: updated, rotatedRefreshToken };\n } catch (error) {\n const cause = classifyRefreshFailure(error);\n\n if (cause === 'invalid_grant' && target.exclusive) {\n // With rotation, a concurrent refresh on another instance may have spent\n // this token first — in which case its result is ours too.\n const latest = await tryReadSession(store, target);\n if (latest.session && refreshedSince(current, latest.session)) {\n return alreadyRefreshed(current, latest.session);\n }\n }\n\n logSessionEvent(logger, {\n event: SESSION_EVENT.REFRESH_FAILED,\n sid: current.sid,\n cause,\n status: getOAuthErrorLogContext(error).status,\n accessTokenExpiresAt: current.tokens.accessTokenExpiresAt,\n error: getOAuthErrorLogContext(error),\n });\n return { ok: false, reason: 'refresh_failed', cause, sid: current.sid };\n }\n}\n\n/**\n * Refreshes the stored session using its refresh token and persists the result.\n * Returns the updated session, or null when there is nothing to refresh or the\n * refresh token is no longer valid.\n *\n * Prefer {@link tryRefreshSessionInStore}: this signature cannot tell a dead\n * refresh token apart from an unreachable provider, and treating the two alike\n * logs out users whose sessions are still valid.\n */\nexport async function refreshSessionInStore(\n store: CookieStore,\n config: OAuthConfig,\n persistence: SessionSecret | SessionPersistence,\n options: CreateSessionOptions = {},\n): Promise<Session | null> {\n const result = await tryRefreshSessionInStore(store, config, persistence, options);\n return result.ok ? result.session : null;\n}\n\n/**\n * Reads the raw ID token independently of session validity — logout needs the\n * hint even when {@link readSession} returns null.\n */\nexport async function readIdToken(\n store: CookieStore,\n persistence: SessionSecret | SessionPersistence,\n): Promise<string | undefined> {\n try {\n return await resolvePersistence(persistence).readIdToken(store);\n } catch (error) {\n if (error instanceof SessionSecretError) throw error;\n logger.error({ error }, 'Unexpected error reading ID token');\n return undefined;\n }\n}\n\n/** Context for the {@link SESSION_EVENT.CLEARED} event raised by {@link clearSession}. */\nexport interface ClearSessionOptions {\n /** Why the session is going away. Omit to clear without raising an event. */\n trigger?: SessionClearedTrigger;\n /** Correlation id of the session being cleared, when the caller has it. */\n sid?: string;\n /**\n * Where the session lives. Defaults to the cookies; under server\n * persistence this is what deletes the record, which is the revocation\n * cookie persistence cannot do.\n */\n persistence?: SessionSecret | SessionPersistence;\n}\n\n/** Ends the session: deletes the cookies and, under server persistence, the record. */\nexport async function clearSession(\n store: CookieStore,\n options: ClearSessionOptions = {},\n): Promise<void> {\n if (options.persistence) {\n await resolvePersistence(options.persistence).clear(store);\n } else {\n await clearSessionCookies(store);\n }\n\n if (options.trigger) {\n logSessionEvent(logger, {\n event: SESSION_EVENT.CLEARED,\n sid: options.sid,\n trigger: options.trigger,\n });\n }\n}\n","// server/login-transaction.ts\n//\n// One login between the authorize redirect and the callback: state, PKCE\n// verifier, returnTo and start time, sealed into one of a few cookie slots, so\n// parallel logins in one browser neither overwrite each other nor pile up. It is\n// sealed under a key derived from the secret, so it never opens as a session.\n\nimport { assertCookieWithinLimit, defaultOAuthCookieOptions } from '../cookies';\nimport { createLogger } from '../logger';\nimport type { PKCEOptions } from '../oauth';\nimport { createEncryptedJWT, decryptJWT, hexToUint8Array } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { assertSessionSecret, type SessionSecret } from './session-persistence';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:login-transaction' });\n\nconst COOKIE_NAME = 'oauth_tx_';\n\n// Only a Secure cookie with Path=/ and no Domain may carry it, so a sibling\n// subdomain cannot plant a transaction.\nconst HOST_PREFIX = '__Host-';\n\n/** Live logins per browser. Another login evicts the oldest. */\nconst MAX_TRANSACTIONS = 3;\n\n/**\n * A longer returnTo is dropped, not stored in a cookie sent with every request.\n * Measured as serialized: JSON escaping grows a control character sixfold.\n */\nconst MAX_RETURN_TO_BYTES = 1024;\n\nconst DEFAULT_MAX_AGE_MS = 10 * 60 * 1000;\n\nconst KEY_INFO = 'fides-auth login-tx';\n\nexport interface LoginTransaction {\n state: string;\n codeVerifier: string;\n /** Path to return to after login, already validated. */\n returnTo?: string;\n /** Start of the login, in epoch milliseconds. */\n createdAt: number;\n}\n\n/** Why {@link validateCallback} rejected a callback. */\nexport type CallbackValidationReason =\n | 'idp_error'\n | 'state_mismatch'\n | 'transaction_expired'\n | 'missing_code';\n\nexport class CallbackValidationError extends Error {\n constructor(\n public readonly reason: CallbackValidationReason,\n /** The IdP's `error` code, for `idp_error`. */\n public readonly idpError?: string,\n /** The IdP's `error_description`. For logs, not for the user. */\n public readonly idpErrorDescription?: string,\n ) {\n super(`OIDC callback rejected: ${reason}`);\n this.name = 'CallbackValidationError';\n }\n}\n\nexport interface CreateTransactionOptions {\n returnTo?: string | null;\n}\n\nexport interface TransactionCookieOptions {\n /** A Secure, `__Host-` prefixed cookie. Turn off only for plain-http development. @default true */\n secure?: boolean;\n}\n\nexport interface ValidateCallbackOptions {\n /** @default 600000 (10 minutes) */\n maxAgeMs?: number;\n}\n\n/** Starts a login transaction from the PKCE parameters sent to the authorize endpoint. */\nexport function createTransaction(\n pkce: Pick<PKCEOptions, 'state' | 'code_verifier'>,\n { returnTo }: CreateTransactionOptions = {},\n): LoginTransaction {\n let kept = returnTo || undefined;\n if (kept && new TextEncoder().encode(JSON.stringify(kept)).length > MAX_RETURN_TO_BYTES) {\n logger.warn({ length: kept.length }, 'returnTo too long, dropped');\n kept = undefined;\n }\n return {\n state: pkce.state,\n codeVerifier: pkce.code_verifier,\n ...(kept ? { returnTo: kept } : {}),\n createdAt: Date.now(),\n };\n}\n\n/**\n * Seals the transaction into a random free `__Host-oauth_tx_<n>` slot, or the\n * oldest login's. Throws `CookieTooLargeError` if it would not fit in a cookie.\n */\nexport async function persistTransaction(\n cookies: CookieStore,\n transaction: LoginTransaction,\n secret: SessionSecret,\n { secure = true }: TransactionCookieOptions = {},\n): Promise<void> {\n const key = await transactionKey(secret);\n const slots = await readSlots(cookies, key, secure);\n const free = slots.filter((candidate) => !candidate.transaction);\n // A random free slot, so two logins started at the same moment rarely share one.\n const slot =\n free[Math.floor(Math.random() * free.length)] ??\n slots.reduce((oldest, candidate) =>\n (candidate.transaction?.createdAt ?? 0) < (oldest.transaction?.createdAt ?? 0)\n ? candidate\n : oldest,\n );\n\n const value = await createEncryptedJWT(transaction, key);\n assertCookieWithinLimit(slot.name, value);\n await cookies.set(slot.name, value, { ...defaultOAuthCookieOptions, secure });\n}\n\n/** The transaction for `state`, or null when none is stored or it does not open under `secret`. */\nexport async function readTransaction(\n cookies: CookieStore,\n state: string,\n secret: SessionSecret,\n { secure = true }: TransactionCookieOptions = {},\n): Promise<LoginTransaction | null> {\n const slots = await readSlots(cookies, await transactionKey(secret), secure);\n const match = findSlot(slots, state);\n if (match) return match.transaction;\n\n if (slots.some((slot) => slot.value && !slot.transaction)) {\n // Usually a different secret at login and callback.\n logger.warn('Unreadable login transaction cookie');\n }\n return null;\n}\n\n/** Deletes the transaction cookie for `state`, leaving other logins alone. */\nexport async function clearTransaction(\n cookies: CookieStore,\n state: string,\n secret: SessionSecret,\n { secure = true }: TransactionCookieOptions = {},\n): Promise<void> {\n const match = findSlot(await readSlots(cookies, await transactionKey(secret), secure), state);\n if (!match) return;\n if (secure) {\n // A plain delete is not Secure, and the browser ignores it for a __Host- cookie.\n await cookies.set(match.name, '', { ...defaultOAuthCookieOptions, secure, maxAge: 0 });\n } else {\n await cookies.delete(match.name);\n }\n}\n\n/** What the handlers use: Secure and `__Host-` unless the callback is plain http. */\nexport function transactionCookieOptions(redirectUri: string): TransactionCookieOptions {\n return { secure: !/^http:/i.test(redirectUri) };\n}\n\n/**\n * Checks the callback query against its transaction and returns the authorization\n * code. Throws {@link CallbackValidationError}.\n */\nexport function validateCallback(\n query: URLSearchParams,\n transaction: LoginTransaction,\n { maxAgeMs = DEFAULT_MAX_AGE_MS }: ValidateCallbackOptions = {},\n): string {\n // Is this our login, and still live? Only then trust what the IdP sent.\n if (!timingSafeEqual(query.get('state') ?? '', transaction.state)) {\n throw new CallbackValidationError('state_mismatch');\n }\n if (Date.now() - transaction.createdAt > maxAgeMs) {\n throw new CallbackValidationError('transaction_expired');\n }\n\n const error = query.get('error');\n if (error !== null) {\n throw new CallbackValidationError(\n 'idp_error',\n error,\n query.get('error_description') ?? undefined,\n );\n }\n\n const code = query.get('code');\n if (!code) {\n throw new CallbackValidationError('missing_code');\n }\n return code;\n}\n\n/** HKDF-SHA-256 of the secret, so a transaction and a session never share a key. */\nasync function transactionKey(secret: SessionSecret): Promise<Uint8Array> {\n // HKDF takes a key of any length; the session encryption doesn't.\n assertSessionSecret(secret);\n const ikm = typeof secret === 'string' ? hexToUint8Array(secret) : secret;\n const base = await crypto.subtle.importKey('raw', ikm as BufferSource, 'HKDF', false, [\n 'deriveBits',\n ]);\n const bits = await crypto.subtle.deriveBits(\n { name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(), info: new TextEncoder().encode(KEY_INFO) },\n base,\n 256,\n );\n return new Uint8Array(bits);\n}\n\ninterface Slot {\n name: string;\n value: string | null | undefined;\n transaction: LoginTransaction | null;\n}\n\nasync function readSlots(cookies: CookieStore, key: Uint8Array, secure: boolean): Promise<Slot[]> {\n const prefix = secure ? HOST_PREFIX : '';\n const names = Array.from({ length: MAX_TRANSACTIONS }, (_, i) => `${prefix}${COOKIE_NAME}${i}`);\n return Promise.all(\n names.map(async (name) => {\n const value = await cookies.get(name);\n return { name, value, transaction: value ? await open(value, key) : null };\n }),\n );\n}\n\nfunction findSlot(slots: Slot[], state: string) {\n for (const { name, transaction } of slots) {\n if (transaction && timingSafeEqual(state, transaction.state)) {\n return { name, transaction };\n }\n }\n return undefined;\n}\n\n/** The sealed transaction, or null when the value is forged, malformed or under another key. */\nasync function open(value: string, key: Uint8Array): Promise<LoginTransaction | null> {\n try {\n const sealed = await decryptJWT(value, key);\n if (\n typeof sealed.state === 'string' &&\n typeof sealed.codeVerifier === 'string' &&\n typeof sealed.createdAt === 'number' &&\n (sealed.returnTo === undefined || typeof sealed.returnTo === 'string')\n ) {\n return {\n state: sealed.state,\n codeVerifier: sealed.codeVerifier,\n returnTo: sealed.returnTo,\n createdAt: sealed.createdAt,\n };\n }\n } catch {\n // Treated as an empty slot.\n }\n return null;\n}\n\n/** Compares in time that depends only on the expected value's length. */\nfunction timingSafeEqual(received: string, expected: string): boolean {\n const encoder = new TextEncoder();\n const a = encoder.encode(received);\n const b = encoder.encode(expected);\n let diff = a.length ^ b.length;\n for (let i = 0; i < b.length; i++) {\n diff |= (a[i] ?? 0) ^ (b[i] ?? 0);\n }\n return diff === 0;\n}\n","// server/authorized-fetch.ts\n//\n// fetch() with the session's access token, locked to one API base. The token\n// goes only to URLs under `allowedBase` and never follows a redirect. A 401 gets\n// one refresh and one retry. Lives in server/ because a refresh must be persisted.\n\nimport { createLogger } from '../logger';\nimport type { RefreshFailureCause, SessionRejectedReason } from '../session-events';\nimport type { Session } from '../types';\nimport type { RefreshSessionResult } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:authorized-fetch' });\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/** Why {@link authorizedFetch} failed. */\nexport type AuthorizedFetchErrorReason =\n /** The URL is outside `allowedBase` or carries credentials. Nothing was sent. */\n | 'url_not_allowed'\n /** The session has no access token. Nothing was sent. */\n | 'no_access_token'\n /** The request failed before a response, including a refused redirect. */\n | 'network_error'\n /** No response within `timeoutMs`. */\n | 'timeout'\n /** A 401, and the refresh failed. Not retried. */\n | 'refresh_failed'\n /** A non-2xx response. */\n | 'http_error'\n /** `parse` threw. */\n | 'parse_failed';\n\nexport interface AuthorizedFetchErrorDetails {\n status?: number;\n method?: string;\n url?: string;\n refreshFailure?: { reason: SessionRejectedReason; cause?: RefreshFailureCause };\n response?: Response;\n cause?: unknown;\n}\n\n/** A failed {@link authorizedFetch}. Never carries the token or the query string. */\nexport class AuthorizedFetchError extends Error {\n readonly reason: AuthorizedFetchErrorReason;\n /** HTTP status, for `http_error` and `refresh_failed`. */\n readonly status?: number;\n readonly method?: string;\n /** Origin and path only. */\n readonly url?: string;\n /** Why the refresh failed. Only `cause: 'invalid_grant'` means the user must log in again. */\n readonly refreshFailure?: { reason: SessionRejectedReason; cause?: RefreshFailureCause };\n /** The unread non-2xx response, for `http_error`. */\n declare readonly response?: Response;\n\n constructor(reason: AuthorizedFetchErrorReason, details: AuthorizedFetchErrorDetails = {}) {\n const { status, method, url, refreshFailure, response, cause } = details;\n const target = url ? ` for ${method ?? 'GET'} ${url}` : '';\n super(\n `authorizedFetch ${reason}${status ? ` (${status})` : ''}${target}`,\n cause === undefined ? undefined : { cause },\n );\n this.name = 'AuthorizedFetchError';\n this.reason = reason;\n this.status = status;\n this.method = method;\n this.url = url;\n this.refreshFailure = refreshFailure;\n // Non-enumerable, so logging the error doesn't print the full URL.\n Object.defineProperty(this, 'response', { value: response, enumerable: false });\n }\n}\n\nexport interface AuthorizedFetchOptions<T = undefined> extends Omit<RequestInit, 'redirect'> {\n /** Absolute http(s) URL. The token goes only to its origin, under its path. */\n allowedBase: string | URL;\n /** Reads and validates a 2xx response. Its result is `data`; a throw is `parse_failed`. */\n parse?: (response: Response) => T | Promise<T>;\n /** Per request, including a retry. @default 10000 */\n timeoutMs?: number;\n /**\n * Called once on a 401, e.g. `() => tryRefreshSessionInStore(store, config, persistence)`.\n * Without it a 401 is an `http_error`. A retry resends `body`, so don't pass a stream.\n */\n refresh?: () => Promise<RefreshSessionResult | Session | null>;\n}\n\nexport interface AuthorizedFetchResult<T> {\n /** The 2xx response. `parse` has read its body, if given. */\n response: Response;\n /** What `parse` returned. */\n data: T;\n /** The session that made the call: the refreshed one if `refreshed`. Use it from now on. */\n session: Session;\n /** A 401 led to a refresh. Persist `session` unless `refresh` already did. */\n refreshed: boolean;\n}\n\n/**\n * Calls an API with the session's access token.\n *\n * @throws {AuthorizedFetchError} on a disallowed URL, non-2xx, timeout, network error or failed refresh.\n * @throws {TypeError} when `allowedBase` is not an http(s) URL without credentials.\n */\nexport async function authorizedFetch<T = undefined>(\n session: Session,\n url: string | URL,\n options: AuthorizedFetchOptions<T>,\n): Promise<AuthorizedFetchResult<T>> {\n const { allowedBase, parse, timeoutMs = DEFAULT_TIMEOUT_MS, refresh, ...init } = options;\n const target = resolveAllowedUrl(url, allowedBase);\n const request = { method: (init.method ?? 'GET').toUpperCase(), url: redactUrl(target) };\n\n let current = session;\n let attempt = await send(current, target, init, timeoutMs, request);\n let refreshed = false;\n\n if (attempt.response.status === 401 && refresh) {\n await attempt.response.body?.cancel().catch(() => undefined);\n attempt.unlink();\n logger.debug(request, 'Got 401, refreshing the session once');\n current = await refreshOnce(refresh, request);\n refreshed = true;\n attempt = await send(current, target, init, timeoutMs, request);\n }\n\n const { response, timeout, unlink } = attempt;\n if (!response.ok) {\n throw new AuthorizedFetchError('http_error', { ...request, status: response.status, response });\n }\n\n try {\n const data = (parse ? await parse(response) : undefined) as T;\n // A body still to be read stays under the timeout and the caller's signal.\n if (!response.body || response.bodyUsed) unlink();\n return { response, data, session: current, refreshed };\n } catch (error) {\n unlink();\n if (init.signal?.aborted) throw error;\n // A body read cut off by the timeout rejects with the timeout's own reason.\n const reason = error === timeout.reason ? 'timeout' : 'parse_failed';\n throw new AuthorizedFetchError(reason, { ...request, status: response.status, cause: error });\n }\n}\n\n/** Resolves `url` against the base and throws unless it stays under it. */\nfunction resolveAllowedUrl(url: string | URL, allowedBase: string | URL): URL {\n const base = new URL(allowedBase);\n if (base.protocol !== 'https:' && base.protocol !== 'http:') {\n throw new TypeError('authorizedFetch: allowedBase must be an http(s) URL');\n }\n if (base.username || base.password) {\n throw new TypeError('authorizedFetch: allowedBase must not carry credentials');\n }\n // A trailing slash makes `users` resolve under `/v1`, and `/v1` not match `/v1-evil`.\n if (!base.pathname.endsWith('/')) base.pathname += '/';\n\n let target: URL;\n try {\n target = new URL(url, base);\n } catch {\n // No cause: Node's TypeError carries the raw input, query and all.\n throw new AuthorizedFetchError('url_not_allowed');\n }\n\n const underBase =\n target.pathname.startsWith(base.pathname) || `${target.pathname}/` === base.pathname;\n if (target.username || target.password || target.origin !== base.origin || !underBase) {\n logger.warn(\n { url: redactUrl(target), allowedBase: redactUrl(base) },\n 'Refused to send the access token outside allowedBase',\n );\n throw new AuthorizedFetchError('url_not_allowed', { url: redactUrl(target) });\n }\n return target;\n}\n\ninterface Attempt {\n response: Response;\n timeout: AbortSignal;\n /** Stops listening to the caller's signal, once nobody will read the body. */\n unlink: () => void;\n}\n\nasync function send(\n session: Session,\n target: URL,\n init: RequestInit,\n timeoutMs: number,\n request: { method: string; url: string },\n): Promise<Attempt> {\n const token = session.tokens?.accessToken;\n if (!token) throw new AuthorizedFetchError('no_access_token', request);\n\n const headers = new Headers(init.headers);\n headers.set('Authorization', `Bearer ${token}`);\n const timeout = AbortSignal.timeout(timeoutMs);\n const { signal, unlink } = linkSignals(timeout, init.signal);\n\n try {\n const response = await fetch(target, { ...init, headers, signal, redirect: 'error' });\n return { response, timeout, unlink };\n } catch (error) {\n unlink();\n // The caller's own abort is theirs to handle.\n if (init.signal?.aborted) throw error;\n const reason = timeout.aborted ? 'timeout' : 'network_error';\n throw new AuthorizedFetchError(reason, { ...request, cause: error });\n }\n}\n\n/**\n * A signal that aborts when the timeout or the caller's signal does, with that signal's\n * reason, and `unlink` to remove the listeners. `AbortSignal.any` would need Node 18.17.\n */\nfunction linkSignals(\n timeout: AbortSignal,\n caller?: AbortSignal | null,\n): { signal: AbortSignal; unlink: () => void } {\n if (!caller) return { signal: timeout, unlink: () => undefined };\n\n const sources = [caller, timeout];\n const controller = new AbortController();\n const unlink = () => {\n for (const source of sources) source.removeEventListener('abort', onAbort);\n };\n const onAbort = () => {\n unlink();\n controller.abort(sources.find((source) => source.aborted)?.reason);\n };\n\n if (sources.some((source) => source.aborted)) onAbort();\n else for (const source of sources) source.addEventListener('abort', onAbort);\n return { signal: controller.signal, unlink };\n}\n\nasync function refreshOnce(\n refresh: NonNullable<AuthorizedFetchOptions['refresh']>,\n request: { method: string; url: string },\n): Promise<Session> {\n let result: RefreshSessionResult;\n try {\n result = toRefreshResult(await refresh());\n } catch (error) {\n throw new AuthorizedFetchError('refresh_failed', { ...request, status: 401, cause: error });\n }\n\n if (!result.ok) {\n const refreshFailure = { reason: result.reason, cause: result.cause };\n throw new AuthorizedFetchError('refresh_failed', { ...request, status: 401, refreshFailure });\n }\n return result.session;\n}\n\n/** Accepts both `tryRefreshSessionInStore` and `refreshSessionInStore` results. */\nfunction toRefreshResult(value: RefreshSessionResult | Session | null): RefreshSessionResult {\n if (!value) return { ok: false, reason: 'refresh_failed' };\n if ('ok' in value) return value;\n return { ok: true, session: value, rotatedRefreshToken: false };\n}\n\n/** Origin and path: never credentials, query or fragment. */\nfunction redactUrl(url: URL): string {\n return `${url.origin}${url.pathname}`;\n}\n","// server/oidc-login.ts\n//\n// Framework-agnostic OIDC login initiation. Builds PKCE parameters, seals them\n// into a login transaction cookie, and redirects to the authorization endpoint.\n// Uses the standard Request/Response, a CookieStore, and an optional rate-limit\n// callback, so any server runtime can call it.\n\nimport { CookieTooLargeError } from '../cookies';\nimport { createLogger } from '../logger';\nimport { buildPKCEOptions, discoverAndBuildAuthorizationUrl } from '../oauth';\nimport type { OAuthConfig } from '../oauth-config';\nimport type { CookieStore } from './cookie-store';\nimport {\n createTransaction,\n persistTransaction,\n transactionCookieOptions,\n} from './login-transaction';\nimport { assertSessionSecret, type SessionSecret } from './session-persistence';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-login' });\n\nexport interface OidcLoginConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store that receives the login transaction cookie. */\n cookies: CookieStore;\n\n /** Seals the login transaction. Must match the callback's. */\n secret: SessionSecret;\n\n /**\n * Optional rate-limit gate. When it resolves false, the handler responds 429.\n */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /**\n * Validates and sanitizes the returnTo parameter, returning a safe path or null.\n * @default Only allows printable-ASCII paths starting with a single `/`.\n */\n validateReturnTo?: (rawReturnTo: string) => string | null;\n\n /**\n * Per-request authorization parameters, merged over `oauthConfig.authorizationParameters`.\n * Reserved keys throw: `state`, `code_challenge`, `code_challenge_method`, `redirect_uri`,\n * `client_id`, `response_type`, `request`, `request_uri` and `nonce`.\n */\n authorizationParameters?: (\n request: Request,\n ) => Record<string, string> | Promise<Record<string, string>>;\n}\n\nconst defaultValidateReturnTo = (raw: string): string | null =>\n /^\\/(?!\\/)[\\x20-\\x7e]*$/.test(raw) ? raw : null;\n\n/**\n * Handles OIDC login initiation: rate-limit, build PKCE, seal it into a login\n * transaction, and redirect to the authorization endpoint.\n */\nexport async function handleOidcLogin(\n request: Request,\n config: OidcLoginConfig,\n): Promise<Response> {\n const { oauthConfig, cookies, rateLimit, validateReturnTo = defaultValidateReturnTo } = config;\n // Before discovery or PAR: a bad secret should fail without contacting the provider.\n assertSessionSecret(config.secret);\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n const url = new URL(request.url);\n const rawReturnTo = url.searchParams.get('returnTo');\n const returnTo = rawReturnTo ? validateReturnTo(rawReturnTo) : null;\n\n const parameters = await config.authorizationParameters?.(request);\n const pkce = await buildPKCEOptions(oauthConfig, { parameters });\n const authorizationUrl = await discoverAndBuildAuthorizationUrl(oauthConfig, pkce);\n\n const transaction = createTransaction(pkce, { returnTo });\n const secret = config.secret;\n const cookieOptions = transactionCookieOptions(oauthConfig.redirect_uri);\n try {\n await persistTransaction(cookies, transaction, secret, cookieOptions);\n } catch (error) {\n // Too big for a cookie: still log in, just without the way back.\n if (!(error instanceof CookieTooLargeError) || !transaction.returnTo) throw error;\n logger.warn({ size: error.size }, 'Login transaction too large; dropped returnTo');\n await persistTransaction(cookies, { ...transaction, returnTo: undefined }, secret, cookieOptions);\n }\n\n logger.info('Redirecting to OIDC provider for login');\n\n return new Response(null, {\n status: 302,\n headers: { Location: authorizationUrl.toString() },\n });\n}\n","// server/oidc-callback.ts\n//\n// Framework-agnostic OIDC callback handler. Orchestrates the OIDC concerns —\n// rate limit, login transaction check, code exchange, session persistence, safe\n// redirect — over the standard Request/Response and a CookieStore, delegating the\n// protocol to the core oauth module and persistence to the server session helpers.\n\nimport { CookieTooLargeError } from '../cookies';\nimport { createLogger } from '../logger';\nimport type { MapSession } from '../oauth';\nimport {\n buildSessionFromTokens,\n exchangeAuthorizationCode,\n validateReturnUrl,\n} from '../oauth';\nimport type { OAuthConfig } from '../oauth-config';\nimport { SESSION_EVENT } from '../session-events';\nimport type { CookieStore } from './cookie-store';\nimport {\n CallbackValidationError,\n clearTransaction,\n readTransaction,\n transactionCookieOptions,\n validateCallback,\n} from './login-transaction';\nimport { persistSession } from './session';\nimport {\n assertSessionSecret,\n type SessionPersistence,\n type SessionSecret,\n} from './session-persistence';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-callback' });\n\n// Registered OAuth/OIDC error codes are snake_case; nothing else is echoed.\nconst IDP_ERROR_CODE = /^[a-z0-9_]{1,64}$/;\n\nexport interface OidcCallbackConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Public application URL (used to reconstruct the callback URL and validate redirects). */\n applicationUrl: string;\n\n /** Cookie store holding the login transaction and receiving the session cookies. */\n cookies: CookieStore;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /**\n * Opens the login transaction (must match `handleOidcLogin`'s) and encrypts the\n * default cookie persistence. Required even with `persistence`.\n */\n secret: SessionSecret;\n\n /** Where the session lives. Defaults to the cookies under `secret`; see `serverPersistence`. */\n persistence?: SessionPersistence;\n\n /**\n * Name of the ID token claim that contains user roles.\n * @default 'roles'\n */\n rolesClaim?: string;\n\n /** Changes the session before it is stored, e.g. to add `data`. See {@link MapSession}. */\n mapSession?: MapSession;\n\n /**\n * Default path to redirect to after login if no returnTo cookie is set.\n * @default '/'\n */\n defaultRedirectPath?: string;\n\n /**\n * Optional validator for the returnTo path. If omitted, only same-origin paths\n * are allowed.\n */\n validateReturnTo?: (path: string) => boolean;\n}\n\n/**\n * Handles the OIDC callback: rate-limit, exchange the code for tokens, persist the\n * session across the cookie store, and redirect to a validated returnTo.\n */\nexport async function handleOidcCallback(\n request: Request,\n config: OidcCallbackConfig,\n): Promise<Response> {\n const {\n oauthConfig,\n applicationUrl,\n cookies,\n secret,\n rateLimit,\n rolesClaim = 'roles',\n mapSession,\n defaultRedirectPath = '/',\n validateReturnTo,\n } = config;\n // Up front, so a bad secret fails before the transaction is read or the code exchanged.\n assertSessionSecret(secret);\n const persistence = config.persistence ?? secret;\n const txCookie = transactionCookieOptions(oauthConfig.redirect_uri);\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n const currentUrl = new URL(request.url);\n const state = currentUrl.searchParams.get('state');\n const idpError = currentUrl.searchParams.get('error') ?? undefined;\n\n // A cancel after the login ended still redirects, but with a fixed code:\n // without a live transaction nothing in the query is trusted.\n const loginEnded = (): Response =>\n idpError === undefined\n ? new Response('Please restart the login process.', { status: 400 })\n : loginErrorRedirect(applicationUrl, defaultRedirectPath, 'login_expired');\n\n try {\n // Reconstruct the public callback URL. Behind a TLS-terminating proxy\n // applicationUrl is just the origin (\"/\"), and the token-exchange redirect_uri\n // must match the callback path used at authorize — so keep the request path.\n const publicUrl = new URL(applicationUrl);\n publicUrl.pathname = currentUrl.pathname;\n publicUrl.search = currentUrl.search;\n\n const transaction = state ? await readTransaction(cookies, state, secret, txCookie) : null;\n if (!transaction) {\n logger.warn({ error: idpError }, 'Missing login transaction');\n return loginEnded();\n }\n\n validateCallback(currentUrl.searchParams, transaction);\n\n const tokens = await exchangeAuthorizationCode(\n oauthConfig,\n publicUrl,\n transaction.codeVerifier,\n transaction.state,\n );\n\n // Build the session and persist it (split across session/session_at).\n // persistSession raises session.created — it is the only layer that knows\n // what the session cost in cookie bytes, and a login that comes back without\n // a refresh token (no offline_access) is visible there on day one.\n const session = buildSessionFromTokens(tokens, { rolesClaim, mapSession });\n await persistSession(cookies, session, persistence, { event: SESSION_EVENT.CREATED });\n\n const redirectUrl = validateReturnUrl(\n transaction.returnTo ?? null,\n applicationUrl,\n defaultRedirectPath,\n validateReturnTo,\n );\n redirectUrl.searchParams.set('login', 'success');\n\n logger.info(\n { sid: session.sid, redirectUrl: redirectUrl.toString() },\n 'Login successful, redirecting',\n );\n\n return new Response(null, {\n status: 302,\n headers: { Location: redirectUrl.toString() },\n });\n } catch (error) {\n if (error instanceof CallbackValidationError) {\n // A denied or cancelled login: send the user home, not to an error page.\n if (error.reason === 'idp_error') {\n logger.warn(\n {\n reason: error.reason,\n error: error.idpError,\n errorDescription: error.idpErrorDescription,\n },\n 'Identity provider returned an error',\n );\n const code = error.idpError ?? '';\n return loginErrorRedirect(\n applicationUrl,\n defaultRedirectPath,\n IDP_ERROR_CODE.test(code) ? code : 'idp_error',\n );\n }\n logger.warn({ reason: error.reason, error: idpError }, 'Rejected OIDC callback');\n return error.reason === 'transaction_expired'\n ? loginEnded()\n : new Response('Please restart the login process.', { status: 400 });\n }\n\n // The session was too large for a cookie — surface this distinctly instead of\n // hiding it in a generic 500, so it's diagnosable in production. Server\n // persistence is the fix; see docs/server-sessions.md.\n if (error instanceof CookieTooLargeError) {\n logger.error(\n { error, cookieName: error.cookieName, size: error.size },\n 'Session cookie too large to store',\n );\n return new Response('The session is too large to store. Please contact support.', {\n status: 431,\n });\n }\n\n logger.error({ error }, 'OIDC callback error');\n return new Response('An unexpected error occurred. Please restart the login process.', {\n status: 500,\n });\n } finally {\n // The login transaction is spent whatever the outcome.\n if (state) await clearTransaction(cookies, state, secret, txCookie);\n }\n}\n\n/** Redirects to the default path with `login=error` and a safe error code, never a description. */\nfunction loginErrorRedirect(\n applicationUrl: string,\n defaultRedirectPath: string,\n code: string,\n): Response {\n const redirectUrl = new URL(defaultRedirectPath, applicationUrl);\n redirectUrl.searchParams.set('login', 'error');\n redirectUrl.searchParams.set('error', code);\n return new Response(null, { status: 302, headers: { Location: redirectUrl.toString() } });\n}\n","// server/oidc-logout.ts\n//\n// Framework-agnostic RP-initiated logout. Clearing only the local session cookie\n// leaves the provider's session alive, so the next /authorize silently\n// re-authenticates — this handler ends both.\n\nimport { createLogger } from '../logger';\nimport { buildOidcLogoutUrl } from '../oauth';\nimport type { OAuthConfig } from '../oauth-config';\nimport type { CookieStore } from './cookie-store';\nimport { clearSession, readIdToken, tryReadSession } from './session';\nimport { persistenceFrom, type SessionPersistenceConfig } from './session-persistence';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-logout' });\n\n/**\n * Whether the request came from the application itself. POST alone is no defence:\n * a third-party page can auto-submit a form at this endpoint, and the response\n * clears the cookies whether or not the browser attached them.\n *\n * `Sec-Fetch-Site` is authoritative where the browser sends it. Otherwise fall back\n * to `Origin`. A request with neither is not a browser form post, so it is allowed.\n */\nfunction isSameOrigin(request: Request, applicationUrl?: string): boolean {\n const site = request.headers.get('sec-fetch-site');\n if (site) {\n return site === 'same-origin' || site === 'none';\n }\n\n const origin = request.headers.get('origin');\n if (!origin) return true;\n\n try {\n return new URL(origin).origin === new URL(applicationUrl ?? request.url).origin;\n } catch {\n return false;\n }\n}\n\n/** {@link handleOidcLogout} config. Takes `secret`, `persistence`, or both. */\nexport type OidcLogoutConfig = OidcLogoutBaseConfig & SessionPersistenceConfig;\n\ninterface OidcLogoutBaseConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store holding the session cookies to read and then clear. */\n cookies: CookieStore;\n\n /** Post-logout redirect URI. Must be registered with the provider. */\n postLogoutRedirectUri: string;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Passed through as `state`; verifying it on the way back is the caller's job. */\n state?: string;\n\n /** `logout_hint`, when the provider documents one. */\n logoutHint?: string;\n\n /** Whether to send `client_id`. Default true — see `OidcLogoutOptions`. */\n includeClientId?: boolean;\n\n /**\n * Request methods that may trigger a logout. Defaults to POST only: on GET, any\n * third-party page can force a logout by navigating the browser.\n * @default ['POST']\n */\n allowedMethods?: string[];\n\n /**\n * Public application URL, used to check the `Origin` header behind a proxy where\n * the request URL carries the internal origin. Defaults to the request's own\n * origin.\n */\n applicationUrl?: string;\n\n /**\n * Where to send the user when logout is local-only (no end_session_endpoint).\n * @default postLogoutRedirectUri\n */\n fallbackRedirectUri?: string;\n}\n\n/**\n * Clears the session cookies and redirects to the provider's end-session endpoint\n * with `id_token_hint`. Local cookies are cleared even when the provider hop is\n * unavailable, so the user is always logged out of this application.\n */\nexport async function handleOidcLogout(\n request: Request,\n config: OidcLogoutConfig,\n): Promise<Response> {\n const {\n oauthConfig,\n cookies,\n postLogoutRedirectUri,\n rateLimit,\n state,\n logoutHint,\n includeClientId,\n fallbackRedirectUri = postLogoutRedirectUri,\n allowedMethods = ['POST'],\n applicationUrl,\n } = config;\n const persistence = persistenceFrom(config);\n\n if (!allowedMethods.includes(request.method)) {\n return new Response(null, {\n status: 405,\n headers: { Allow: allowedMethods.join(', ') },\n });\n }\n\n if (!isSameOrigin(request, applicationUrl)) {\n logger.warn(\n { origin: request.headers.get('origin'), site: request.headers.get('sec-fetch-site') },\n 'Cross-origin logout request rejected',\n );\n return new Response('Forbidden', { status: 403 });\n }\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n // Read before clearing. A session too broken to read still logs out fine,\n // and the hint matters most exactly then — so fall back to the raw ID token.\n // The sid is best-effort correlation, so the logout line joins the rest of\n // this session's events.\n const read = await tryReadSession(cookies, persistence);\n const idTokenHint =\n read.session?.tokens?.idToken ?? (await readIdToken(cookies, persistence));\n const sid = read.session?.sid;\n\n try {\n await clearSession(cookies, { trigger: 'logout', sid, persistence });\n } catch (error) {\n // Never redirect to the provider on a half-cleared local session — the user\n // would come back looking logged in.\n logger.error({ error }, 'Failed to clear session cookies');\n return new Response('Logout failed', { status: 500 });\n }\n\n const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {\n postLogoutRedirectUri,\n idTokenHint,\n state,\n logoutHint,\n includeClientId,\n });\n\n if (!logoutUrl) {\n // Either the provider advertises no end_session_endpoint or discovery failed;\n // buildOidcLogoutUrl logs which. Local logout stands either way.\n logger.info(\n { issuer: oauthConfig.issuer },\n 'No logout URL — local session cleared, provider session may persist',\n );\n return new Response(null, { status: 302, headers: { Location: fallbackRedirectUri } });\n }\n\n logger.info({ hasIdTokenHint: !!idTokenHint }, 'Redirecting to provider for logout');\n\n return new Response(null, { status: 302, headers: { Location: logoutUrl.toString() } });\n}\n","// server/heartbeat-handler.ts\n//\n// Framework-agnostic heartbeat endpoint. Pairs with the client-side heartbeat:\n// when the user is active the client POSTs here, and this handler exchanges the\n// current refresh token for a fresh access token. Glue only — rate limit, load\n// session, refresh, respond — over the standard Request/Response and a CookieStore.\n\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth-config';\nimport { SESSION_EVENT, logSessionEvent, type SessionRejectedReason } from '../session-events';\nimport type { CookieStore } from './cookie-store';\nimport { tryRefreshSessionInStore } from './session';\nimport { persistenceFrom, type SessionPersistenceConfig } from './session-persistence';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:heartbeat' });\n\n/** {@link handleHeartbeat} config. Takes `secret`, `persistence`, or both. */\nexport type HeartbeatHandlerConfig = HeartbeatBaseConfig & SessionPersistenceConfig;\n\ninterface HeartbeatBaseConfig {\n /** OAuth/OIDC configuration used to refresh the access token. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store holding the session. */\n cookies: CookieStore;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n}\n\n/**\n * Handles a heartbeat request — refresh the active session if there is one.\n *\n * Returns 200 with `{ accessTokenExpiresAt }` on success, 401 with `{ reason }`\n * when the session is genuinely over, 503 when the provider could not be\n * reached, 405 for non-POST methods, and 429 when rate-limited.\n *\n * The 401/503 split matters: a 401 tells the client to log the user out, so it\n * must be reserved for the cases where the session really is finished. A network\n * blip or a provider 5xx says nothing about the session's validity — answering\n * 401 there logs out users whose sessions were fine, which is precisely the\n * \"logged out every few minutes\" symptom. The client already retries non-401\n * failures with backoff.\n */\nexport async function handleHeartbeat(\n request: Request,\n config: HeartbeatHandlerConfig,\n): Promise<Response> {\n const { oauthConfig, cookies, rateLimit } = config;\n const persistence = persistenceFrom(config);\n\n if (request.method !== 'POST') {\n return new Response(null, { status: 405, headers: { Allow: 'POST' } });\n }\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Heartbeat rate limit exceeded');\n return new Response(null, { status: 429 });\n }\n\n // An expired access token is what the heartbeat recovers from, not 401 on.\n // tryRefreshSessionInStore reads the session, refreshes it, and reports which\n // of the two it failed at — so this handler never has to re-read cookies to\n // work out why.\n const result = await tryRefreshSessionInStore(cookies, oauthConfig, persistence);\n\n if (result.ok) {\n logger.debug({ sid: result.session.sid }, 'Heartbeat refresh succeeded');\n return Response.json(\n { accessTokenExpiresAt: result.session.tokens?.accessTokenExpiresAt ?? null },\n {\n // Auth/session endpoint — must never be cached by the browser or any\n // intermediary, or a stale 200 could fool the client into thinking a refresh\n // succeeded without actually hitting the server.\n headers: { 'Cache-Control': 'private, no-store' },\n },\n );\n }\n\n // The provider was unreachable or broken. The session is probably still fine,\n // so keep it and let the client retry rather than ending it.\n if (result.cause === 'transport' || result.cause === 'idp_error') {\n logger.warn(\n { sid: result.sid, cause: result.cause },\n 'Heartbeat refresh could not be completed — keeping the session',\n );\n return new Response(null, { status: 503, headers: { 'Cache-Control': 'private, no-store' } });\n }\n\n // Everything left is a session that genuinely cannot continue.\n logSessionEvent(logger, {\n event: SESSION_EVENT.REJECTED,\n sid: result.sid,\n reason: result.reason,\n source: 'heartbeat',\n });\n\n return unauthorized(result.reason);\n}\n\n/**\n * 401 carrying the reason in the body.\n *\n * The browser is where the logout becomes visible, so letting the client name\n * the reason lets a consumer beacon it back with the same vocabulary the server\n * used. The values are session state, not secrets — and clients that ignore the\n * body are unaffected.\n */\nfunction unauthorized(reason: SessionRejectedReason): Response {\n return Response.json(\n { reason },\n { status: 401, headers: { 'Cache-Control': 'private, no-store' } },\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { PKCEOptions } from '../oauth';
2
+ import type { CookieStore } from './cookie-store';
3
+ import { type SessionSecret } from './session-persistence';
4
+ export interface LoginTransaction {
5
+ state: string;
6
+ codeVerifier: string;
7
+ /** Path to return to after login, already validated. */
8
+ returnTo?: string;
9
+ /** Start of the login, in epoch milliseconds. */
10
+ createdAt: number;
11
+ }
12
+ /** Why {@link validateCallback} rejected a callback. */
13
+ export type CallbackValidationReason = 'idp_error' | 'state_mismatch' | 'transaction_expired' | 'missing_code';
14
+ export declare class CallbackValidationError extends Error {
15
+ readonly reason: CallbackValidationReason;
16
+ /** The IdP's `error` code, for `idp_error`. */
17
+ readonly idpError?: string | undefined;
18
+ /** The IdP's `error_description`. For logs, not for the user. */
19
+ readonly idpErrorDescription?: string | undefined;
20
+ constructor(reason: CallbackValidationReason,
21
+ /** The IdP's `error` code, for `idp_error`. */
22
+ idpError?: string | undefined,
23
+ /** The IdP's `error_description`. For logs, not for the user. */
24
+ idpErrorDescription?: string | undefined);
25
+ }
26
+ export interface CreateTransactionOptions {
27
+ returnTo?: string | null;
28
+ }
29
+ export interface TransactionCookieOptions {
30
+ /** A Secure, `__Host-` prefixed cookie. Turn off only for plain-http development. @default true */
31
+ secure?: boolean;
32
+ }
33
+ export interface ValidateCallbackOptions {
34
+ /** @default 600000 (10 minutes) */
35
+ maxAgeMs?: number;
36
+ }
37
+ /** Starts a login transaction from the PKCE parameters sent to the authorize endpoint. */
38
+ export declare function createTransaction(pkce: Pick<PKCEOptions, 'state' | 'code_verifier'>, { returnTo }?: CreateTransactionOptions): LoginTransaction;
39
+ /**
40
+ * Seals the transaction into a random free `__Host-oauth_tx_<n>` slot, or the
41
+ * oldest login's. Throws `CookieTooLargeError` if it would not fit in a cookie.
42
+ */
43
+ export declare function persistTransaction(cookies: CookieStore, transaction: LoginTransaction, secret: SessionSecret, { secure }?: TransactionCookieOptions): Promise<void>;
44
+ /** The transaction for `state`, or null when none is stored or it does not open under `secret`. */
45
+ export declare function readTransaction(cookies: CookieStore, state: string, secret: SessionSecret, { secure }?: TransactionCookieOptions): Promise<LoginTransaction | null>;
46
+ /** Deletes the transaction cookie for `state`, leaving other logins alone. */
47
+ export declare function clearTransaction(cookies: CookieStore, state: string, secret: SessionSecret, { secure }?: TransactionCookieOptions): Promise<void>;
48
+ /** What the handlers use: Secure and `__Host-` unless the callback is plain http. */
49
+ export declare function transactionCookieOptions(redirectUri: string): TransactionCookieOptions;
50
+ /**
51
+ * Checks the callback query against its transaction and returns the authorization
52
+ * code. Throws {@link CallbackValidationError}.
53
+ */
54
+ export declare function validateCallback(query: URLSearchParams, transaction: LoginTransaction, { maxAgeMs }?: ValidateCallbackOptions): string;
55
+ //# sourceMappingURL=login-transaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-transaction.d.ts","sourceRoot":"","sources":["../../src/server/login-transaction.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAuBhF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wDAAwD;AACxD,MAAM,MAAM,wBAAwB,GAChC,WAAW,GACX,gBAAgB,GAChB,qBAAqB,GACrB,cAAc,CAAC;AAEnB,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,MAAM,EAAE,wBAAwB;IAChD,+CAA+C;aAC/B,QAAQ,CAAC,EAAE,MAAM;IACjC,iEAAiE;aACjD,mBAAmB,CAAC,EAAE,MAAM;IAL9C,YACkB,MAAM,EAAE,wBAAwB;IAChD,+CAA+C;IAC/B,QAAQ,CAAC,EAAE,MAAM,YAAA;IACjC,iEAAiE;IACjD,mBAAmB,CAAC,EAAE,MAAM,YAAA,EAI7C;CACF;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,mGAAmG;IACnG,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,eAAe,CAAC,EAClD,EAAE,QAAQ,EAAE,GAAE,wBAA6B,GAC1C,gBAAgB,CAYlB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,WAAW,EACpB,WAAW,EAAE,gBAAgB,EAC7B,MAAM,EAAE,aAAa,EACrB,EAAE,MAAa,EAAE,GAAE,wBAA6B,GAC/C,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,mGAAmG;AACnG,wBAAsB,eAAe,CACnC,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,EACrB,EAAE,MAAa,EAAE,GAAE,wBAA6B,GAC/C,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAUlC;AAED,8EAA8E;AAC9E,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,EACrB,EAAE,MAAa,EAAE,GAAE,wBAA6B,GAC/C,OAAO,CAAC,IAAI,CAAC,CASf;AAED,qFAAqF;AACrF,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,wBAAwB,CAEtF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,EACtB,WAAW,EAAE,gBAAgB,EAC7B,EAAE,QAA6B,EAAE,GAAE,uBAA4B,GAC9D,MAAM,CAuBR"}
@@ -1,21 +1,30 @@
1
- import { OAuthConfig } from '../oauth';
2
- import { CookieStore } from './cookie-store';
1
+ import type { MapSession } from '../oauth';
2
+ import type { OAuthConfig } from '../oauth-config';
3
+ import type { CookieStore } from './cookie-store';
4
+ import { type SessionPersistence, type SessionSecret } from './session-persistence';
3
5
  export interface OidcCallbackConfig {
4
6
  /** OAuth/OIDC configuration. */
5
7
  oauthConfig: OAuthConfig;
6
8
  /** Public application URL (used to reconstruct the callback URL and validate redirects). */
7
9
  applicationUrl: string;
8
- /** Cookie store holding the PKCE state and receiving the session cookies. */
10
+ /** Cookie store holding the login transaction and receiving the session cookies. */
9
11
  cookies: CookieStore;
10
12
  /** Optional rate-limit gate. When it resolves false, the handler responds 429. */
11
13
  rateLimit?: () => boolean | Promise<boolean>;
12
- /** Session encryption secret. Defaults to {@link getSessionSecret}. */
13
- secret?: string | Uint8Array;
14
+ /**
15
+ * Opens the login transaction (must match `handleOidcLogin`'s) and encrypts the
16
+ * default cookie persistence. Required even with `persistence`.
17
+ */
18
+ secret: SessionSecret;
19
+ /** Where the session lives. Defaults to the cookies under `secret`; see `serverPersistence`. */
20
+ persistence?: SessionPersistence;
14
21
  /**
15
22
  * Name of the ID token claim that contains user roles.
16
23
  * @default 'roles'
17
24
  */
18
25
  rolesClaim?: string;
26
+ /** Changes the session before it is stored, e.g. to add `data`. See {@link MapSession}. */
27
+ mapSession?: MapSession;
19
28
  /**
20
29
  * Default path to redirect to after login if no returnTo cookie is set.
21
30
  * @default '/'
@@ -1 +1 @@
1
- {"version":3,"file":"oidc-callback.d.ts","sourceRoot":"","sources":["../../src/server/oidc-callback.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAQ5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAKlD,MAAM,WAAW,kBAAkB;IACjC,gCAAgC;IAChC,WAAW,EAAE,WAAW,CAAC;IAEzB,4FAA4F;IAC5F,cAAc,EAAE,MAAM,CAAC;IAEvB,6EAA6E;IAC7E,OAAO,EAAE,WAAW,CAAC;IAErB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAE7B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CAC9C;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,QAAQ,CAAC,CAyFnB"}
1
+ {"version":3,"file":"oidc-callback.d.ts","sourceRoot":"","sources":["../../src/server/oidc-callback.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAM3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AASlD,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,aAAa,EACnB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,WAAW,kBAAkB;IACjC,gCAAgC;IAChC,WAAW,EAAE,WAAW,CAAC;IAEzB,4FAA4F;IAC5F,cAAc,EAAE,MAAM,CAAC;IAEvB,oFAAoF;IACpF,OAAO,EAAE,WAAW,CAAC;IAErB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C;;;OAGG;IACH,MAAM,EAAE,aAAa,CAAC;IAEtB,gGAAgG;IAChG,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAEjC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,2FAA2F;IAC3F,UAAU,CAAC,EAAE,UAAU,CAAC;IAExB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CAC9C;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,QAAQ,CAAC,CA8HnB"}
@@ -1,23 +1,32 @@
1
- import { OAuthConfig } from '../oauth';
2
- import { CookieStore } from './cookie-store';
1
+ import type { OAuthConfig } from '../oauth-config';
2
+ import type { CookieStore } from './cookie-store';
3
+ import { type SessionSecret } from './session-persistence';
3
4
  export interface OidcLoginConfig {
4
5
  /** OAuth/OIDC configuration. */
5
6
  oauthConfig: OAuthConfig;
6
- /** Cookie store used to persist the PKCE state. */
7
+ /** Cookie store that receives the login transaction cookie. */
7
8
  cookies: CookieStore;
9
+ /** Seals the login transaction. Must match the callback's. */
10
+ secret: SessionSecret;
8
11
  /**
9
12
  * Optional rate-limit gate. When it resolves false, the handler responds 429.
10
13
  */
11
14
  rateLimit?: () => boolean | Promise<boolean>;
12
15
  /**
13
16
  * Validates and sanitizes the returnTo parameter, returning a safe path or null.
14
- * @default Only allows relative paths starting with a single `/`.
17
+ * @default Only allows printable-ASCII paths starting with a single `/`.
15
18
  */
16
19
  validateReturnTo?: (rawReturnTo: string) => string | null;
20
+ /**
21
+ * Per-request authorization parameters, merged over `oauthConfig.authorizationParameters`.
22
+ * Reserved keys throw: `state`, `code_challenge`, `code_challenge_method`, `redirect_uri`,
23
+ * `client_id`, `response_type`, `request`, `request_uri` and `nonce`.
24
+ */
25
+ authorizationParameters?: (request: Request) => Record<string, string> | Promise<Record<string, string>>;
17
26
  }
18
27
  /**
19
- * Handles OIDC login initiation: rate-limit, build PKCE, persist it, and redirect
20
- * to the authorization endpoint.
28
+ * Handles OIDC login initiation: rate-limit, build PKCE, seal it into a login
29
+ * transaction, and redirect to the authorization endpoint.
21
30
  */
22
31
  export declare function handleOidcLogin(request: Request, config: OidcLoginConfig): Promise<Response>;
23
32
  //# sourceMappingURL=oidc-login.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"oidc-login.d.ts","sourceRoot":"","sources":["../../src/server/oidc-login.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAIlD,MAAM,WAAW,eAAe;IAC9B,gCAAgC;IAChC,WAAW,EAAE,WAAW,CAAC;IAEzB,mDAAmD;IACnD,OAAO,EAAE,WAAW,CAAC;IAErB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAC3D;AAKD;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,QAAQ,CAAC,CA+BnB"}
1
+ {"version":3,"file":"oidc-login.d.ts","sourceRoot":"","sources":["../../src/server/oidc-login.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAMlD,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAIhF,MAAM,WAAW,eAAe;IAC9B,gCAAgC;IAChC,WAAW,EAAE,WAAW,CAAC;IAEzB,+DAA+D;IAC/D,OAAO,EAAE,WAAW,CAAC;IAErB,8DAA8D;IAC9D,MAAM,EAAE,aAAa,CAAC;IAEtB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAE1D;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,CACxB,OAAO,EAAE,OAAO,KACb,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC/D;AAKD;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,QAAQ,CAAC,CAoCnB"}
@@ -1,14 +1,15 @@
1
- import { OAuthConfig } from '../oauth';
2
- import { CookieStore } from './cookie-store';
3
- export interface OidcLogoutConfig {
1
+ import type { OAuthConfig } from '../oauth-config';
2
+ import type { CookieStore } from './cookie-store';
3
+ import { type SessionPersistenceConfig } from './session-persistence';
4
+ /** {@link handleOidcLogout} config. Takes `secret`, `persistence`, or both. */
5
+ export type OidcLogoutConfig = OidcLogoutBaseConfig & SessionPersistenceConfig;
6
+ interface OidcLogoutBaseConfig {
4
7
  /** OAuth/OIDC configuration. */
5
8
  oauthConfig: OAuthConfig;
6
9
  /** Cookie store holding the session cookies to read and then clear. */
7
10
  cookies: CookieStore;
8
11
  /** Post-logout redirect URI. Must be registered with the provider. */
9
12
  postLogoutRedirectUri: string;
10
- /** Session encryption secret. Defaults to {@link getSessionSecret}. */
11
- secret?: string | Uint8Array;
12
13
  /** Optional rate-limit gate. When it resolves false, the handler responds 429. */
13
14
  rateLimit?: () => boolean | Promise<boolean>;
14
15
  /** Passed through as `state`; verifying it on the way back is the caller's job. */
@@ -41,4 +42,5 @@ export interface OidcLogoutConfig {
41
42
  * unavailable, so the user is always logged out of this application.
42
43
  */
43
44
  export declare function handleOidcLogout(request: Request, config: OidcLogoutConfig): Promise<Response>;
45
+ export {};
44
46
  //# sourceMappingURL=oidc-logout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"oidc-logout.d.ts","sourceRoot":"","sources":["../../src/server/oidc-logout.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AA6BlD,MAAM,WAAW,gBAAgB;IAC/B,gCAAgC;IAChC,WAAW,EAAE,WAAW,CAAC;IAEzB,uEAAuE;IACvE,OAAO,EAAE,WAAW,CAAC;IAErB,sEAAsE;IACtE,qBAAqB,EAAE,MAAM,CAAC;IAE9B,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAE7B,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,QAAQ,CAAC,CAyEnB"}
1
+ {"version":3,"file":"oidc-logout.d.ts","sourceRoot":"","sources":["../../src/server/oidc-logout.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAmB,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AA4BvF,+EAA+E;AAC/E,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GAAG,wBAAwB,CAAC;AAE/E,UAAU,oBAAoB;IAC5B,gCAAgC;IAChC,WAAW,EAAE,WAAW,CAAC;IAEzB,uEAAuE;IACvE,OAAO,EAAE,WAAW,CAAC;IAErB,sEAAsE;IACtE,qBAAqB,EAAE,MAAM,CAAC;IAE9B,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,QAAQ,CAAC,CA0EnB"}