@quatronic/sdk 0.7.0-rc.6 → 0.7.0-rc.7
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.
- package/dist/auth/index.js +1 -1
- package/dist/{chunk-G7VPNC6M.js → chunk-5MNPBJYS.js} +11 -5
- package/dist/{chunk-G7VPNC6M.js.map → chunk-5MNPBJYS.js.map} +1 -1
- package/dist/{chunk-MET4SIMB.js → chunk-EK6TGWWT.js} +8 -2
- package/dist/chunk-EK6TGWWT.js.map +1 -0
- package/dist/{chunk-TEBDKNUB.js → chunk-ROHIN74F.js} +2 -2
- package/dist/{chunk-HQT57ITV.js → chunk-ZZTO5Z3N.js} +2 -2
- package/dist/eslint/index.js +22 -2
- package/dist/eslint/index.js.map +1 -1
- package/dist/next/auth/index.js +1 -1
- package/dist/next/auth/index.js.map +1 -1
- package/dist/next/better-auth/index.js +1 -1
- package/dist/next/connections/index.js +3 -3
- package/dist/next/security/index.js +3 -3
- package/dist/next/sentry/client.d.ts +1 -0
- package/dist/next/sentry/client.js +2 -2
- package/dist/next/sentry/register.js +2 -2
- package/dist/node/connections/index.js +3 -3
- package/dist/node/identity/index.js +2 -2
- package/dist/node/sentry/register.js +2 -2
- package/dist/observability/index.d.ts +10 -0
- package/dist/observability/index.js +7 -0
- package/dist/observability/index.js.map +1 -0
- package/migrations/no-raw-fetch/codemod.mjs +97 -0
- package/migrations/no-raw-fetch/instructions.md +68 -0
- package/migrations/no-raw-fetch/meta.json +6 -0
- package/migrations/report-error/codemod.mjs +94 -0
- package/migrations/report-error/instructions.md +46 -0
- package/migrations/report-error/meta.json +6 -0
- package/package.json +12 -3
- package/dist/chunk-MET4SIMB.js.map +0 -1
- package/dist/next/auth/migrations/0001_init.sql +0 -31
- package/dist/next/auth/migrations/0002_id_token.sql +0 -1
- /package/dist/{chunk-TEBDKNUB.js.map → chunk-ROHIN74F.js.map} +0 -0
- /package/dist/{chunk-HQT57ITV.js.map → chunk-ZZTO5Z3N.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/next/auth/createAuthRouteHandlers.ts","../../../src/auth-core/crypto.ts","../../../src/auth-core/session.ts","../../../src/auth-core/kcClient.ts","../../../src/auth-core/jwks.ts","../../../src/auth-core/refreshStore.ts","../../../src/auth-core/schema.ts","../../../src/auth-core/identityHandoff.ts","../../../src/auth-core/organizationClaim.ts","../../../src/next/auth/loginState.ts","../../../src/auth-core/sanitize.ts","../../../src/next/auth/loginBinding.ts","../../../src/next/auth/refreshWithinSkew.ts","../../../src/next/auth/requireAuth.ts","../../../src/next/auth/appAccess.ts","../../../src/auth-core/roles.ts","../../../src/auth-core/permissions.ts","../../../src/next/auth/types.ts","../../../src/next/auth/authSpan.ts","../../../src/next/auth/requireAuthFor.ts","../../../src/next/auth/getAccessToken.ts","../../../src/auth-core/migrate.ts","../../../src/next/auth/permissionGuards.ts","../../../src/next/auth/withConnectionToken.ts"],"sourcesContent":["import { NextRequest, NextResponse } from \"next/server\"\nimport { eq } from \"drizzle-orm\"\nimport { decodeJwt } from \"jose\"\nimport { createCipher } from \"../../auth-core/crypto\"\nimport { sessionCookie } from \"../../auth-core/session\"\nimport { createKcClient } from \"../../auth-core/kcClient\"\nimport { createJwks } from \"../../auth-core/jwks\"\nimport { createRefreshStore } from \"../../auth-core/refreshStore\"\nimport { createIdentityHandoff } from \"../../auth-core/identityHandoff\"\nimport { loginState } from \"../../auth-core/schema\"\nimport { parseOrganizationClaim } from \"../../auth-core/organizationClaim\"\nimport { randomB64Url, sha256B64Url, sanitizeRedirect, sanitizeLoginHint, sanitizeIdpHint } from \"./loginState\"\nimport { readBoundStates, appendBoundState, removeBoundState } from \"./loginBinding\"\nimport { refreshWithinSkew } from \"./refreshWithinSkew\"\nimport { AuthError } from \"./requireAuth\"\nimport { rolesForClient, type ClaimsWithAccess } from \"./appAccess\"\nimport { extractRoles, decodeJwtPayload } from \"../../auth-core/roles\"\nimport { permissionsForRoles, type PermissionMap } from \"../../auth-core/permissions\"\nimport {\n DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n type NextAuthOptions,\n} from \"./types\"\nimport { getLogger } from \"../../telemetry-hooks\"\nimport { runAuthSpan } from \"./authSpan\"\n\nexport type AuthRouteHandlers = {\n GET: (req: NextRequest) => Promise<NextResponse>\n POST: (req: NextRequest) => Promise<NextResponse>\n}\n\n/** Clock-skew allowance between our `login_state.created_at` and Keycloak's\n * `auth_time`. Both are server clocks; 5s is generous. */\nconst AUTH_TIME_SKEW_SEC = 5\n\n/** The SDK owns no UI, so the default is the app's own login page with a reason\n * in the query. Consumers that have a real interstitial override it. */\nconst DEFAULT_IDENTITY_MISMATCH_PATH = \"/login?reason=identity_mismatch\"\n\n/** Where Keycloak sends the browser after ending the SSO session. */\nconst LOGGED_OUT_PATH = \"/api/auth/logged-out\"\n/** Only \"cookies\" — see the note on `handleLoggedOut` for the measurements that\n * ruled out \"cache\", and why \"storage\" was never in scope. */\nconst CLEAR_SITE_DATA = '\"cookies\"'\n\nfunction segment(req: NextRequest): string {\n const parts = req.nextUrl.pathname.split(\"/\").filter(Boolean)\n return parts[parts.length - 1] ?? \"\"\n}\n\n/** A `/me` response describes exactly one identity. Without `no-store` a browser\n * may replay it on history navigation — which is how pressing \"back\" showed a\n * colleague's account on 2026-08-04 — and without `Vary: Cookie` any shared\n * cache would key it by URL alone, i.e. by nothing that identifies the user. */\nfunction noStore<T extends NextResponse>(res: T): T {\n res.headers.set(\"Cache-Control\", \"no-store\")\n res.headers.set(\"Vary\", \"Cookie\")\n return res\n}\n\nexport async function createAuthRouteHandlers(\n opts: NextAuthOptions,\n): Promise<AuthRouteHandlers> {\n const cipher = await createCipher(opts.sessionCookieSecret)\n const cookies = sessionCookie({ secret: opts.sessionCookieSecret })\n const refreshStore = createRefreshStore({ db: opts.db, cipher })\n // Same secret as the session cookie, a different derived key (distinct HKDF\n // info) — this cookie authorizes a different thing and must not be\n // interchangeable with a session cookie.\n const handoff = await createIdentityHandoff({ secret: opts.sessionCookieSecret })\n const kcClient = createKcClient({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n clientId: opts.clientId,\n clientSecret: opts.clientSecret,\n publicOrigin: opts.publicOrigin,\n orgAlias: opts.orgAlias,\n organizationScope: opts.organizationScope,\n })\n const jwks = createJwks({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n audience: opts.clientId,\n })\n\n const ctx = { cookies, refreshStore, handoff, kcClient, jwks, db: opts.db, publicOrigin: opts.publicOrigin, clientId: opts.clientId, requireAppRole: opts.requireAppRole !== false, orgAlias: opts.orgAlias, permissions: opts.permissions, identityMismatchPath: opts.identityMismatchPath, idleTimeoutSec: opts.sessionIdleTimeoutSec ?? DEFAULT_SESSION_IDLE_TIMEOUT_SEC, absoluteMaxSec: opts.sessionAbsoluteMaxSec ?? DEFAULT_SESSION_ABSOLUTE_MAX_SEC }\n\n async function GET(req: NextRequest): Promise<NextResponse> {\n switch (segment(req)) {\n case \"login\":\n return handleLogin(req, ctx)\n case \"me\":\n return handleMe(req, ctx)\n case \"callback\":\n return handleCallback(req, ctx)\n case \"logout\":\n return handleLogoutRedirect(req, ctx)\n case \"logged-out\":\n return handleLoggedOut(req, ctx)\n case \"identity-mismatch\":\n return handleIdentityMismatch(req, ctx)\n default:\n return NextResponse.json({ error: \"not_found\" }, { status: 404 })\n }\n }\n\n async function POST(req: NextRequest): Promise<NextResponse> {\n switch (segment(req)) {\n case \"refresh\":\n return handleRefresh(req, ctx)\n case \"logout\":\n return handleLogout(req, ctx)\n case \"backchannel-logout\":\n return handleBackchannelLogout(req, ctx)\n case \"confirm-identity\":\n return handleConfirmIdentity(req, ctx)\n default:\n return NextResponse.json({ error: \"not_found\" }, { status: 404 })\n }\n }\n\n return { GET, POST }\n}\n\ntype Ctx = {\n cookies: ReturnType<typeof sessionCookie>\n refreshStore: ReturnType<typeof createRefreshStore>\n handoff: Awaited<ReturnType<typeof createIdentityHandoff>>\n kcClient: ReturnType<typeof createKcClient>\n jwks: ReturnType<typeof createJwks>\n db: NextAuthOptions[\"db\"]\n publicOrigin: string\n clientId: string\n requireAppRole: boolean\n orgAlias?: string\n permissions?: PermissionMap\n identityMismatchPath?: string\n idleTimeoutSec: number\n absoluteMaxSec: number\n}\n\nasync function handleLogin(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\n \"Log in\",\n {\n \"qore.auth.event\": \"login_started\",\n ...(ctx.orgAlias ? { \"qore.auth.org_alias\": ctx.orgAlias } : {}),\n },\n async () => {\n const log = getLogger()\n const redirect = sanitizeRedirect(req.nextUrl.searchParams.get(\"redirect\"))\n const loginHint = sanitizeLoginHint(req.nextUrl.searchParams.get(\"login_hint\"))\n const kcIdpHint = sanitizeIdpHint(req.nextUrl.searchParams.get(\"kc_idp_hint\"))\n\n // \"Sign in as someone else\" is just a new login. Whatever was parked for\n // confirmation is abandoned here rather than left waiting: a pending row\n // that outlives the choice it was created for is a login somebody could\n // still complete.\n const stale = await ctx.handoff.read(req.headers.get(\"cookie\"))\n if (stale) await ctx.refreshStore.revokeSession(stale.sessionId)\n\n const state = randomB64Url(32)\n const codeVerifier = randomB64Url(48)\n const codeChallenge = await sha256B64Url(codeVerifier)\n const nonce = randomB64Url(32)\n\n // Set only when THIS request is our own restart hitting /api/auth/login\n // (?retried=1) after the callback bounced Keycloak's authentication_expired\n // / login_required back to us. Stamped on the row rather than carried on\n // the KC authorize URL: the callback's redirect_uri is fixed by Keycloak,\n // which never echoes our own query parameters back to it, so a guard read\n // off the callback's query string alone could never fire in production.\n const retried = req.nextUrl.searchParams.get(\"retried\") === \"1\"\n\n const sessionId = await ctx.cookies.read(req.headers.get(\"cookie\"))\n const liveSession = sessionId ? await ctx.refreshStore.getSession(sessionId) : null\n // Re-authentication is forced by an explicit ?prompt=login — our own\n // restart after Keycloak returned authentication_expired / login_required,\n // but also any other caller that sends it directly (e.g. the SSO wizard's\n // test login, kc_idp_hint=...&prompt=login with no retried flag) — and by\n // staleSessionCookie below.\n //\n // A typed login_hint and a refresh-expired-but-present session do not\n // force it: both write an expectation onto the row below (expectedEmail\n // from the hint, expectedSub from the session — getSession filters only\n // revokedAt and pendingIdentityConfirm, so a refresh-expired row still\n // yields one), and the callback refuses to adopt an identity that does\n // not match it. That is the guard against the 2026-08-04 identity\n // substitution: not \"never let the SSO cookie answer\" but \"check whose\n // answer came back\", which protects without making every user\n // re-authenticate at their upstream IdP.\n //\n // A session cookie whose row is missing, revoked, or parked awaiting\n // identity confirmation (getSession hides all three) forces\n // re-authentication regardless of loginHint: even with a hint,\n // expectedEmail is written onto the row below, but that only lets the\n // callback catch a mismatch after the round trip, so Keycloak is still\n // made to re-assert who is arriving up front rather than trust its SSO\n // cookie. It is not the only expectation-less login either — a request\n // with no session cookie at all and no hint also records nothing below,\n // but that case is mitigated by prompt=select_account instead (see\n // hasExpectation), not by forcing. Without forcing here, a stale\n // session cookie would let Keycloak's SSO cookie silently name whoever\n // it likes, which is the 2026-08-04 shape.\n //\n // Deliberately NOT the old sessionIsDead condition: a row that is\n // present but refresh-expired still yields expectedSub, so the callback\n // verifies it. It falls through to the id_token_hint branch only when\n // that row actually has an idToken — sessions.id_token is nullable, so\n // without one it falls through further, to sending nothing, and still\n // relies on expectedSub for verification.\n const staleSessionCookie = Boolean(sessionId) && !liveSession\n const forcedReauth =\n req.nextUrl.searchParams.get(\"prompt\") === \"login\" || staleSessionCookie\n\n // Whether this login has anything the callback can check the returned\n // identity against: the subject of the session being replaced, or the\n // address the user typed. Logged for triage; it no longer decides the\n // prompt (see below).\n const hasExpectation = Boolean(liveSession?.userSub) || Boolean(loginHint)\n\n // What the authorize URL actually carries, computed once so the URL and\n // the log below cannot disagree. Three outcomes: forcedReauth wins\n // outright; otherwise a live id token asks Keycloak to confirm the\n // existing session; otherwise the IdP is asked to let the user pick an\n // account.\n //\n // That last branch used to be reached only when there was no\n // expectation to verify, so a typed address suppressed the picker. It\n // no longer does. An address says which account the user meant, not\n // that they never want a different one, and someone who has several\n // accounts at the same IdP had no way to reach the others without\n // logging out first. Upstream the two compose rather than conflict:\n // Entra's picker lists the signed-in accounts plus \"use another\n // account\", and login_hint pre-fills the sign-in field behind it.\n //\n // This costs a click on a login that verification already protected, so\n // it is not a security fix — it is a deliberate trade of one click for\n // the ability to choose. It stops short of prompt=login, which would\n // negate SSO and cost a full re-authentication.\n const idTokenHint = liveSession?.idToken ?? undefined\n const authorizePrompt = forcedReauth\n ? \"forced\"\n : idTokenHint\n ? \"id_token_hint\"\n : \"select_account\"\n\n await ctx.db.insert(loginState).values({\n state,\n codeVerifier,\n redirectPath: redirect,\n expiresAt: new Date(Date.now() + 10 * 60_000),\n forcedReauth,\n nonce,\n retried,\n // Whom this login is supposed to end up as. `liveSession` is non-null\n // even for a refresh-expired row (getSession only filters revokedAt\n // and pendingIdentityConfirm), so the common \"session timed out\" case\n // still carries an expectation.\n //\n // A revoked or absent row (with a session cookie present) yields no\n // expected subject, and that is exactly the case `staleSessionCookie`\n // above forces re-authentication for instead: with nothing here for\n // the callback to verify, Keycloak itself is made to re-assert who is\n // arriving. An explicit `GET /logout` clears the session cookie, so\n // that case has no cookie at login time either — no expectedSub, no\n // staleSessionCookie, and `select_account` (not forcing) covers it,\n // same as any other cookie-less login. If a session cookie somehow\n // survives a logout regardless, `staleSessionCookie` still forces,\n // same as any other cookie-with-no-live-row case. A login with no\n // session cookie and no typed email carries no expectation and is\n // not forced: a browser with no cookie is never covered by\n // `staleSessionCookie` above, which only fires when a session cookie\n // is present.\n expectedSub: liveSession?.userSub ?? null,\n expectedEmail: loginHint ?? null,\n })\n\n // Diagnostic: pairs with auth.login.succeeded via `state`. A second\n // auth.login.started for one user-initiated login — especially with\n // had_session=true or a referer of an in-app page — reveals a spurious\n // re-login round-trip (e.g. a redirect loop), which logs as two \"succeeded\".\n log.info(\n {\n event: \"auth.login.started\",\n state,\n redirect_path: redirect,\n has_login_hint: Boolean(loginHint),\n had_session: Boolean(sessionId),\n forced_reauth: forcedReauth,\n has_expectation: hasExpectation,\n prompt_sent: authorizePrompt,\n referer: req.headers.get(\"referer\") ?? undefined,\n },\n \"auth login started\",\n )\n\n const url = ctx.kcClient.buildAuthorizeUrl({\n state,\n codeChallenge,\n nonce,\n loginHint,\n kcIdpHint,\n ...(authorizePrompt === \"forced\"\n ? { prompt: \"login\" as const, maxAge: 0 }\n : // Only useful when we are NOT forcing: it asks the AS to confirm the\n // session is still this user's, and returns login_required if not.\n // Alongside prompt=login it adds nothing — that already re-prompts.\n // This is the path that moves the idle-timeout boundary: a refresh-\n // expired-but-present app session sends id_token_hint instead of\n // forcing, so if Keycloak's own SSO session is still alive the user\n // is logged back in silently as the same user, and Keycloak's SSO\n // Session Idle becomes the effective idle re-authentication\n // boundary in place of this app's refresh lifetime. See\n // docs/LOGBOOK.md for the trade-off this makes.\n authorizePrompt === \"id_token_hint\"\n ? { idTokenHint }\n : { prompt: \"select_account\" as const }),\n })\n const res = NextResponse.redirect(url, 302)\n res.headers.append(\"Set-Cookie\", appendBoundState(req.headers.get(\"cookie\"), state))\n // Unconditional: the browser must not carry a handoff into the new login\n // either way, and clearing a cookie that was never there costs nothing.\n res.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return res\n },\n )\n}\n\nasync function handleMe(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n try {\n return noStore(await handleMeInner(req, ctx))\n } catch (err) {\n // Defensive: any unexpected error — a corrupt stored access token making\n // decodeJwt() throw, a transient refresh-store failure — must not escape as\n // a bare unhandled 500. Log it with the stack so it stays diagnosable, and\n // return a controlled response. This is not a dead session, so the cookie is\n // left intact (no logout loop on a transient blip).\n getLogger().error({ err }, \"auth /me failed unexpectedly\")\n return noStore(NextResponse.json({ error: \"internal\" }, { status: 500 }))\n }\n}\n\nasync function handleMeInner(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n const id = await ctx.cookies.read(req.headers.get(\"cookie\"))\n if (!id) return NextResponse.json({ error: \"unauthenticated\" }, { status: 401 })\n const existing = await ctx.refreshStore.getSession(id)\n if (!existing) return NextResponse.json({ error: \"unauthenticated\" }, { status: 401 })\n\n let session\n try {\n session = await refreshWithinSkew(\n {\n refreshStore: ctx.refreshStore,\n kcClient: ctx.kcClient,\n skewSec: 30,\n orgAlias: ctx.orgAlias,\n idleTimeoutSec: ctx.idleTimeoutSec,\n absoluteMaxSec: ctx.absoluteMaxSec,\n },\n id,\n existing,\n )\n } catch (err) {\n if (!(err instanceof AuthError)) throw err\n if (err.reason === \"refresh_unavailable\") {\n // Keycloak hiccup, not a dead session: let the client retry; do NOT\n // clear the cookie or the user gets logged out by a transient blip.\n return NextResponse.json({ error: \"refresh_unavailable\" }, { status: 503 })\n }\n // Dead session: refreshWithinSkew already revoked the row. Clear the\n // cookie so /login stops seeing us as authenticated and stops looping.\n const res = NextResponse.json({ error: \"session_expired\" }, { status: 401 })\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n return res\n }\n\n // nosemgrep: qdc.jwt.unverified-decode -- access token was read out of this app's own server-side session store, not from the request\n const claims = decodeJwt(session.accessToken) as ClaimsWithAccess\n if (ctx.requireAppRole && rolesForClient(claims, ctx.clientId).length === 0) {\n return NextResponse.json({ error: \"no_app_access\" }, { status: 403 })\n }\n\n return NextResponse.json({\n user: { id: session.userSub, email: session.userEmail },\n organization: { id: session.organizationId, alias: session.organizationAlias },\n permissions: resolveMePermissions(ctx.permissions, session.accessToken, ctx.clientId),\n })\n}\n\nconst BACKCHANNEL_EVENT = \"http://schemas.openid.net/event/backchannel-logout\"\n\nasync function handleRefresh(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\"Refresh session\", {}, async (auth) => {\n const id = await ctx.cookies.read(req.headers.get(\"cookie\"))\n if (!id) {\n auth.set(\"qore.auth.event\", \"refresh_skipped\")\n return NextResponse.json({ error: \"unauthenticated\" }, { status: 401 })\n }\n const session = await ctx.refreshStore.getSession(id)\n if (!session) {\n auth.set(\"qore.auth.event\", \"refresh_skipped\")\n return NextResponse.json({ error: \"unauthenticated\" }, { status: 401 })\n }\n try {\n await refreshWithinSkew(\n {\n refreshStore: ctx.refreshStore,\n kcClient: ctx.kcClient,\n skewSec: 30,\n orgAlias: ctx.orgAlias,\n idleTimeoutSec: ctx.idleTimeoutSec,\n absoluteMaxSec: ctx.absoluteMaxSec,\n },\n id,\n session,\n )\n } catch (err) {\n if (!(err instanceof AuthError)) throw err\n auth.set(\"qore.auth.event\", \"refresh_failed\")\n if (err.reason === \"refresh_unavailable\") {\n auth.fail(\"refresh_unavailable\")\n return NextResponse.json({ error: \"refresh_unavailable\" }, { status: 503 })\n }\n auth.fail(\"session_expired\")\n const res = NextResponse.json({ error: \"session_expired\" }, { status: 401 })\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n return res\n }\n auth.set(\"qore.auth.event\", \"session_refreshed\")\n return new NextResponse(null, { status: 204 })\n })\n}\n\nasync function handleLogout(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\"Log out\", { \"qore.auth.event\": \"logout\", \"qore.auth.method\": \"post\" }, async () => {\n const log = getLogger()\n const id = await ctx.cookies.read(req.headers.get(\"cookie\"))\n if (id) {\n const session = await ctx.refreshStore.getSession(id)\n if (session) {\n try { await ctx.kcClient.logout(session.refreshToken) } catch { /* best-effort */ }\n await ctx.refreshStore.revokeSession(id)\n log.info(\n {\n event: \"auth.logout\",\n method: \"post\",\n user_sub: session.userSub,\n user_email: session.userEmail,\n org_id: session.organizationId,\n org_alias: session.organizationAlias,\n },\n \"auth logout\",\n )\n }\n }\n const res = new NextResponse(null, { status: 204 })\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n // Site data is cleared by GET /api/auth/logged-out, not here — see the note\n // on CLEAR_SITE_DATA. A caller navigates the moment this resolves, and a\n // clear blocks whatever navigation is waiting on the response that carries\n // it.\n return res\n })\n}\n\nasync function handleLogoutRedirect(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\"Log out\", { \"qore.auth.event\": \"logout\", \"qore.auth.method\": \"redirect\" }, async () => {\n const log = getLogger()\n const id = await ctx.cookies.read(req.headers.get(\"cookie\"))\n let idTokenHint: string | undefined\n if (id) {\n const session = await ctx.refreshStore.getSession(id)\n if (session) {\n idTokenHint = session.idToken ?? undefined\n try { await ctx.kcClient.logout(session.refreshToken) } catch { /* best-effort */ }\n await ctx.refreshStore.revokeSession(id)\n log.info(\n {\n event: \"auth.logout\",\n method: \"redirect\",\n user_sub: session.userSub,\n user_email: session.userEmail,\n org_id: session.organizationId,\n org_alias: session.organizationAlias,\n },\n \"auth logout\",\n )\n }\n // A logout that arrives a second time — the user clicked again, the page\n // was reloaded, the first attempt already revoked the row — finds nothing\n // above. Without a hint Keycloak asks the user to confirm, and a\n // confirmation page is a place where ending the SSO session can simply\n // not happen. The ID token of a revoked row is still a truthful statement\n // about who was logged in, and that is all a hint has to be.\n idTokenHint ??= (await ctx.refreshStore.getIdTokenHint(id)) ?? undefined\n }\n const rawRedirect = req.nextUrl.searchParams.get(\"redirect\")\n const target = rawRedirect ? sanitizeRedirect(rawRedirect) : \"/login\"\n // Come back through our own terminal page rather than straight to `target`:\n // that page is the last stop of the chain and the only response that can\n // afford to clear site data. Keycloak accepts it without a config change —\n // both the portal client and the per-app clients register post-logout\n // redirect URIs as `{origin}/*`.\n const landing = new URL(LOGGED_OUT_PATH, ctx.publicOrigin)\n landing.searchParams.set(\"redirect\", target)\n const endSessionUrl = ctx.kcClient.buildEndSessionUrl({\n postLogoutRedirectUri: landing.toString(),\n idTokenHint,\n })\n const res = NextResponse.redirect(endSessionUrl, 302)\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n return res\n })\n}\n\n/** The browser lands here from Keycloak with the logout already done, so nothing\n * is queued behind this response and it is safe to spend time clearing.\n *\n * Measured 2026-08-10 against a real, heavily used Chrome profile: clearing\n * blocks whatever navigation is waiting on the response that carries the\n * header, and the wait scales with how much there is to clear. On the 302 that\n * cost showed up as a logout that never arrived (34s in staging before the user\n * gave up). The same clear on a terminal page costs 74ms — *without* \"cache\".\n * With \"cache\" it was 93_174ms on the same page, which is why only \"cookies\"\n * survives here; \"cache\" also reaches far past the HTTP cache (bfcache,\n * prerender, script and shader caches), and `noStore()` is what actually keeps\n * a personalized response from being replayed on history navigation.\n *\n * Still deliberately not \"storage\": that is origin-wide and would wipe every\n * tenant app's localStorage/IndexedDB on every logout. */\nasync function handleLoggedOut(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n const target = sanitizeRedirect(req.nextUrl.searchParams.get(\"redirect\"))\n // `target` is already a same-origin path, but it still lands inside an HTML\n // attribute and a JS string literal, so escape it for each context.\n const attr = target.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/\"/g, \""\")\n const literal = JSON.stringify(target).replace(/</g, \"\\\\u003c\")\n const res = new NextResponse(\n `<!doctype html><html><head><meta charset=\"utf-8\">` +\n `<meta http-equiv=\"refresh\" content=\"0;url=${attr}\">` +\n `<title>Signing out…</title></head><body>` +\n `<p>Signing out… <a href=\"${attr}\">Continue</a></p>` +\n `<script>location.replace(${literal})</script>` +\n `</body></html>`,\n { status: 200, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } },\n )\n res.headers.set(\"Cache-Control\", \"no-store\")\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n res.headers.set(\"Clear-Site-Data\", CLEAR_SITE_DATA)\n return res\n}\n\nasync function handleBackchannelLogout(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\n \"Log out (backchannel)\",\n { \"qore.auth.event\": \"logout\", \"qore.auth.method\": \"backchannel\" },\n async (auth) => {\n const log = getLogger()\n const form = await req.formData()\n const logoutToken = form.get(\"logout_token\")\n if (typeof logoutToken !== \"string\") {\n auth.fail(\"missing_logout_token\")\n return new NextResponse(\"missing logout_token\", { status: 400 })\n }\n let claims: { sub?: string; events?: Record<string, unknown> }\n try {\n claims = (await ctx.jwks.verify(logoutToken)) as never\n } catch {\n auth.fail(\"invalid_logout_token\")\n return new NextResponse(\"invalid logout_token\", { status: 400 })\n }\n if (!claims.events || !(BACKCHANNEL_EVENT in claims.events)) {\n auth.fail(\"invalid_events\")\n return new NextResponse(\"invalid logout_token events\", { status: 400 })\n }\n if (!claims.sub) {\n auth.fail(\"missing_sub\")\n return new NextResponse(\"missing sub\", { status: 400 })\n }\n await ctx.refreshStore.revokeByUser(claims.sub)\n log.info({ event: \"auth.logout\", method: \"backchannel\", user_sub: claims.sub }, \"auth logout\")\n return new NextResponse(\"ok\", { status: 200 })\n },\n )\n}\n\n/** What the interstitial needs to render, and nothing more: two email\n * addresses. The pending session id is deliberately not returned — the browser\n * never needs it, and the handoff cookie is the only thing allowed to name it. */\nasync function handleIdentityMismatch(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n const handoff = await ctx.handoff.read(req.headers.get(\"cookie\"))\n if (!handoff) {\n return noStore(NextResponse.json({ error: \"no_pending_identity\" }, { status: 404 }))\n }\n const pending = await ctx.refreshStore.getPendingSession(handoff.sessionId)\n if (!pending) {\n return noStore(NextResponse.json({ error: \"no_pending_identity\" }, { status: 404 }))\n }\n return noStore(\n NextResponse.json({\n expected: { email: handoff.expectedEmail },\n actual: { email: pending.userEmail },\n }),\n )\n}\n\nasync function handleConfirmIdentity(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\n \"Confirm identity\",\n { \"qore.auth.event\": \"identity_confirmed\" },\n async (auth) => {\n const log = getLogger()\n // No CSRF token needed and none invented: this is a POST, and the handoff\n // cookie is SameSite=Lax, so a cross-site POST arrives without it and\n // lands here as 409. The cookie is both the authorization and the pointer.\n const handoff = await ctx.handoff.read(req.headers.get(\"cookie\"))\n if (!handoff) {\n auth.fail(\"no_pending_identity\")\n return NextResponse.json({ error: \"no_pending_identity\" }, { status: 409 })\n }\n const pending = await ctx.refreshStore.getPendingSession(handoff.sessionId)\n if (!pending) {\n auth.fail(\"no_pending_identity\")\n const res = NextResponse.json({ error: \"no_pending_identity\" }, { status: 409 })\n res.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return res\n }\n\n // The callback parked this row BEFORE it ran requireAppRole — Task 3 puts\n // the mismatch branch in front of that check on purpose, so an identity\n // the login did not expect is never measured for app access, because a\n // /no-access answer would itself disclose that the account exists here.\n // The consequence is that nothing has yet asked whether this person may\n // use the app, and confirming without asking would hand a session to\n // somebody the callback would have bounced. So the same check runs here,\n // against the same access token, and refuses in the same terms. Nothing\n // is disclosed that an ordinary login by this person would not have\n // disclosed: by this point the interstitial has already shown who came\n // back, and the answer is about the identity now being adopted.\n if (ctx.requireAppRole) {\n // nosemgrep: qdc.jwt.unverified-decode -- pending row's access token was stored by this handler after its own token exchange\n const claims = decodeJwt(pending.accessToken) as ClaimsWithAccess\n if (rolesForClient(claims, ctx.clientId).length === 0) {\n // Revoke rather than leave it pending: the choice this row existed\n // for has been made, and it must not stay confirmable behind the\n // refusal.\n await ctx.refreshStore.revokeSession(handoff.sessionId)\n log.warn(\n {\n event: \"auth.identity.confirm_failed\",\n reason: \"no_app_access\",\n user_sub: pending.userSub,\n user_email: pending.userEmail ?? undefined,\n },\n \"auth identity confirm failed\",\n )\n auth.fail(\"no_app_access\")\n const denied = NextResponse.json({ error: \"no_app_access\" }, { status: 403 })\n denied.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return denied\n }\n }\n\n if (!(await ctx.refreshStore.confirmSession(handoff.sessionId))) {\n // Lost a race, or the row was revoked between the two reads. Returning\n // false here is what stops a replay minting a second session cookie.\n auth.fail(\"no_pending_identity\")\n const res = NextResponse.json({ error: \"no_pending_identity\" }, { status: 409 })\n res.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return res\n }\n log.info(\n {\n event: \"auth.identity.confirmed\",\n user_sub: pending.userSub,\n user_email: pending.userEmail,\n expected_email: handoff.expectedEmail ?? undefined,\n },\n \"auth identity confirmed\",\n )\n const res = new NextResponse(null, { status: 204 })\n // The cookie cannot outlive the row behind it: past the refresh token's\n // own expiry there is nothing left to refresh, so a longer cookie would\n // only produce the \"cookie alive, session dead\" state again.\n res.headers.append(\n \"Set-Cookie\",\n await ctx.cookies.issue(handoff.sessionId, {\n expiresAt: pending.refreshExpiresAt,\n }),\n )\n res.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return res\n },\n )\n}\n\n/** Resolve the permissions a token grants under the given map. Returns [] when\n * no map is configured (identity-only consumers). Exported for unit testing. */\nexport function resolveMePermissions(\n map: PermissionMap | undefined,\n accessToken: string,\n clientId: string,\n): string[] {\n if (!map) return []\n const roles = extractRoles(decodeJwtPayload(accessToken), clientId)\n return permissionsForRoles(map, roles)\n}\n\nasync function handleCallback(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\"Complete login\", {}, async (auth) => {\n const log = getLogger()\n const code = req.nextUrl.searchParams.get(\"code\")\n const state = req.nextUrl.searchParams.get(\"state\")\n\n // Every exit below the browser-binding check consumes the transaction:\n // the row leaves `login_state` and the state leaves the browser's\n // qdc_login cookie. Defined this early (rather than only after the\n // code/state check) so the OIDC-error branch — which never has a `code`\n // — can use it too; a restart that left a stale state behind would waste\n // one of the three binding slots for no benefit once the row itself is\n // gone. Tolerant of a missing `state`: consume() is simply a no-op then,\n // since there is nothing to clear.\n //\n // The one exception is the binding check itself, below: an unbound state\n // is not proven to belong to this browser, so it returns directly\n // without ever touching the row or the cookie — that is the entire point\n // of the check.\n const consume = async (res: NextResponse): Promise<NextResponse> => {\n if (state) {\n await ctx.db.delete(loginState).where(eq(loginState.state, state))\n res.headers.append(\"Set-Cookie\", removeBoundState(req.headers.get(\"cookie\"), state))\n }\n return res\n }\n\n // RFC 9700 §2.1.1 — the transaction must be bound to the user agent that\n // started it. Without this, whoever opens the callback URL with a stolen\n // (non-secret — it appears in history/referrers) `state` could delete\n // another transaction's row and force this browser into an unrequested\n // redirect. Returns null when there is nothing to check (no `state` at\n // all — e.g. an OIDC error with no `state` echoed back), letting the\n // caller fall through to its normal missing-state handling. Must run\n // before ANY branch reads or deletes the row, which is why it is called\n // both at the top of the OIDC-error branch below and again ahead of the\n // ordinary row lookup further down.\n const checkBinding = (): NextResponse | null => {\n if (!state) return null\n if (readBoundStates(req.headers.get(\"cookie\")).includes(state)) return null\n log.warn({ event: \"auth.login.failed\", reason: \"state_not_bound\" }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"state_not_bound\")\n return new NextResponse(\"state_not_bound\", { status: 400 })\n }\n\n // Keycloak's authorization-error response: `error` (+ optionally\n // `error_description`, and usually `state`) instead of `code`. Read this\n // BEFORE the `missing code or state` check below — every KC error has no\n // `code`, so without this branch every one of them fell into that generic\n // 400 and logged as the same undiagnosable `missing_params` (production:\n // seven failed logins in ten minutes, all logged as `missing_params`, the\n // real reason — `already_logged_in` — visible only in the KC admin\n // console; qdc-roadmap#136).\n const oidcError = req.nextUrl.searchParams.get(\"error\")\n if (oidcError) {\n // Gate this branch on the binding check too: a `state` on an error\n // callback is exactly as forgeable as one on a success callback, and\n // this branch is the one that reads and deletes the row. When the\n // binding cookie has itself expired (Max-Age 600s) before an error\n // callback arrives, this now yields state_not_bound instead of the\n // auto-restart — fail-closed, and the user simply sees a failed-login\n // page instead of being bounced back to /login; they can retry by hand.\n const bindingFailure = checkBinding()\n if (bindingFailure) return bindingFailure\n\n const description = req.nextUrl.searchParams.get(\"error_description\") ?? undefined\n log.warn(\n { event: \"auth.login.failed\", reason: oidcError, error_description: description },\n \"auth login failed\",\n )\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(oidcError)\n\n // authentication_expired / login_required are Keycloak's explicit\n // \"start over\" signals, not \"show the user a 400\". One restart only:\n // the bound comes from `login_state.retried`, NOT from a `retried`\n // query parameter echoed back on this callback — the callback's\n // `redirect_uri` is fixed by Keycloak, which never echoes our own query\n // parameters back to it, so a guard read only off this URL could never\n // fire in production and the redirect would bounce between the two\n // servers forever. `handleLogin` stamps `retried: true` on the NEW row\n // it inserts when ITS OWN request carries `retried=1` — that request is\n // our own restart hitting /api/auth/login, so the parameter really is\n // present there. The query-string check below is kept anyway (the\n // brief's tests rely on it, and it costs nothing): it is a\n // belt-and-braces check, not the real bound.\n const restartable = oidcError === \"authentication_expired\" || oidcError === \"login_required\"\n const queryRetried = req.nextUrl.searchParams.get(\"retried\") === \"1\"\n\n let alreadyRetried = queryRetried\n let retryRow: typeof loginState.$inferSelect | undefined\n if (!state) {\n // No `state` at all means there is no row to consult and nothing to\n // bind a restart to. Defaulting to \"allow a restart\" here is exactly\n // the unbounded case this guard exists to prevent, so a missing\n // transaction is treated the same as an already-retried one: fail\n // closed rather than license a blind restart.\n alreadyRetried = true\n } else {\n const [row] = await ctx.db\n .select().from(loginState).where(eq(loginState.state, state)).limit(1)\n // Same reasoning for a `state` that IS present but matches no row\n // (expired, already consumed by an earlier exit, or simply invalid):\n // we cannot prove this is a fresh, first attempt, so we do not grant\n // another restart on the strength of a row we can't read.\n if (!row || row.retried) alreadyRetried = true\n retryRow = row\n }\n\n if (restartable && !alreadyRetried) {\n // Carry the original login's destination and typed address through\n // the restart, or this bounces the user to \"/\" and drops the address\n // they typed instead of resuming what they asked for — the ordinary\n // outcome now that a refresh-expired session (routine, not an odd\n // path) sends id_token_hint and lands here whenever Keycloak's own\n // SSO session is also dead. `retryRow` is defined whenever this\n // branch is reached (alreadyRetried is only false when `state` was\n // present and its row existed and was unread), so this is not a\n // fallback for a missing row, just satisfying the type.\n const restart = new URL(\"/api/auth/login\", ctx.publicOrigin)\n restart.searchParams.set(\"prompt\", \"login\")\n restart.searchParams.set(\"retried\", \"1\")\n if (retryRow?.redirectPath) restart.searchParams.set(\"redirect\", retryRow.redirectPath)\n if (retryRow?.expectedEmail) restart.searchParams.set(\"login_hint\", retryRow.expectedEmail)\n return await consume(NextResponse.redirect(restart, 302))\n }\n return await consume(new NextResponse(`login failed: ${oidcError}`, { status: 400 }))\n }\n\n // RFC 9700 §2.1.1 — the transaction must be bound to the user agent that\n // started it. Without this, whoever opens the callback URL gets the\n // session — or, on the `missing_params` branch just below, gets to\n // delete another transaction's row by supplying its `state` with no\n // `code`. Must run BEFORE that branch, not after: `missing_params` also\n // calls `consume()`, and a check placed after an early return is dead\n // code. (checkBinding() is a no-op when `state` is absent, so this does\n // not disturb the ordinary missing-both-params case.) The `oidcError`\n // branch above already ran its own copy of this same check ahead of\n // touching the row, so this second call is unreachable whenever `error`\n // was present — it exists for every other request shape.\n const bindingFailure = checkBinding()\n if (bindingFailure) return bindingFailure\n\n if (!code || !state) {\n log.warn({ event: \"auth.login.failed\", reason: \"missing_params\" }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"missing_params\")\n // A bound `state` with no `code` (and no `error`) is a leftover\n // transaction that will never be finished; consume() closes it out\n // (a no-op below when `state` is absent, the common case here). Safe\n // to consume unconditionally here: checkBinding() above already\n // proved a present `state` belongs to this browser.\n return await consume(new NextResponse(\"missing code or state\", { status: 400 }))\n }\n\n // `consume` was already defined above, ahead of the OIDC-error branch —\n // it is safe to reuse here since `state` is guaranteed non-null past this\n // point (the `missing code or state` check above returned already).\n\n const [row] = await ctx.db\n .select().from(loginState).where(eq(loginState.state, state)).limit(1)\n if (!row) {\n log.warn({ event: \"auth.login.failed\", reason: \"invalid_state\" }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"invalid_state\")\n return await consume(new NextResponse(\"invalid state\", { status: 400 }))\n }\n if (row.expiresAt < new Date()) {\n log.warn({ event: \"auth.login.failed\", reason: \"expired_state\" }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"expired_state\")\n return await consume(new NextResponse(\"expired state\", { status: 400 }))\n }\n\n // Whatever identity the browser carried into this callback is being\n // replaced — either by the new one below, or by nothing at all on the\n // no-access path. Either way the old session must not survive it: the\n // cookie is about to be overwritten or cleared, but the row would stay\n // valid with a working refresh token behind it.\n const previousSessionId = await ctx.cookies.read(req.headers.get(\"cookie\"))\n\n // RFC 9207 / RFC 9700 §2.1 — mix-up defence. Absent is allowed: an AS that\n // does not send `iss` is not a mismatch. A wrong one is.\n const iss = req.nextUrl.searchParams.get(\"iss\")\n if (iss && iss !== ctx.kcClient.issuer) {\n log.warn({ event: \"auth.login.failed\", reason: \"iss_mismatch\", iss }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"iss_mismatch\")\n return await consume(new NextResponse(\"iss_mismatch\", { status: 400 }))\n }\n\n try {\n const tokens = await ctx.kcClient.exchangeCode({ code, codeVerifier: row.codeVerifier })\n\n // Not a missing signature check: OIDC 3.1.3.7 point 6 allows TLS server\n // validation in place of verifying the signature when the token came\n // straight from the token endpoint over a direct connection, which is this\n // flow. Tokens that arrive any other way (backchannel logout) do go\n // through jwks.verify — see handleBackchannelLogout. Decoded lazily: a\n // row that needs neither check (no nonce, no forced reauth) must not\n // pay for parsing the ID token.\n const idClaims: { nonce?: string; auth_time?: number } | undefined =\n row.nonce || row.forcedReauth\n // nosemgrep: qdc.jwt.unverified-decode -- id token came back from the authorization server's token endpoint in this same request\n ? (decodeJwt(tokens.idToken) as { nonce?: string; auth_time?: number })\n : undefined\n\n // RFC 9700 §4.5.3.2 — until this check passes, every token from this\n // exchange is disregarded. A row without a nonce predates this build.\n if (row.nonce) {\n if (idClaims?.nonce !== row.nonce) {\n log.warn(\n { event: \"auth.login.failed\", reason: \"nonce_mismatch\" },\n \"auth login failed\",\n )\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"nonce_mismatch\")\n return await consume(new NextResponse(\"nonce_mismatch\", { status: 400 }))\n }\n }\n\n // nosemgrep: qdc.jwt.unverified-decode -- access token came back from the authorization server's token endpoint in this same request\n const claims = decodeJwt(tokens.accessToken) as ClaimsWithAccess & { sub: string; email?: string; organization?: unknown }\n\n // OIDC 3.1.2.1: when max_age is sent the ID token MUST carry auth_time.\n // We only sent it on a forced login, and there it is the proof that the\n // authorization server really re-authenticated instead of replaying its\n // SSO session. A missing or stale claim means the identity in front of us\n // was never re-proven, so it is not adopted.\n //\n // This MUST be read from the ID token, not the access token: OIDC only\n // mandates auth_time on the ID token, and Keycloak does not necessarily\n // mirror it onto the access token. Reading it off `claims` (decoded from\n // tokens.accessToken) would make this check fail closed on every forced\n // login in production. No fallback to the access token either — the ID\n // token is the normative source; a fallback would let a non-conforming\n // AS satisfy the check.\n if (row.forcedReauth) {\n const authTime = idClaims?.auth_time\n const requestedAt = Math.floor(row.createdAt.getTime() / 1000) - AUTH_TIME_SKEW_SEC\n // Also reject an auth_time in the future (beyond the same skew\n // allowance): a value \"verified against the request time\" must be\n // bounded on both sides, or an AS/token bug that reports a\n // far-future auth_time would pass unconditionally.\n const nowAt = Math.floor(Date.now() / 1000) + AUTH_TIME_SKEW_SEC\n if (typeof authTime !== \"number\" || authTime < requestedAt || authTime > nowAt) {\n log.warn(\n { event: \"auth.login.failed\", reason: \"stale_auth_time\", user_sub: claims.sub },\n \"auth login failed\",\n )\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"stale_auth_time\")\n return await consume(new NextResponse(\"re-authentication was not performed\", { status: 400 }))\n }\n }\n\n // Whom this login was supposed to end up as, recorded by handleLogin.\n // Checked after the nonce and auth_time checks — those decide whether\n // the tokens may be looked at at all, and this one reads claims out of\n // them — and before requireAppRole, so a mismatching identity is never\n // measured for app access, let alone told whether it has any.\n const expectedSub = row.expectedSub\n const expectedEmail = row.expectedEmail\n const actualEmail = claims.email ?? null\n const subMismatch = Boolean(expectedSub) && claims.sub !== expectedSub\n const emailMismatch =\n Boolean(expectedEmail) &&\n expectedEmail!.toLowerCase() !== (actualEmail ?? \"\").toLowerCase()\n\n if (subMismatch || emailMismatch) {\n // Do not adopt this identity. The tokens are real and the login is\n // valid — it is simply not the person this browser was acting as, or\n // the person who typed their address. Park it and let a human decide.\n const org = parseOrganizationClaim(claims.organization)\n const pendingId = await ctx.refreshStore.createSession({\n userSub: claims.sub,\n userEmail: claims.email,\n organizationId: org.id,\n organizationAlias: org.alias,\n accessToken: tokens.accessToken,\n accessExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),\n idToken: tokens.idToken,\n refreshToken: tokens.refreshToken,\n refreshExpiresAt: new Date(Date.now() + tokens.refreshExpiresIn * 1000),\n pendingIdentityConfirm: true,\n })\n if (previousSessionId) await ctx.refreshStore.revokeSession(previousSessionId)\n log.warn(\n {\n event: \"auth.login.identity_mismatch\",\n reason: subMismatch ? \"sub_mismatch\" : \"email_mismatch\",\n expected_sub: expectedSub ?? undefined,\n expected_email: expectedEmail ?? undefined,\n user_sub: claims.sub,\n user_email: actualEmail ?? undefined,\n },\n \"auth login identity mismatch\",\n )\n auth.set(\"qore.auth.event\", \"login_identity_mismatch\")\n auth.fail(\"identity_mismatch\")\n const target = new URL(\n ctx.identityMismatchPath ?? DEFAULT_IDENTITY_MISMATCH_PATH,\n ctx.publicOrigin,\n )\n const res = NextResponse.redirect(target, 302)\n // The old identity does not survive this either: its row is revoked\n // above and its cookie goes now. Nobody is signed in until someone\n // chooses.\n res.headers.append(\"Set-Cookie\", ctx.cookies.clear())\n res.headers.append(\n \"Set-Cookie\",\n await ctx.handoff.issue({\n sessionId: pendingId,\n expectedSub,\n expectedEmail,\n actualEmail,\n }),\n )\n return await consume(res)\n }\n\n if (ctx.requireAppRole && rolesForClient(claims, ctx.clientId).length === 0) {\n log.warn(\n { event: \"auth.login.failed\", reason: \"no_app_access\", user_sub: claims.sub, user_email: claims.email },\n \"auth login failed\",\n )\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"no_app_access\")\n if (previousSessionId) await ctx.refreshStore.revokeSession(previousSessionId)\n const denied = NextResponse.redirect(new URL(\"/no-access\", ctx.publicOrigin), 302)\n // Without this the previous identity's cookie survives the redirect:\n // one click to / and the browser is that person again.\n denied.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n return await consume(denied)\n }\n if (previousSessionId) await ctx.refreshStore.revokeSession(previousSessionId)\n const org = parseOrganizationClaim(claims.organization)\n const refreshExpiresAt = new Date(Date.now() + tokens.refreshExpiresIn * 1000)\n const sessionId = await ctx.refreshStore.createSession({\n userSub: claims.sub,\n userEmail: claims.email,\n organizationId: org.id,\n organizationAlias: org.alias,\n accessToken: tokens.accessToken,\n accessExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),\n idToken: tokens.idToken,\n refreshToken: tokens.refreshToken,\n refreshExpiresAt,\n })\n\n log.info(\n {\n event: \"auth.login.succeeded\",\n user_sub: claims.sub,\n user_email: claims.email,\n org_id: org.id,\n org_alias: org.alias,\n },\n \"auth login succeeded\",\n )\n auth.set(\"qore.auth.event\", \"login_succeeded\")\n if (org.alias) auth.set(\"qore.auth.org_alias\", org.alias)\n\n // Resolve the stored redirect path against the configured public origin, not\n // req.url: behind a proxy (Northflank) the container sees the internal bind\n // address (e.g. http://0.0.0.0:8080), which would leak into the Location.\n const res = NextResponse.redirect(new URL(row.redirectPath, ctx.publicOrigin), 302)\n // The cookie dies with the refresh token: a cookie that outlives the row\n // is what makes \"cookie alive, session dead\" — and a silent re-login — the\n // normal state instead of the exception.\n res.headers.set(\"Set-Cookie\", await ctx.cookies.issue(sessionId, { expiresAt: refreshExpiresAt }))\n return await consume(res)\n } catch (err) {\n log.error({ event: \"auth.login.failed\", reason: \"token_exchange\", err }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"token_exchange\")\n // This path rethrows — there is no response to attach a cleared\n // qdc_login cookie to — but the row must still not survive a failed\n // exchange, or a `state` whose token exchange failed stays a valid,\n // replayable transaction for up to 10 minutes.\n await ctx.db.delete(loginState).where(eq(loginState.state, state))\n throw err\n }\n })\n}\n","import { webcrypto } from \"node:crypto\"\n\nconst SUBTLE = webcrypto.subtle\n\nexport type Cipher = { key: CryptoKey }\n\n// HKDF salt/info are fixed, so the derived key is a pure function of the hex\n// secret — safe to memoize per-secret across the process lifetime. Cache the\n// in-flight promise (not just the resolved value) so concurrent callers that\n// race in before the first derivation settles still dedupe onto one derive.\nconst cipherCache = new Map<string, Promise<Cipher>>()\n\nasync function deriveCipher(hexSecret: string): Promise<Cipher> {\n const raw = Buffer.from(hexSecret, \"hex\")\n const baseKey = await SUBTLE.importKey(\"raw\", raw, \"HKDF\", false, [\"deriveKey\"])\n const key = await SUBTLE.deriveKey(\n {\n name: \"HKDF\",\n hash: \"SHA-256\",\n salt: new Uint8Array(16),\n info: new TextEncoder().encode(\"qdc-sdk:refresh-token:v1\"),\n },\n baseKey,\n { name: \"AES-GCM\", length: 256 },\n false,\n [\"encrypt\", \"decrypt\"],\n )\n return { key }\n}\n\nexport async function createCipher(hexSecret: string): Promise<Cipher> {\n if (hexSecret.length < 32) {\n throw new Error(\"SESSION_COOKIE_SECRET must be at least 32 hex chars\")\n }\n const cached = cipherCache.get(hexSecret)\n if (cached) return cached\n const promise = deriveCipher(hexSecret)\n cipherCache.set(hexSecret, promise)\n return promise\n}\n\nexport async function encrypt(cipher: Cipher, plaintext: string) {\n const iv = webcrypto.getRandomValues(new Uint8Array(12))\n const enc = await SUBTLE.encrypt(\n { name: \"AES-GCM\", iv },\n cipher.key,\n new TextEncoder().encode(plaintext),\n )\n return { ciphertext: new Uint8Array(enc), iv }\n}\n\nexport async function decrypt(\n cipher: Cipher,\n ciphertext: Uint8Array,\n iv: Uint8Array,\n): Promise<string> {\n const dec = await SUBTLE.decrypt({ name: \"AES-GCM\", iv }, cipher.key, ciphertext)\n return new TextDecoder().decode(dec)\n}\n","import { webcrypto } from \"node:crypto\"\n\nexport const COOKIE_NAME = \"qdc_session\"\n\n/** Absolute ceiling on a session cookie when the caller gives no expiry. The old\n * value was 30 days, which is how \"cookie alive, session dead\" became the normal\n * state rather than the exception — and that state is what triggered the silent\n * re-login behind the 2026-08-04 identity incident. */\nexport const DEFAULT_ABSOLUTE_MAX_SEC = 12 * 60 * 60\n\nconst SUBTLE = webcrypto.subtle\n\nfunction b64url(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString(\"base64url\")\n}\nfunction b64urlDecode(s: string): Uint8Array {\n return new Uint8Array(Buffer.from(s, \"base64url\"))\n}\n\nasync function hmacKey(hexSecret: string): Promise<CryptoKey> {\n const raw = Buffer.from(hexSecret, \"hex\")\n return SUBTLE.importKey(\n \"raw\",\n raw,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\", \"verify\"],\n )\n}\n\nexport function sessionCookie(opts: { secret: string }) {\n if (opts.secret.length < 32) throw new Error(\"session cookie secret too short\")\n\n async function sign(id: string): Promise<string> {\n const key = await hmacKey(opts.secret)\n const mac = await SUBTLE.sign(\"HMAC\", key, new TextEncoder().encode(id))\n return `${b64url(new TextEncoder().encode(id))}.${b64url(new Uint8Array(mac))}`\n }\n\n async function verify(value: string): Promise<string | null> {\n const parts = value.split(\".\")\n if (parts.length !== 2) return null\n const [encId, encMac] = parts as [string, string]\n let idBytes: Uint8Array\n let macBytes: Uint8Array\n try {\n idBytes = b64urlDecode(encId)\n macBytes = b64urlDecode(encMac)\n } catch { return null }\n const key = await hmacKey(opts.secret)\n const ok = await SUBTLE.verify(\"HMAC\", key, macBytes, idBytes)\n return ok ? new TextDecoder().decode(idBytes) : null\n }\n\n async function issue(id: string, opts?: { expiresAt?: Date }): Promise<string> {\n const signed = await sign(id)\n const maxAge = opts?.expiresAt\n ? Math.max(0, Math.floor((opts.expiresAt.getTime() - Date.now()) / 1000))\n : DEFAULT_ABSOLUTE_MAX_SEC\n return `${COOKIE_NAME}=${signed}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`\n }\n\n function clear(): string {\n return `${COOKIE_NAME}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`\n }\n\n async function read(cookieHeader: string | null | undefined): Promise<string | null> {\n if (!cookieHeader) return null\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim()\n if (!trimmed.startsWith(`${COOKIE_NAME}=`)) continue\n const value = trimmed.slice(COOKIE_NAME.length + 1)\n return verify(value)\n }\n return null\n }\n\n return { issue, clear, read }\n}\n","export type KcConfig = {\n keycloakUrl: string\n realm: string\n clientId: string\n clientSecret: string\n publicOrigin: string\n orgAlias?: string\n /** \"auto\" (default): request `organization:<orgAlias>` when orgAlias is set,\n * else the bare `organization` scope. \"none\": never request the organization\n * scope — for identity-first consumers (the portal) where Keycloak's\n * org-selection prompt must not appear for multi-org users. */\n organizationScope?: \"auto\" | \"none\"\n /** Upper bound (ms) on the Keycloak token- and logout-endpoint fetches.\n * Bounded because refresh runs inside an open pg transaction holding a row\n * lock (a hung KC would pin the connection) and because logout runs inside\n * the request the browser is waiting on (a hung KC would leave the logout\n * navigation never returning). Defaults to DEFAULT_TOKEN_TIMEOUT_MS. */\n tokenTimeoutMs?: number\n}\n\nconst DEFAULT_TOKEN_TIMEOUT_MS = 10_000\n\nexport type KcTokens = {\n accessToken: string\n refreshToken: string\n idToken: string\n expiresIn: number\n refreshExpiresIn: number\n}\n\nexport type KcClient = ReturnType<typeof createKcClient>\n\nexport class KcTokenError extends Error {\n constructor(\n public status: number,\n public oauthError: string | undefined,\n body: string,\n ) {\n super(`KC token endpoint ${status}: ${body}`)\n this.name = \"KcTokenError\"\n }\n}\n\nexport function createKcClient(cfg: KcConfig) {\n const issuer = `${cfg.keycloakUrl}/realms/${cfg.realm}`\n const tokenUrl = `${issuer}/protocol/openid-connect/token`\n const logoutUrl = `${issuer}/protocol/openid-connect/logout`\n const authorizeUrl = `${issuer}/protocol/openid-connect/auth`\n const redirectUri = `${cfg.publicOrigin}/api/auth/callback`\n\n function scope(): string {\n const base = \"openid profile email\"\n if (cfg.organizationScope === \"none\") return base\n return cfg.orgAlias ? `${base} organization:${cfg.orgAlias}` : `${base} organization`\n }\n\n function buildAuthorizeUrl(args: {\n state: string\n codeChallenge: string\n nonce?: string\n loginHint?: string\n kcIdpHint?: string\n prompt?: string\n maxAge?: number\n idTokenHint?: string\n }) {\n const params = new URLSearchParams({\n response_type: \"code\",\n client_id: cfg.clientId,\n redirect_uri: redirectUri,\n scope: scope(),\n state: args.state,\n code_challenge: args.codeChallenge,\n code_challenge_method: \"S256\",\n })\n if (args.nonce) params.set(\"nonce\", args.nonce)\n if (args.loginHint) params.set(\"login_hint\", args.loginHint)\n if (args.kcIdpHint) params.set(\"kc_idp_hint\", args.kcIdpHint)\n if (args.prompt) params.set(\"prompt\", args.prompt)\n // max_age is not redundant next to prompt=login: it is what obliges the AS\n // to return an auth_time claim (OIDC 3.1.2.1), which is the only way the\n // callback can verify that a re-authentication actually happened rather\n // than trust that Keycloak honoured the prompt.\n if (args.maxAge !== undefined) params.set(\"max_age\", String(args.maxAge))\n // Asks the AS to confirm the existing session still belongs to this user\n // and to answer login_required when it does not, instead of silently\n // handing back whoever its SSO cookie names. Only meaningful when we are\n // NOT forcing a prompt — alongside prompt=login it adds nothing.\n if (args.idTokenHint) params.set(\"id_token_hint\", args.idTokenHint)\n return `${authorizeUrl}?${params.toString().replace(/\\+/g, \"%20\")}`\n }\n\n function buildEndSessionUrl(args: { postLogoutRedirectUri: string; idTokenHint?: string }) {\n const params = new URLSearchParams({\n client_id: cfg.clientId,\n post_logout_redirect_uri: args.postLogoutRedirectUri,\n })\n // Without id_token_hint, Keycloak's end-session endpoint shows a \"do you\n // want to log out?\" confirmation page instead of ending the SSO session\n // directly.\n if (args.idTokenHint) params.set(\"id_token_hint\", args.idTokenHint)\n return `${logoutUrl}?${params.toString()}`\n }\n\n async function postToken(body: URLSearchParams): Promise<KcTokens> {\n body.set(\"client_id\", cfg.clientId)\n body.set(\"client_secret\", cfg.clientSecret)\n // Bounded because refresh runs under a pg row lock — a hung KC must not\n // pin the checked-out connection for the full hang duration.\n const timeoutMs = cfg.tokenTimeoutMs ?? DEFAULT_TOKEN_TIMEOUT_MS\n const res = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body,\n signal: AbortSignal.timeout(timeoutMs),\n })\n if (!res.ok) {\n const body = await res.text()\n let oauthError: string | undefined\n try {\n oauthError = (JSON.parse(body) as { error?: string }).error\n } catch {\n /* non-JSON error body */\n }\n throw new KcTokenError(res.status, oauthError, body)\n }\n const j = (await res.json()) as {\n access_token: string\n refresh_token: string\n id_token: string\n expires_in: number\n refresh_expires_in: number\n }\n return {\n accessToken: j.access_token,\n refreshToken: j.refresh_token,\n idToken: j.id_token,\n expiresIn: j.expires_in,\n refreshExpiresIn: j.refresh_expires_in,\n }\n }\n\n async function exchangeCode(args: { code: string; codeVerifier: string }) {\n const body = new URLSearchParams({\n grant_type: \"authorization_code\",\n code: args.code,\n redirect_uri: redirectUri,\n code_verifier: args.codeVerifier,\n })\n return postToken(body)\n }\n\n async function refresh(refreshToken: string) {\n const body = new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n })\n return postToken(body)\n }\n\n async function logout(refreshToken: string): Promise<void> {\n const body = new URLSearchParams({\n client_id: cfg.clientId,\n client_secret: cfg.clientSecret,\n refresh_token: refreshToken,\n })\n // Bounded for a different reason than postToken: this runs inside the\n // request the browser is waiting on, so an unbounded fetch turns a slow\n // Keycloak into a logout navigation that never returns. Callers treat the\n // revocation as best-effort, so timing out is the right outcome — the\n // session row is revoked either way and Keycloak expires its own.\n const res = await fetch(logoutUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body,\n signal: AbortSignal.timeout(cfg.tokenTimeoutMs ?? DEFAULT_TOKEN_TIMEOUT_MS),\n })\n if (!res.ok && res.status !== 204) {\n throw new Error(`KC logout ${res.status}: ${await res.text()}`)\n }\n }\n\n return { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, refresh, logout, redirectUri, scope, issuer }\n}\n","import { createRemoteJWKSet, jwtVerify, type JWTPayload } from \"jose\"\n\nexport type JwksOptions = {\n keycloakUrl: string\n realm: string\n audience: string\n cooldownMs?: number\n}\n\nexport type Jwks = {\n verify: (token: string) => Promise<JWTPayload>\n}\n\nexport function createJwks(opts: JwksOptions): Jwks {\n const url = new URL(`${opts.keycloakUrl}/realms/${opts.realm}/protocol/openid-connect/certs`)\n const jwkset = createRemoteJWKSet(url, {\n cooldownDuration: opts.cooldownMs ?? 30_000,\n })\n const issuer = `${opts.keycloakUrl}/realms/${opts.realm}`\n\n async function verify(token: string): Promise<JWTPayload> {\n const { payload } = await jwtVerify(token, jwkset, {\n audience: opts.audience,\n issuer,\n })\n return payload\n }\n\n return { verify }\n}\n","import { and, eq, isNull } from \"drizzle-orm\"\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\"\nimport { sessions } from \"./schema\"\nimport { decrypt, encrypt, type Cipher } from \"./crypto\"\n\nexport type CreateSessionInput = {\n userSub: string\n userEmail?: string\n organizationId?: string\n organizationAlias?: string\n accessToken: string\n accessExpiresAt: Date\n idToken?: string\n refreshToken: string\n refreshExpiresAt: Date\n pendingIdentityConfirm?: boolean\n}\n\nexport type Session = {\n id: string\n userSub: string\n userEmail: string | null\n organizationId: string | null\n organizationAlias: string | null\n accessToken: string\n accessExpiresAt: Date\n idToken: string | null\n refreshToken: string\n refreshExpiresAt: Date\n createdAt: Date\n updatedAt: Date\n}\n\nexport type RotateInput = {\n accessToken: string\n accessExpiresAt: Date\n idToken?: string\n refreshToken: string\n refreshExpiresAt: Date\n}\n\nexport type LockedSession = {\n /** Row state after acquiring the row lock; null if revoked/deleted meanwhile. */\n session: Session | null\n rotate: (t: RotateInput) => Promise<void>\n revoke: () => Promise<void>\n}\n\nexport type RefreshStore = ReturnType<typeof createRefreshStore>\n\n/** How long a read `getSession()` result may be served from the in-memory\n * cache before the next call re-queries the DB. Bounds the staleness window\n * for out-of-band revocations from other process instances (see module doc\n * below); refresh-within-skew logic still runs against the cached row on\n * every call, so an actually-expired access token is never served stale —\n * only revocation-detection latency is bounded by this TTL. */\nexport const SESSION_CACHE_TTL_MS = 5000\n\ntype SessionCacheEntry = { session: Session; at: number }\n\nexport function createRefreshStore(opts: {\n db: NodePgDatabase\n cipher: Cipher\n /** Clock source for the session cache, in arbitrary monotonic units\n * (default: `performance.now()`, milliseconds). Overridable in tests so\n * TTL expiry can be exercised without a real 5s sleep. */\n clock?: () => number\n}) {\n const { db, cipher } = opts\n const now = opts.clock ?? (() => performance.now())\n\n // Short-TTL read cache for getSession(), keyed by session id. Populated on\n // a real DB hit; invalidated on any write that can change the row (rotate,\n // single-session revoke) or that can affect many rows at once (revoke-all,\n // which just clears the whole cache — simpler than per-user bookkeeping and\n // rare on the hot path).\n const sessionCache = new Map<string, SessionCacheEntry>()\n\n async function createSession(input: CreateSessionInput): Promise<string> {\n const { ciphertext, iv } = await encrypt(cipher, input.refreshToken)\n const [row] = await db\n .insert(sessions)\n .values({\n userSub: input.userSub,\n userEmail: input.userEmail,\n organizationId: input.organizationId,\n organizationAlias: input.organizationAlias,\n accessToken: input.accessToken,\n accessExpiresAt: input.accessExpiresAt,\n idToken: input.idToken,\n refreshCt: ciphertext,\n refreshIv: iv,\n refreshExpiresAt: input.refreshExpiresAt,\n pendingIdentityConfirm: input.pendingIdentityConfirm ?? false,\n })\n .returning({ id: sessions.id })\n return row!.id\n }\n\n async function rowToSession(row: typeof sessions.$inferSelect): Promise<Session> {\n const refreshToken = await decrypt(cipher, row.refreshCt, row.refreshIv)\n return {\n id: row.id,\n userSub: row.userSub,\n userEmail: row.userEmail,\n organizationId: row.organizationId,\n organizationAlias: row.organizationAlias,\n accessToken: row.accessToken,\n accessExpiresAt: row.accessExpiresAt,\n idToken: row.idToken,\n refreshToken,\n refreshExpiresAt: row.refreshExpiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }\n }\n\n async function getSession(id: string): Promise<Session | null> {\n const cached = sessionCache.get(id)\n if (cached && now() - cached.at < SESSION_CACHE_TTL_MS) {\n return cached.session\n }\n const [row] = await db\n .select()\n .from(sessions)\n .where(\n and(\n eq(sessions.id, id),\n isNull(sessions.revokedAt),\n // A pending row is invisible here on purpose: this is the read every\n // guard funnels through (requireAuth, getAccessToken, /me, refresh),\n // so filtering once is what makes \"pending grants nothing\" true\n // everywhere instead of at each call site.\n eq(sessions.pendingIdentityConfirm, false),\n ),\n )\n .limit(1)\n if (!row) {\n sessionCache.delete(id)\n return null\n }\n const session = await rowToSession(row)\n sessionCache.set(id, { session, at: now() })\n return session\n }\n\n /** Read a row that is still awaiting identity confirmation. Deliberately\n * uncached: it is read once, by the confirm/inspect endpoints, and caching a\n * row whose whole point is that it is about to change state is a trap. */\n async function getPendingSession(id: string): Promise<Session | null> {\n const [row] = await db\n .select()\n .from(sessions)\n .where(\n and(\n eq(sessions.id, id),\n isNull(sessions.revokedAt),\n eq(sessions.pendingIdentityConfirm, true),\n ),\n )\n .limit(1)\n return row ? rowToSession(row) : null\n }\n\n /** The session's stored ID token, for the sole purpose of putting it in\n * `id_token_hint` on Keycloak's end-session URL.\n *\n * Deliberately ignores `revokedAt` and `pendingIdentityConfirm`, which every\n * other read filters on. Logging out is the one operation that must still\n * work *after* the row is revoked: without the hint Keycloak shows a\n * \"do you want to log out?\" page (maintainers, keycloak#12183: a valid hint\n * is always honoured, an absent one always prompts), and that page's button\n * is one the user can simply never press — leaving the SSO session alive.\n * So a retried or second-pass logout must still be able to produce it.\n *\n * Returns the ID token and nothing else. An ID token is an assertion about a\n * past authentication, not a credential, and RP-Initiated Logout 1.0 says an\n * expired one is acceptable here — while the refresh and access tokens stay\n * behind `getSession()`, so a revoked row still grants nothing.\n *\n * Uncached on purpose: it is read at most once per logout, and caching a row\n * whose whole point is that it has just been revoked is a trap. */\n async function getIdTokenHint(id: string): Promise<string | null> {\n const [row] = await db\n .select({ idToken: sessions.idToken })\n .from(sessions)\n .where(eq(sessions.id, id))\n .limit(1)\n return row?.idToken ?? null\n }\n\n /** Flip a pending row to confirmed. Returns false when there was nothing to\n * confirm — the caller issues a session cookie on true, so this must never\n * report success twice for one row. */\n async function confirmSession(id: string): Promise<boolean> {\n const rows = await db\n .update(sessions)\n .set({ pendingIdentityConfirm: false, updatedAt: new Date() })\n .where(\n and(\n eq(sessions.id, id),\n isNull(sessions.revokedAt),\n eq(sessions.pendingIdentityConfirm, true),\n ),\n )\n .returning({ id: sessions.id })\n sessionCache.delete(id)\n return rows.length === 1\n }\n\n async function rotateTokens(id: string, t: RotateInput): Promise<void> {\n const { ciphertext, iv } = await encrypt(cipher, t.refreshToken)\n await db\n .update(sessions)\n .set({\n accessToken: t.accessToken,\n accessExpiresAt: t.accessExpiresAt,\n ...(t.idToken !== undefined ? { idToken: t.idToken } : {}),\n refreshCt: ciphertext,\n refreshIv: iv,\n refreshExpiresAt: t.refreshExpiresAt,\n updatedAt: new Date(),\n })\n .where(eq(sessions.id, id))\n sessionCache.delete(id)\n }\n\n async function revokeSession(id: string): Promise<void> {\n await db.update(sessions).set({ revokedAt: new Date() }).where(eq(sessions.id, id))\n sessionCache.delete(id)\n }\n\n async function revokeByUser(userSub: string): Promise<void> {\n await db\n .update(sessions)\n .set({ revokedAt: new Date() })\n .where(and(eq(sessions.userSub, userSub), isNull(sessions.revokedAt)))\n // Revoking every session for a user could invalidate several cache\n // entries; clearing the whole cache is simpler than per-user bookkeeping\n // and this path isn't hot enough to justify it.\n sessionCache.clear()\n }\n\n /** Run fn while holding a `SELECT … FOR UPDATE` row lock on the session.\n * Serializes refresh per session: concurrent callers block on the lock,\n * then see the winner's rotated tokens in `locked.session`. The KC refresh\n * call deliberately happens inside the transaction — that's the point. */\n async function withSessionLock<T>(\n id: string,\n fn: (locked: LockedSession) => Promise<T>,\n ): Promise<T> {\n return db.transaction(async (tx) => {\n const [row] = await tx\n .select()\n .from(sessions)\n .where(and(eq(sessions.id, id), isNull(sessions.revokedAt)))\n .for(\"update\")\n .limit(1)\n const session = row ? await rowToSession(row) : null\n return fn({\n session,\n rotate: async (t: RotateInput) => {\n const { ciphertext, iv } = await encrypt(cipher, t.refreshToken)\n await tx\n .update(sessions)\n .set({\n accessToken: t.accessToken,\n accessExpiresAt: t.accessExpiresAt,\n ...(t.idToken !== undefined ? { idToken: t.idToken } : {}),\n refreshCt: ciphertext,\n refreshIv: iv,\n refreshExpiresAt: t.refreshExpiresAt,\n updatedAt: new Date(),\n })\n .where(eq(sessions.id, id))\n // This bypasses rotateTokens() (runs inside the tx), so invalidate\n // the read cache here too — otherwise a subsequent getSession()\n // within the TTL would serve the pre-rotation row.\n sessionCache.delete(id)\n },\n revoke: async () => {\n await tx.update(sessions).set({ revokedAt: new Date() }).where(eq(sessions.id, id))\n sessionCache.delete(id)\n },\n })\n })\n }\n\n return {\n createSession,\n getSession,\n getPendingSession,\n getIdTokenHint,\n confirmSession,\n rotateTokens,\n revokeSession,\n revokeByUser,\n withSessionLock,\n }\n}\n","import {\n boolean,\n customType,\n index,\n pgSchema,\n text,\n timestamp,\n uuid,\n varchar,\n} from \"drizzle-orm/pg-core\"\n\nexport const authSchema = pgSchema(\"auth\")\n\nconst bytea = customType<{ data: Uint8Array; driverData: Buffer }>({\n dataType: () => \"bytea\",\n toDriver: (v) => Buffer.from(v),\n fromDriver: (v) => new Uint8Array(v),\n})\n\nexport const sessions = authSchema.table(\n \"sessions\",\n {\n id: uuid(\"id\").primaryKey().defaultRandom(),\n userSub: varchar(\"user_sub\", { length: 64 }).notNull(),\n userEmail: varchar(\"user_email\", { length: 320 }),\n organizationId: varchar(\"organization_id\", { length: 64 }),\n organizationAlias: varchar(\"organization_alias\", { length: 255 }),\n accessToken: text(\"access_token\").notNull(),\n accessExpiresAt: timestamp(\"access_expires_at\", { withTimezone: true }).notNull(),\n idToken: text(\"id_token\"),\n refreshCt: bytea(\"refresh_ct\").notNull(),\n refreshIv: bytea(\"refresh_iv\").notNull(),\n refreshExpiresAt: timestamp(\"refresh_expires_at\", { withTimezone: true }).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull().defaultNow(),\n revokedAt: timestamp(\"revoked_at\", { withTimezone: true }),\n pendingIdentityConfirm: boolean(\"pending_identity_confirm\").notNull().default(false),\n },\n (t) => ({\n userSubIdx: index(\"ix_auth_sessions_user_sub\").on(t.userSub),\n }),\n)\n\nexport const loginState = authSchema.table(\"login_state\", {\n state: varchar(\"state\", { length: 128 }).primaryKey(),\n codeVerifier: varchar(\"code_verifier\", { length: 255 }).notNull(),\n redirectPath: text(\"redirect_path\").notNull().default(\"/\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true }).notNull(),\n forcedReauth: boolean(\"forced_reauth\").notNull().default(false),\n nonce: varchar(\"nonce\", { length: 128 }),\n // Set by handleLogin when its own request carries retried=1 (our restart\n // hitting /api/auth/login). handleCallback reads it back to bound the\n // one-and-only restart on Keycloak's explicit \"start over\" signals — see\n // MIGRATION_0005_RETRIED for why this lives on the row and not on the KC\n // callback URL.\n retried: boolean(\"retried\").notNull().default(false),\n // Whom this login was supposed to end up as: the subject of the session\n // being replaced and/or the address the user typed. The callback compares\n // Keycloak's answer against these instead of adopting whatever comes back.\n expectedSub: varchar(\"expected_sub\", { length: 64 }),\n expectedEmail: varchar(\"expected_email\", { length: 320 }),\n})\n","import { webcrypto } from \"node:crypto\"\n\n/** Points at a session row that is waiting for a human to say whose it is.\n *\n * Signed and HttpOnly, so the browser can neither read nor forge it: the\n * session id it carries is the only way to reach a pending row, and a caller\n * who could name an arbitrary id could confirm somebody else's pending login.\n *\n * It also carries the expected identity, because the `login_state` row is\n * deleted on every exit from the callback — by the time the interstitial asks\n * \"who did you expect?\", the database no longer knows.\n */\nexport const HANDOFF_COOKIE_NAME = \"qdc_identity_handoff\"\n\n/** Five minutes: long enough to read two sentences and click, short enough that\n * an abandoned mismatch stops being an option rather than lingering. */\nconst TTL_SEC = 300\n\nconst SUBTLE = webcrypto.subtle\n\nexport type IdentityHandoff = {\n sessionId: string\n expectedSub: string | null\n expectedEmail: string | null\n actualEmail: string | null\n exp: number\n}\n\n/** Distinct HKDF info from the refresh-token cipher (`qdc-sdk:refresh-token:v1`)\n * and from the session cookie's MAC, so this key cannot be substituted for\n * either. Same secret, different purpose, different key. */\nconst HKDF_INFO = \"qdc-sdk:identity-handoff:v1\"\n\nasync function macKey(hexSecret: string): Promise<CryptoKey> {\n const raw = Buffer.from(hexSecret, \"hex\")\n const baseKey = await SUBTLE.importKey(\"raw\", raw, \"HKDF\", false, [\"deriveKey\"])\n return SUBTLE.deriveKey(\n {\n name: \"HKDF\",\n hash: \"SHA-256\",\n salt: new Uint8Array(16),\n info: new TextEncoder().encode(HKDF_INFO),\n },\n baseKey,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\", \"verify\"],\n )\n}\n\nfunction attributes(maxAge: number): string {\n return `Path=/api/auth; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`\n}\n\nexport async function createIdentityHandoff(opts: { secret: string }) {\n if (opts.secret.length < 32) throw new Error(\"session cookie secret too short\")\n const key = await macKey(opts.secret)\n\n async function issue(payload: Omit<IdentityHandoff, \"exp\">): Promise<string> {\n const full: IdentityHandoff = {\n ...payload,\n exp: Math.floor(Date.now() / 1000) + TTL_SEC,\n }\n const body = Buffer.from(JSON.stringify(full)).toString(\"base64url\")\n const mac = await SUBTLE.sign(\"HMAC\", key, new TextEncoder().encode(body))\n const value = `${body}.${Buffer.from(new Uint8Array(mac)).toString(\"base64url\")}`\n return `${HANDOFF_COOKIE_NAME}=${value}; ${attributes(TTL_SEC)}`\n }\n\n async function read(\n cookieHeader: string | null | undefined,\n ): Promise<IdentityHandoff | null> {\n if (!cookieHeader) return null\n let value: string | null = null\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${HANDOFF_COOKIE_NAME}=`)) {\n value = trimmed.slice(HANDOFF_COOKIE_NAME.length + 1)\n break\n }\n }\n if (!value) return null\n const parts = value.split(\".\")\n if (parts.length !== 2) return null\n const [body, mac] = parts as [string, string]\n let ok: boolean\n try {\n ok = await SUBTLE.verify(\n \"HMAC\",\n key,\n new Uint8Array(Buffer.from(mac, \"base64url\")),\n new TextEncoder().encode(body),\n )\n } catch {\n return null\n }\n if (!ok) return null\n let parsed: IdentityHandoff\n try {\n parsed = JSON.parse(Buffer.from(body, \"base64url\").toString(\"utf8\")) as IdentityHandoff\n } catch {\n return null\n }\n if (typeof parsed.exp !== \"number\" || parsed.exp < Math.floor(Date.now() / 1000)) {\n return null\n }\n if (typeof parsed.sessionId !== \"string\" || !parsed.sessionId) return null\n return parsed\n }\n\n function clear(): string {\n return `${HANDOFF_COOKIE_NAME}=; ${attributes(0)}`\n }\n\n return { issue, read, clear }\n}\n","export type OrganizationClaim = { id?: string; alias?: string }\n\n/**\n * Normalize Keycloak's `organization` token claim into `{ id, alias }`.\n *\n * Keycloak emits this claim in several shapes depending on the realm's\n * organization-mapper configuration:\n *\n * - **multivalued array of aliases** — `[\"qdc-dev\"]` (the qdc realm's current\n * config). Only the alias is available; there is no id in the token.\n * - **map keyed by alias** — `{ \"qdc-dev\": { id: \"<uuid>\" } }`.\n * - **explicit object** — `{ id, alias }`.\n *\n * Anything else (missing, empty, a bare string, malformed) yields `{}`.\n * The first organization wins when several are present.\n */\nexport function parseOrganizationClaim(raw: unknown): OrganizationClaim {\n if (!raw) return {}\n\n // Array of alias strings: [\"qdc-dev\", ...]\n if (Array.isArray(raw)) {\n const alias = raw.find((x): x is string => typeof x === \"string\" && x.length > 0)\n return alias ? { alias, id: undefined } : {}\n }\n\n if (typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>\n\n // Explicit { id, alias } object.\n if (typeof obj.alias === \"string\" || typeof obj.id === \"string\") {\n return {\n id: typeof obj.id === \"string\" ? obj.id : undefined,\n alias: typeof obj.alias === \"string\" ? obj.alias : undefined,\n }\n }\n\n // Map keyed by alias: { \"<alias>\": { id, ... } }\n const alias = Object.keys(obj)[0]\n if (alias) {\n const value = obj[alias]\n const id =\n value && typeof value === \"object\" && typeof (value as { id?: unknown }).id === \"string\"\n ? ((value as { id: string }).id)\n : undefined\n return { alias, id }\n }\n }\n\n return {}\n}\n\n/** True when the claim names the given alias, across every shape Keycloak\n * emits (array of aliases, map keyed by alias, explicit {id, alias}). */\nexport function organizationClaimHasAlias(raw: unknown, alias: string): boolean {\n if (!raw) return false\n if (Array.isArray(raw)) return raw.includes(alias)\n if (typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>\n if (typeof obj.alias === \"string\") return obj.alias === alias\n return alias in obj\n }\n return false\n}\n","import { webcrypto } from \"node:crypto\"\n\nexport function randomB64Url(bytes: number): string {\n const buf = new Uint8Array(bytes)\n webcrypto.getRandomValues(buf)\n return Buffer.from(buf).toString(\"base64url\")\n}\n\nexport async function sha256B64Url(input: string): Promise<string> {\n const data = new TextEncoder().encode(input)\n const digest = await webcrypto.subtle.digest(\"SHA-256\", data)\n return Buffer.from(new Uint8Array(digest)).toString(\"base64url\")\n}\n\n// The three sanitizers moved to `src/auth-core/sanitize.ts`: this module imports\n// `node:crypto` and uses `Buffer`, so nothing in it can be imported from an edge\n// proxy — and `safeRedirect` is precisely what a proxy wants. These aliases keep\n// the existing call sites in createAuthRouteHandlers.ts unchanged.\nexport {\n safeRedirect as sanitizeRedirect,\n safeLoginHint as sanitizeLoginHint,\n safeIdpHint as sanitizeIdpHint,\n} from \"../../auth-core/sanitize\"\n","// Input sanitizers for values that come off a URL and go back out as a\n// redirect target or an upstream query parameter.\n//\n// This module is deliberately pure: no `node:crypto`, no `Buffer`, no `next`.\n// It lives in auth-core rather than next/auth because a Next proxy runs on the\n// edge runtime, where neither of those exists — and `safeRedirect` is exactly\n// the helper a proxy wants.\n\n// Control characters (incl. CR/LF/tab) to strip before validating a redirect\n// or login hint — defence in depth against header/URL injection and stray\n// whitespace, and against the WHATWG URL parser's own tab/CR/LF stripping\n// happening after our check instead of before it.\n// eslint-disable-next-line no-control-regex -- matching control characters is the point of this regex\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f]/g\n\n// Same-origin relative path only. A prefix check on the raw string (\"starts\n// with / but not //\") is not enough: a value like \"/\\evil.com\" also starts\n// with a single \"/\", but the WHATWG URL parser (the same algorithm browsers\n// use) normalises a backslash right after the leading slash into a second\n// slash for special schemes, turning it protocol-relative. A raw tab between\n// the slashes (\"/\\t/evil.com\") collapses into \"//evil.com\" the same way once\n// the parser strips whitespace. Since this runs server-side on the login\n// path of every tenant app, resolve the candidate against a throwaway\n// absolute base and require the parsed result's origin to match the base's\n// origin, rather than trusting the raw string.\n//\n// Returns null — not a fallback — so the caller decides what \"unsafe\" means.\nfunction normalizePath(raw: string | null | undefined): string | null {\n if (!raw) return null\n const cleaned = raw.replace(CONTROL_CHARS, \"\")\n if (!cleaned.startsWith(\"/\") || cleaned.startsWith(\"//\")) return null\n try {\n const base = new URL(\"https://sanitize.invalid\")\n const resolved = new URL(cleaned, base)\n if (resolved.origin !== base.origin) return null\n return resolved.pathname + resolved.search + resolved.hash\n } catch {\n return null\n }\n}\n\n/**\n * Reduce a caller-supplied redirect target to a same-origin relative path.\n *\n * Anything absolute, protocol-relative, or unparseable resolves to `fallback`.\n * The fallback is validated by the same rule, so passing a tainted value there\n * cannot be used to smuggle an open redirect through the back door; a fallback\n * that fails too yields `\"/\"`.\n */\nexport function safeRedirect(candidate: string | null | undefined, fallback = \"/\"): string {\n return normalizePath(candidate) ?? normalizePath(fallback) ?? \"/\"\n}\n\nconst LOGIN_HINT_MAX = 320 // RFC 5321 max email length\nconst IDP_HINT_MAX = 255\nconst IDP_ALIAS = /^[A-Za-z0-9._-]+$/\n\n/**\n * Sanitize an OIDC `login_hint`.\n *\n * The hint is a login identifier — email OR username — so this strips control\n * characters and caps the length but deliberately does not enforce an email\n * format, which would break username logins.\n */\nexport function safeLoginHint(raw: string | null | undefined): string | undefined {\n if (!raw) return undefined\n const cleaned = raw.replace(CONTROL_CHARS, \"\").trim().slice(0, LOGIN_HINT_MAX)\n return cleaned.length > 0 ? cleaned : undefined\n}\n\n/**\n * Sanitize a Keycloak `kc_idp_hint` (an IdP alias).\n *\n * Rejects anything outside the alias character set rather than mangling it —\n * a half-valid alias would select the wrong identity provider silently.\n */\nexport function safeIdpHint(raw: string | null | undefined): string | undefined {\n if (!raw) return undefined\n const cleaned = raw.trim()\n if (cleaned.length === 0 || cleaned.length > IDP_HINT_MAX) return undefined\n return IDP_ALIAS.test(cleaned) ? cleaned : undefined\n}\n","/** Binds an in-flight authorization request to the user agent that started it.\n *\n * RFC 9700 §2.1.1 requires the PKCE challenge (or nonce) to be \"securely bound\n * to the client and the user agent in which the transaction was started\". Ours\n * was not: `state` and `code_verifier` lived only in `auth.login_state`, so any\n * browser presenting a `state` from a URL could complete someone else's login\n * and receive their session cookie. This cookie is the missing half.\n *\n * `SameSite=Lax`, never `Strict`: the callback arrives as a cross-site\n * top-level navigation from Keycloak, which `Strict` would withhold — breaking\n * every login. Same reasoning as `gh_link_state` in portal-web.\n *\n * Up to three states are carried, newest first, so starting a login in a second\n * tab does not silently invalidate the first.\n */\nexport const LOGIN_COOKIE_NAME = \"qdc_login\"\nexport const MAX_BOUND_STATES = 3\nconst MAX_AGE_SEC = 600\n\nfunction attributes(maxAge: number): string {\n return `Path=/api/auth; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`\n}\n\nexport function readBoundStates(cookieHeader: string | null | undefined): string[] {\n if (!cookieHeader) return []\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim()\n if (!trimmed.startsWith(`${LOGIN_COOKIE_NAME}=`)) continue\n return trimmed\n .slice(LOGIN_COOKIE_NAME.length + 1)\n .split(\" \")\n .map((s) => s.trim())\n .filter(Boolean)\n }\n return []\n}\n\nexport function appendBoundState(\n cookieHeader: string | null | undefined,\n state: string,\n): string {\n const next = [state, ...readBoundStates(cookieHeader).filter((s) => s !== state)]\n .slice(0, MAX_BOUND_STATES)\n .join(\" \")\n return `${LOGIN_COOKIE_NAME}=${next}; ${attributes(MAX_AGE_SEC)}`\n}\n\nexport function removeBoundState(\n cookieHeader: string | null | undefined,\n state: string,\n): string {\n const next = readBoundStates(cookieHeader).filter((s) => s !== state)\n if (next.length === 0) return `${LOGIN_COOKIE_NAME}=; ${attributes(0)}`\n return `${LOGIN_COOKIE_NAME}=${next.join(\" \")}; ${attributes(MAX_AGE_SEC)}`\n}\n","import { decodeJwt } from \"jose\"\nimport { KcTokenError, type KcClient } from \"../../auth-core/kcClient\"\nimport type { RefreshStore, Session } from \"../../auth-core/refreshStore\"\nimport { organizationClaimHasAlias } from \"../../auth-core/organizationClaim\"\nimport { AuthError } from \"./requireAuth\"\n\n/** Only a definitive token rejection may kill the session. Anything else\n * (network errors, KC 5xx, invalid_client misconfig) is transient: revoking\n * on those is exactly what caused the spurious logouts. */\nfunction isDefinitiveRefreshFailure(err: unknown): boolean {\n return err instanceof KcTokenError && err.status === 400 && err.oauthError === \"invalid_grant\"\n}\n\nexport async function refreshWithinSkew(\n opts: {\n refreshStore: RefreshStore\n kcClient: KcClient\n skewSec: number\n orgAlias?: string\n idleTimeoutSec?: number\n absoluteMaxSec?: number\n },\n id: string,\n session: Session,\n): Promise<Session> {\n // Both bounds are checked before the skew shortcut: a session past its\n // deadline must die on the next request, not on the next token refresh.\n //\n // `updatedAt` moves on every token rotation, so \"idle\" here means \"no request\n // needed a refresh\", which within the 10-minute access-token lifespan is a\n // faithful proxy for activity. A tab that keeps polling is genuinely active.\n const idleSec = opts.idleTimeoutSec\n const absoluteSec = opts.absoluteMaxSec\n const tooIdle =\n idleSec !== undefined && Date.now() - session.updatedAt.getTime() > idleSec * 1000\n const tooOld =\n absoluteSec !== undefined && Date.now() - session.createdAt.getTime() > absoluteSec * 1000\n if (tooIdle || tooOld) {\n await opts.refreshStore.revokeSession(id)\n throw new AuthError(\"session_expired\")\n }\n\n const skewMs = opts.skewSec * 1000\n if (session.accessExpiresAt.getTime() - Date.now() >= skewMs) {\n return session\n }\n\n type Outcome =\n | { kind: \"ok\"; session: Session }\n | { kind: \"dead\" }\n | { kind: \"transient\" }\n\n const result: Outcome = await opts.refreshStore.withSessionLock(id, async (locked) => {\n if (!locked.session) return { kind: \"dead\" as const }\n // A concurrent request may have rotated while we waited for the lock —\n // single-flight: use its tokens instead of burning ours.\n if (locked.session.accessExpiresAt.getTime() - Date.now() >= skewMs) {\n return { kind: \"ok\" as const, session: locked.session }\n }\n try {\n const tokens = await opts.kcClient.refresh(locked.session.refreshToken)\n if (opts.orgAlias) {\n const claims = decodeJwt(tokens.accessToken) as { organization?: unknown }\n if (!organizationClaimHasAlias(claims.organization, opts.orgAlias)) {\n // Definitive: the user is no longer a member of this app's\n // organization — kill the session, don't persist the new tokens.\n await locked.revoke()\n return { kind: \"dead\" as const }\n }\n }\n const rotated = {\n accessToken: tokens.accessToken,\n accessExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),\n idToken: tokens.idToken,\n refreshToken: tokens.refreshToken,\n refreshExpiresAt: new Date(Date.now() + tokens.refreshExpiresIn * 1000),\n }\n await locked.rotate(rotated)\n return { kind: \"ok\" as const, session: { ...locked.session, ...rotated } }\n } catch (err) {\n if (isDefinitiveRefreshFailure(err)) {\n await locked.revoke()\n return { kind: \"dead\" as const }\n }\n return { kind: \"transient\" as const }\n }\n })\n\n if (result.kind === \"ok\") return result.session\n if (result.kind === \"dead\") throw new AuthError(\"session_expired\")\n // Transient KC failure: keep serving while the access token is still valid;\n // the next request retries the refresh. Only fail once it actually expired.\n if (session.accessExpiresAt.getTime() > Date.now()) return session\n throw new AuthError(\"refresh_unavailable\")\n}\n","import { cookies } from \"next/headers\"\nimport { decodeJwt } from \"jose\"\nimport { COOKIE_NAME } from \"../../auth-core/session\"\nimport type { KcClient } from \"../../auth-core/kcClient\"\nimport type { RefreshStore } from \"../../auth-core/refreshStore\"\nimport { refreshWithinSkew } from \"./refreshWithinSkew\"\nimport { rolesForClient, type ClaimsWithAccess } from \"./appAccess\"\nimport { setUserContext } from \"../../telemetry-hooks\"\n\ntype Cookies = {\n read: (cookieHeader: string | null | undefined) => Promise<string | null>\n}\n\nexport type AuthUser = {\n id: string\n email: string | null\n organization: { id: string | null; alias: string | null }\n}\n\nexport class AuthError extends Error {\n constructor(\n public reason: \"unauthenticated\" | \"session_expired\" | \"no_app_access\" | \"refresh_unavailable\",\n ) {\n super(reason)\n this.name = \"AuthError\"\n }\n}\n\nexport function requireAuth(opts: {\n cookies: Cookies\n refreshStore: RefreshStore\n kcClient: KcClient\n refreshSkewSec?: number\n clientId?: string\n orgAlias?: string\n /** Same two bounds `/me` enforces. A guard that enforced a weaker policy\n * would silently become the session's real lifetime. */\n idleTimeoutSec?: number\n absoluteMaxSec?: number\n}): () => Promise<AuthUser> {\n const skewSec = opts.refreshSkewSec ?? 30\n return async () => {\n const jar = await cookies()\n const raw = jar.get(COOKIE_NAME)?.value\n const id = await opts.cookies.read(raw ? `${COOKIE_NAME}=${raw}` : null)\n if (!id) throw new AuthError(\"unauthenticated\")\n\n let session = await opts.refreshStore.getSession(id)\n if (!session) throw new AuthError(\"unauthenticated\")\n\n session = await refreshWithinSkew(\n {\n refreshStore: opts.refreshStore,\n kcClient: opts.kcClient,\n skewSec,\n orgAlias: opts.orgAlias,\n idleTimeoutSec: opts.idleTimeoutSec,\n absoluteMaxSec: opts.absoluteMaxSec,\n },\n id,\n session,\n )\n\n if (opts.clientId) {\n const claims = decodeJwt(session.accessToken) as ClaimsWithAccess\n if (rolesForClient(claims, opts.clientId).length === 0) {\n throw new AuthError(\"no_app_access\")\n }\n }\n\n setUserContext(session.userSub)\n return {\n id: session.userSub,\n email: session.userEmail,\n organization: { id: session.organizationId, alias: session.organizationAlias },\n }\n }\n}\n","export type ClaimsWithAccess = { resource_access?: Record<string, { roles?: string[] }> }\n\n/** Roles the token carries for a specific app client (its own audience). */\nexport function rolesForClient(claims: ClaimsWithAccess, clientId: string): string[] {\n return claims.resource_access?.[clientId]?.roles ?? []\n}\n","// Pure helpers for reading roles out of a Keycloak access token. No `next` or\n// React imports — this is auth-core shared logic, imported by both sides.\n\nexport type JwtPayload = {\n realm_access?: { roles?: string[] }\n resource_access?: Record<string, { roles?: string[] }>\n}\n\n// Roles Keycloak adds to every token; not meaningful as app roles.\nconst KC_DEFAULT_ROLES = new Set([\"offline_access\", \"uma_authorization\"])\n\n// Decode the payload (middle) segment of a JWT. Signature is NOT verified — the\n// token is read from our own trusted session store, never from the client.\nexport function decodeJwtPayload(token: string): JwtPayload {\n const segment = token.split(\".\")[1]\n if (!segment) throw new Error(\"Malformed JWT: missing payload segment\")\n const json = Buffer.from(segment, \"base64url\").toString(\"utf8\")\n return JSON.parse(json) as JwtPayload\n}\n\n// Merge realm + client roles, drop Keycloak defaults, dedupe.\nexport function extractRoles(payload: JwtPayload, clientId: string | undefined): string[] {\n const realm = payload.realm_access?.roles ?? []\n const client = clientId ? (payload.resource_access?.[clientId]?.roles ?? []) : []\n const meaningful = [...realm, ...client].filter(\n (role) => !KC_DEFAULT_ROLES.has(role) && !role.startsWith(\"default-roles-\"),\n )\n return [...new Set(meaningful)]\n}\n","// Pure permission model. No `next`/React imports. A permission map binds each\n// role name to the permissions that role grants. The map lives in the app; the\n// SDK only provides the shape, the type helper, and the resolver.\n\nexport type PermissionMap = Record<string, readonly string[]>\n\n// The union of every permission string in the map, e.g. \"app:rename\" | \"profile:edit\".\nexport type PermissionOf<M extends PermissionMap> = M[keyof M][number] & string\n\n// Identity at runtime; exists to capture literal types via the `const` type\n// parameter so `PermissionOf<typeof map>` yields a precise string-literal union.\nexport function definePermissions<const M extends PermissionMap>(map: M): M {\n return map\n}\n\n// Resolve a user's roles to the set of permissions they hold. Roles not present\n// in the map contribute nothing (fail-closed). Result is deduped.\nexport function permissionsForRoles(map: PermissionMap, roles: string[]): string[] {\n const set = new Set<string>()\n for (const role of roles) {\n for (const perm of map[role] ?? []) set.add(perm)\n }\n return [...set]\n}\n","import type { NodePgDatabase } from \"drizzle-orm/node-postgres\"\nimport type { PermissionMap } from \"../../auth-core/permissions\"\n\nexport type NextAuthOptions = {\n keycloakUrl: string\n realm: string\n clientId: string\n clientSecret: string\n orgAlias?: string\n /** \"auto\" (default): request `organization:<orgAlias>` when set, else the\n * bare `organization` scope. \"none\": never request it — identity-first\n * consumers (the portal) authorize relationally and must not trigger\n * Keycloak's org-selection prompt for multi-org users. */\n organizationScope?: \"auto\" | \"none\"\n publicOrigin: string\n sessionCookieSecret: string\n db: NodePgDatabase\n /** Enforce per-app client-role default-deny at the BFF. Default true.\n * Set false for identity-only consumers (e.g. the multi-tenant portal),\n * which authorize relationally at their own API. */\n requireAppRole?: boolean\n /** Role→permission map for this app. When set, `/api/auth/me` returns the\n * caller's resolved permissions and the permission guards resolve against it.\n * Omitted by identity-only consumers (the portal). */\n permissions?: PermissionMap\n /** Where to send the browser when Keycloak returns an identity the login did\n * not expect. The SDK ships no UI: the default lands on the app's own login\n * page with a reason, which is correct for a generated tenant app. The portal\n * points this at its two-button interstitial. */\n identityMismatchPath?: string\n /** Session dies after this long without a token refresh. Default 8h. */\n sessionIdleTimeoutSec?: number\n /** Hard ceiling no activity can extend. Default 12h. */\n sessionAbsoluteMaxSec?: number\n}\n\n/** Session dies after 8h without a token refresh unless the app says otherwise. */\nexport const DEFAULT_SESSION_IDLE_TIMEOUT_SEC = 28800\n/** Hard ceiling no activity can extend: 12h. */\nexport const DEFAULT_SESSION_ABSOLUTE_MAX_SEC = 43200\n","import { SpanStatusCode, trace } from \"@opentelemetry/api\"\n\n/** Auth-flow spans need failure semantics observe() can't express: a failed\n * login RETURNS a 4xx/redirect instead of throwing, and observe() stamps OK\n * unconditionally after fn returns. This wrapper lets the handler mark the\n * span failed (with a reason) while still returning its response. Internal to\n * next/auth — the public observe() contract stays untouched. */\nexport type AuthSpanController = {\n set(key: string, value: string): void\n fail(reason: string): void\n}\n\nexport function runAuthSpan<T>(\n label: string,\n attributes: Record<string, string>,\n fn: (auth: AuthSpanController) => Promise<T>,\n): Promise<T> {\n return trace.getTracer(\"@quatronic/sdk\").startActiveSpan(label, async (span) => {\n span.setAttributes(attributes)\n span.setAttribute(\"qore.span.label\", label)\n span.setAttribute(\"qore.span.type\", \"task\")\n let failed = false\n const auth: AuthSpanController = {\n set: (key, value) => void span.setAttribute(key, value),\n fail: (reason) => {\n failed = true\n span.setAttribute(\"qore.auth.failure_reason\", reason)\n span.setStatus({ code: SpanStatusCode.ERROR, message: reason })\n },\n }\n try {\n const result = await fn(auth)\n if (!failed) span.setStatus({ code: SpanStatusCode.OK })\n return result\n } catch (err) {\n span.recordException(err as Error)\n span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error)?.message })\n throw err\n } finally {\n span.end()\n }\n })\n}\n","import { createCipher } from \"../../auth-core/crypto\"\nimport { sessionCookie } from \"../../auth-core/session\"\nimport { createKcClient } from \"../../auth-core/kcClient\"\nimport { createRefreshStore } from \"../../auth-core/refreshStore\"\nimport { requireAuth, type AuthUser } from \"./requireAuth\"\nimport {\n DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n type NextAuthOptions,\n} from \"./types\"\n\nexport async function requireAuthFor(\n opts: NextAuthOptions,\n): Promise<() => Promise<AuthUser>> {\n const cipher = await createCipher(opts.sessionCookieSecret)\n const cookies = sessionCookie({ secret: opts.sessionCookieSecret })\n const refreshStore = createRefreshStore({ db: opts.db, cipher })\n const kcClient = createKcClient({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n clientId: opts.clientId,\n clientSecret: opts.clientSecret,\n publicOrigin: opts.publicOrigin,\n orgAlias: opts.orgAlias,\n organizationScope: opts.organizationScope,\n })\n const clientId = opts.requireAppRole === false ? undefined : opts.clientId\n return requireAuth({\n cookies,\n refreshStore,\n kcClient,\n clientId,\n orgAlias: opts.orgAlias,\n idleTimeoutSec: opts.sessionIdleTimeoutSec ?? DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n absoluteMaxSec: opts.sessionAbsoluteMaxSec ?? DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n })\n}\n","import { cookies } from \"next/headers\"\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\"\nimport { COOKIE_NAME, sessionCookie } from \"../../auth-core/session\"\nimport { createCipher } from \"../../auth-core/crypto\"\nimport { createKcClient } from \"../../auth-core/kcClient\"\nimport { createRefreshStore } from \"../../auth-core/refreshStore\"\nimport type { KcClient } from \"../../auth-core/kcClient\"\nimport type { RefreshStore } from \"../../auth-core/refreshStore\"\nimport { AuthError } from \"./requireAuth\"\nimport { refreshWithinSkew } from \"./refreshWithinSkew\"\nimport {\n DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n type NextAuthOptions,\n} from \"./types\"\n\ntype Cookies = {\n read: (cookieHeader: string | null | undefined) => Promise<string | null>\n}\n\ntype GetAccessTokenDeps = {\n cookies: Cookies\n refreshStore: RefreshStore\n kcClient: KcClient\n refreshSkewSec?: number\n orgAlias?: string\n /** Same two bounds `/me` enforces. A guard that enforced a weaker policy\n * would silently become the session's real lifetime. */\n idleTimeoutSec?: number\n absoluteMaxSec?: number\n}\n\nasync function resolveAccessToken(\n opts: GetAccessTokenDeps,\n): Promise<string | null> {\n const skewSec = opts.refreshSkewSec ?? 30\n const jar = await cookies()\n const raw = jar.get(COOKIE_NAME)?.value\n const id = await opts.cookies.read(raw ? `${COOKIE_NAME}=${raw}` : null)\n if (!id) return null\n\n let session = await opts.refreshStore.getSession(id)\n if (!session) return null\n\n session = await refreshWithinSkew(\n {\n refreshStore: opts.refreshStore,\n kcClient: opts.kcClient,\n skewSec,\n orgAlias: opts.orgAlias,\n idleTimeoutSec: opts.idleTimeoutSec,\n absoluteMaxSec: opts.absoluteMaxSec,\n },\n id,\n session,\n )\n\n return session.accessToken\n}\n\nexport function getAccessToken(\n opts: GetAccessTokenDeps,\n): () => Promise<string | null> {\n return () => resolveAccessToken(opts)\n}\n\nexport function requireAccessToken(\n opts: GetAccessTokenDeps,\n): () => Promise<string> {\n return async () => {\n const token = await resolveAccessToken(opts)\n if (token === null) throw new AuthError(\"unauthenticated\")\n return token\n }\n}\n\ntype BuildDepsResult = {\n cookies: Cookies\n refreshStore: RefreshStore\n kcClient: KcClient\n orgAlias?: string\n idleTimeoutSec: number\n absoluteMaxSec: number\n}\n\n// buildDeps constructs a cipher, a refresh-store (wraps the pg pool handed in\n// via opts.db — a long-lived pool, safe to share) and a kcClient (pure config\n// closure, no per-request state) every call. None of that needs redoing per\n// request for the same effective config, so memoize the built-deps promise\n// keyed on the non-function config fields buildDeps actually reads. `db` is\n// an object (not JSON-serializable/stable), so it keys an outer WeakMap —\n// entries fall out of the cache automatically when a db handle is GC'd.\nconst depsCache = new WeakMap<NodePgDatabase, Map<string, Promise<BuildDepsResult>>>()\n\nfunction depsCacheKey(opts: NextAuthOptions): string {\n return JSON.stringify({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n clientId: opts.clientId,\n clientSecret: opts.clientSecret,\n orgAlias: opts.orgAlias,\n organizationScope: opts.organizationScope,\n publicOrigin: opts.publicOrigin,\n sessionCookieSecret: opts.sessionCookieSecret,\n requireAppRole: opts.requireAppRole,\n permissions: opts.permissions,\n sessionIdleTimeoutSec: opts.sessionIdleTimeoutSec,\n sessionAbsoluteMaxSec: opts.sessionAbsoluteMaxSec,\n })\n}\n\nasync function buildDepsUncached(opts: NextAuthOptions): Promise<BuildDepsResult> {\n const cipher = await createCipher(opts.sessionCookieSecret)\n const cookies = sessionCookie({ secret: opts.sessionCookieSecret })\n const refreshStore = createRefreshStore({ db: opts.db, cipher })\n const kcClient = createKcClient({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n clientId: opts.clientId,\n clientSecret: opts.clientSecret,\n publicOrigin: opts.publicOrigin,\n orgAlias: opts.orgAlias,\n organizationScope: opts.organizationScope,\n })\n return {\n cookies,\n refreshStore,\n kcClient,\n orgAlias: opts.orgAlias,\n idleTimeoutSec: opts.sessionIdleTimeoutSec ?? DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n absoluteMaxSec: opts.sessionAbsoluteMaxSec ?? DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n }\n}\n\nasync function buildDeps(opts: NextAuthOptions): Promise<BuildDepsResult> {\n let byKey = depsCache.get(opts.db)\n if (!byKey) {\n byKey = new Map()\n depsCache.set(opts.db, byKey)\n }\n const key = depsCacheKey(opts)\n const cached = byKey.get(key)\n if (cached) return cached\n const promise = buildDepsUncached(opts)\n byKey.set(key, promise)\n return promise\n}\n\nexport async function getAccessTokenFor(\n opts: NextAuthOptions,\n): Promise<() => Promise<string | null>> {\n return getAccessToken(await buildDeps(opts))\n}\n\nexport async function requireAccessTokenFor(\n opts: NextAuthOptions,\n): Promise<() => Promise<string>> {\n return requireAccessToken(await buildDeps(opts))\n}\n","import { sql } from \"drizzle-orm\"\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\"\nimport { grantRuntimeDefaultPrivileges } from \"../db-core/migration-db\"\n\n// Migrations are inlined as string constants rather than read from .sql files\n// at runtime. A runtime `readFile` does not survive bundlers that only trace\n// JS — notably Next.js standalone output, where the .sql is omitted and the\n// read ENOENTs, crashing the instrumentation hook and 500-ing the whole app.\n// Inlining keeps the SQL in the bundled JS so it works in every consumer.\n\n// Split out of what used to be a single MIGRATION_0001_INIT on purpose, and\n// the reason generalises to every statement below it: **Postgres checks the\n// connecting role's privileges before it evaluates `IF NOT EXISTS`.** So an\n// already-applied statement is not a no-op for a role that lacks the right —\n// it is an error:\n//\n// - `CREATE SCHEMA IF NOT EXISTS auth` needs CREATE on the *database* →\n// `permission denied for database <db>` (SQLSTATE 42501)\n// - `CREATE INDEX IF NOT EXISTS` and `ALTER TABLE ... ADD COLUMN IF NOT\n// EXISTS` need *ownership* of the table → `must be owner of table\n// sessions` (also 42501)\n//\n// That is the normal shape of a least-privileged runtime role against an\n// already-provisioned database, and because migrations run in order one such\n// error aborts every statement after it. It has now bitten staging twice: the\n// first time `login_state.forced_reauth` never landed and every\n// `/api/auth/login` 500'd; the second time `0001_init:tables` failed on its\n// `CREATE INDEX` with `must be owner of table sessions`, again blocking every\n// later ALTER even though there was nothing at all to do.\n//\n// So each migration below carries an `isApplied` catalog probe and the runner\n// skips it when its effect is already present. Catalog lookups need nothing\n// beyond CONNECT, so a role with only DML inside an up-to-date `auth` schema\n// gets through cleanly, while a migration that genuinely still has work to do\n// is attempted and — if the role cannot do it — fails loudly, by name.\nconst MIGRATION_0001_SCHEMA = `\nCREATE SCHEMA IF NOT EXISTS auth;\n`\n\nconst MIGRATION_0001_TABLES = `\nCREATE TABLE IF NOT EXISTS auth.sessions (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n user_sub varchar(64) NOT NULL,\n user_email varchar(320),\n organization_id varchar(64),\n organization_alias varchar(255),\n access_token text NOT NULL,\n access_expires_at timestamptz NOT NULL,\n refresh_ct bytea NOT NULL,\n refresh_iv bytea NOT NULL,\n refresh_expires_at timestamptz NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n revoked_at timestamptz\n);\n\nCREATE INDEX IF NOT EXISTS ix_auth_sessions_user_sub\n ON auth.sessions(user_sub) WHERE revoked_at IS NULL;\n\nCREATE TABLE IF NOT EXISTS auth.login_state (\n state varchar(128) PRIMARY KEY,\n code_verifier varchar(255) NOT NULL,\n redirect_path text NOT NULL DEFAULT '/',\n created_at timestamptz NOT NULL DEFAULT now(),\n expires_at timestamptz NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS ix_auth_login_state_expires\n ON auth.login_state(expires_at);\n`\n\n// The KC ID token, so RP-initiated logout can pass it as `id_token_hint` —\n// without it, Keycloak's end-session endpoint falls back to showing a logout\n// confirmation page. Nullable: existing sessions predate this column until\n// their next login or token refresh backfills it.\nconst MIGRATION_0002_ID_TOKEN = `\nALTER TABLE auth.sessions ADD COLUMN IF NOT EXISTS id_token text;\n`\n\n// Records that this login asked Keycloak to re-authenticate (prompt=login +\n// max_age=0). The callback needs it to know whether the ID token's auth_time\n// MUST prove a fresh authentication (OIDC 3.1.2.1) — without the flag it cannot\n// tell a forced login from an ordinary SSO continuation.\nconst MIGRATION_0003_FORCED_REAUTH = `\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS forced_reauth boolean NOT NULL DEFAULT false;\n`\n\n// OIDC 3.1.3.7 (11): the nonce sent on the authorization request must be\n// checked against the ID token's claim. Nullable because rows written by an\n// older build have none; the callback treats a null nonce on the row as\n// \"nothing to check\" so an in-flight login survives the deploy.\nconst MIGRATION_0004_NONCE = `\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS nonce varchar(128);\n`\n\n// Bounds the one-and-only restart the callback grants on Keycloak's explicit\n// \"start over\" signals (authentication_expired / login_required). The\n// callback URL is Keycloak's fixed redirect_uri, so Keycloak never echoes our\n// own query parameters back to it — a guard read off the callback's query\n// string can never fire in production. The restarted login (our own request\n// to /api/auth/login?retried=1) stamps this column on the NEW row instead, so\n// the second callback can see it on the row rather than in a URL Keycloak\n// controls. NOT NULL DEFAULT false: every row, old or new, starts un-retried.\nconst MIGRATION_0005_RETRIED = `\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS retried boolean NOT NULL DEFAULT false;\n`\n\n// A session created for an identity that did not match what the login expected\n// exists, holds real tokens, and grants nothing until a human confirms it.\n//\n// DELIBERATELY `DEFAULT false` rather than a nullable column with a backfill:\n// these migrations re-run on every app start, so an `UPDATE ... WHERE\n// pending_identity_confirm IS NULL` would confirm every row still awaiting a\n// human on the next deploy — silently adopting exactly the identity this\n// mechanism exists to stop. A default means pre-existing rows are already\n// confirmed, which is correct: they were issued under the old rules.\nconst MIGRATION_0006_PENDING_CONFIRM = `\nALTER TABLE auth.sessions\n ADD COLUMN IF NOT EXISTS pending_identity_confirm boolean NOT NULL DEFAULT false;\n`\n\n// Who this login was supposed to end up as: the subject of the session being\n// replaced, and/or the address the user typed. The callback compares Keycloak's\n// answer against these instead of adopting whatever comes back. Nullable: a\n// clean first login on a fresh browser expects nobody in particular.\nconst MIGRATION_0007_EXPECTED_IDENTITY = `\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS expected_sub varchar(64);\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS expected_email varchar(320);\n`\n\ninterface Migration {\n // Named so a failure can say *which* statement broke — see the catch below.\n readonly name: string\n readonly sql: string\n // \"Is this migration's effect already present?\" Answered from the catalogs\n // only, so it never needs a privilege the migration itself would need. A\n // partially-applied migration must answer `false` (the SQL is written to be\n // idempotent, so re-running the applied half is harmless).\n readonly isApplied: (db: NodePgDatabase) => Promise<boolean>\n}\n\n// All probes funnel through here: one boolean expression, evaluated by the\n// server. Every one of them reads `pg_catalog` directly rather than going\n// through `to_regclass`/`to_regnamespace` or `information_schema`, both of\n// which are privilege-sensitive in ways that defeat the purpose: `to_regclass`\n// resolves the name and so raises `permission denied for schema auth` for a\n// role without USAGE, and `information_schema.columns` silently hides columns\n// the role holds no privilege on — reporting an applied migration as missing\n// and sending us straight into the ALTER we were trying to avoid. The catalogs\n// themselves are world-readable.\nasync function probe(db: NodePgDatabase, expression: string): Promise<boolean> {\n const result = await db.execute<{ applied: boolean }>(\n sql.raw(`SELECT (${expression}) AS \"applied\"`),\n )\n return result.rows[0]?.applied === true\n}\n\nconst schemaExists = (schema: string) => (db: NodePgDatabase) =>\n probe(db, `EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = '${schema}')`)\n\n// Tables and indexes alike — both live in `pg_class`.\nconst relationsExist =\n (...relations: readonly (readonly [schema: string, name: string])[]) =>\n (db: NodePgDatabase) => {\n const tuples = relations.map(([schema, name]) => `('${schema}','${name}')`).join(\", \")\n return probe(\n db,\n `(SELECT count(*) FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE (n.nspname, c.relname) IN (${tuples})) = ${relations.length}`,\n )\n }\n\nconst columnsExist =\n (...columns: readonly (readonly [schema: string, table: string, column: string])[]) =>\n (db: NodePgDatabase) => {\n const tuples = columns\n .map(([schema, table, column]) => `('${schema}','${table}','${column}')`)\n .join(\", \")\n return probe(\n db,\n `(SELECT count(*) FROM pg_attribute a\n JOIN pg_class c ON c.oid = a.attrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE a.attnum > 0 AND NOT a.attisdropped\n AND (n.nspname, c.relname, a.attname) IN (${tuples})) = ${columns.length}`,\n )\n }\n\nconst MIGRATIONS: readonly Migration[] = [\n {\n name: \"0001_init:schema\",\n sql: MIGRATION_0001_SCHEMA,\n isApplied: schemaExists(\"auth\"),\n },\n {\n name: \"0001_init:tables\",\n sql: MIGRATION_0001_TABLES,\n isApplied: relationsExist(\n [\"auth\", \"sessions\"],\n [\"auth\", \"ix_auth_sessions_user_sub\"],\n [\"auth\", \"login_state\"],\n [\"auth\", \"ix_auth_login_state_expires\"],\n ),\n },\n {\n name: \"0002_id_token\",\n sql: MIGRATION_0002_ID_TOKEN,\n isApplied: columnsExist([\"auth\", \"sessions\", \"id_token\"]),\n },\n {\n name: \"0003_forced_reauth\",\n sql: MIGRATION_0003_FORCED_REAUTH,\n isApplied: columnsExist([\"auth\", \"login_state\", \"forced_reauth\"]),\n },\n {\n name: \"0004_nonce\",\n sql: MIGRATION_0004_NONCE,\n isApplied: columnsExist([\"auth\", \"login_state\", \"nonce\"]),\n },\n {\n name: \"0005_retried\",\n sql: MIGRATION_0005_RETRIED,\n isApplied: columnsExist([\"auth\", \"login_state\", \"retried\"]),\n },\n {\n name: \"0006_pending_confirm\",\n sql: MIGRATION_0006_PENDING_CONFIRM,\n isApplied: columnsExist([\"auth\", \"sessions\", \"pending_identity_confirm\"]),\n },\n {\n name: \"0007_expected_identity\",\n sql: MIGRATION_0007_EXPECTED_IDENTITY,\n isApplied: columnsExist(\n [\"auth\", \"login_state\", \"expected_sub\"],\n [\"auth\", \"login_state\", \"expected_email\"],\n ),\n },\n]\n\nexport interface RunAuthMigrationsOptions {\n // When provided, grants this role SELECT/INSERT/UPDATE/DELETE on every\n // table in the `auth` schema — both ones this call just created and ones a\n // future boot's migrations will create. Omit for a single-role setup (the\n // fallback state of an environment that hasn't opted into a separate\n // migration role yet); passing it is what makes it safe for `db` to\n // connect as an admin/owner role while the app itself never does.\n readonly runtimeRole?: string\n}\n\nexport async function runAuthMigrations(\n db: NodePgDatabase,\n opts: RunAuthMigrationsOptions = {},\n): Promise<void> {\n for (const migration of MIGRATIONS) {\n try {\n if (await migration.isApplied(db)) {\n // Already there — do not issue the statement at all. See the comment\n // above MIGRATION_0001_SCHEMA for why relying on `IF NOT EXISTS` to make\n // it a no-op is not safe under every role.\n continue\n }\n await db.execute(sql.raw(migration.sql))\n } catch (err) {\n // Re-thrown, not swallowed: the caller decides what to do with a failed\n // migration. Naming the statement is what turns a bare \"permission\n // denied for database x\" into something you can actually act on; the\n // `cause` keeps the original Postgres error (code, detail, etc.) intact.\n const message = err instanceof Error ? err.message : String(err)\n throw new Error(`auth migration \"${migration.name}\" failed: ${message}`, { cause: err })\n }\n }\n\n if (opts.runtimeRole) {\n await grantRuntimeDefaultPrivileges(db, \"auth\", opts.runtimeRole)\n }\n}\n","import { NextResponse } from \"next/server\"\nimport { requireAccessTokenFor } from \"./getAccessToken\"\nimport { requireAuthFor } from \"./requireAuthFor\"\nimport { AuthError, type AuthUser } from \"./requireAuth\"\nimport { extractRoles, decodeJwtPayload } from \"../../auth-core/roles\"\nimport { permissionsForRoles, type PermissionMap, type PermissionOf } from \"../../auth-core/permissions\"\nimport type { NextAuthOptions } from \"./types\"\n\nexport type PermissionGuards<M extends PermissionMap> = {\n can: (permission: PermissionOf<M>) => Promise<boolean>\n requirePermission: (permission: PermissionOf<M>) => Promise<AuthUser>\n withPermission: <A extends unknown[]>(\n permission: PermissionOf<M>,\n handler: (...args: A) => Promise<Response> | Response,\n ) => (...args: A) => Promise<Response>\n}\n\n// Dependency-injected core (no next/headers) — see permissionGuards.test.ts.\nexport function makeGuards<M extends PermissionMap>(\n map: M,\n getPermissions: () => Promise<string[]>,\n // requireUser defaults to throwing-unauthenticated; injected in real use.\n requireUser: () => Promise<AuthUser> = async () => {\n throw new AuthError(\"unauthenticated\")\n },\n): PermissionGuards<M> {\n const can = async (permission: PermissionOf<M>): Promise<boolean> =>\n (await getPermissions()).includes(permission)\n\n const requirePermission = async (permission: PermissionOf<M>): Promise<AuthUser> => {\n const user = await requireUser()\n if (!(await can(permission))) throw new AuthError(\"no_app_access\")\n return user\n }\n\n const withPermission = <A extends unknown[]>(\n permission: PermissionOf<M>,\n handler: (...args: A) => Promise<Response> | Response,\n ) =>\n async (...args: A): Promise<Response> => {\n if (!(await can(permission))) return new NextResponse(null, { status: 403 })\n return handler(...args)\n }\n\n return { can, requirePermission, withPermission }\n}\n\n/** Bind the guards once per app. `getOpts` is the app's `authOptions` thunk so\n * options (and env) are read lazily per request, never at module scope. */\nexport function createPermissionGuards<M extends PermissionMap>(\n map: M,\n getOpts: () => NextAuthOptions,\n): PermissionGuards<M> {\n const getPermissions = async (): Promise<string[]> => {\n const opts = getOpts()\n const requireAccessToken = await requireAccessTokenFor(opts)\n const token = await requireAccessToken()\n const roles = extractRoles(decodeJwtPayload(token), opts.clientId)\n return permissionsForRoles(map, roles)\n }\n const requireUser = async (): Promise<AuthUser> => {\n const requireAuth = await requireAuthFor(getOpts())\n return requireAuth()\n }\n return makeGuards(map, getPermissions, requireUser)\n}\n","import { timingSafeEqual } from \"node:crypto\";\n\ntype Handler = (req: Request) => Promise<Response> | Response;\n\nfunction expectedTokens(): string[] {\n return Object.entries(process.env)\n .filter(([k, v]) => /^CONN_.+_TOKEN$/.test(k) && !!v)\n .map(([, v]) => v as string);\n}\n\nfunction safeEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a);\n const bb = Buffer.from(b);\n if (ab.length !== bb.length) return false;\n return timingSafeEqual(ab, bb);\n}\n\nexport function withConnectionToken(handler: Handler): Handler {\n return async (req: Request): Promise<Response> => {\n const header =\n req.headers.get(\"authorization\")?.replace(/^Bearer\\s+/i, \"\") ??\n req.headers.get(\"x-qdc-connection-token\") ??\n \"\";\n const ok = !!header && expectedTokens().some((t) => safeEqual(header, t));\n if (!ok) return new Response(\"unauthorized\", { status: 401 });\n return handler(req);\n };\n}\n"],"mappings":"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","names":["eq","decodeJwt","webcrypto","SUBTLE","opts","body","webcrypto","SUBTLE","webcrypto","webcrypto","attributes","decodeJwt","decodeJwt","segment","attributes","cookies","decodeJwt","res","eq","bindingFailure","row","org","cookies","cookies","cookies","NextResponse","NextResponse","requireAccessToken","requireAuth"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/next/auth/createAuthRouteHandlers.ts","../../../src/auth-core/crypto.ts","../../../src/auth-core/session.ts","../../../src/auth-core/kcClient.ts","../../../src/auth-core/jwks.ts","../../../src/auth-core/refreshStore.ts","../../../src/auth-core/schema.ts","../../../src/auth-core/identityHandoff.ts","../../../src/auth-core/organizationClaim.ts","../../../src/next/auth/loginState.ts","../../../src/auth-core/sanitize.ts","../../../src/next/auth/loginBinding.ts","../../../src/next/auth/refreshWithinSkew.ts","../../../src/next/auth/requireAuth.ts","../../../src/next/auth/appAccess.ts","../../../src/auth-core/roles.ts","../../../src/auth-core/permissions.ts","../../../src/next/auth/types.ts","../../../src/next/auth/authSpan.ts","../../../src/next/auth/requireAuthFor.ts","../../../src/next/auth/getAccessToken.ts","../../../src/auth-core/migrate.ts","../../../src/next/auth/permissionGuards.ts","../../../src/next/auth/withConnectionToken.ts"],"sourcesContent":["import { NextRequest, NextResponse } from \"next/server\"\nimport { eq } from \"drizzle-orm\"\nimport { decodeJwt } from \"jose\"\nimport { createCipher } from \"../../auth-core/crypto\"\nimport { sessionCookie } from \"../../auth-core/session\"\nimport { createKcClient } from \"../../auth-core/kcClient\"\nimport { createJwks } from \"../../auth-core/jwks\"\nimport { createRefreshStore } from \"../../auth-core/refreshStore\"\nimport { createIdentityHandoff } from \"../../auth-core/identityHandoff\"\nimport { loginState } from \"../../auth-core/schema\"\nimport { parseOrganizationClaim } from \"../../auth-core/organizationClaim\"\nimport { randomB64Url, sha256B64Url, sanitizeRedirect, sanitizeLoginHint, sanitizeIdpHint } from \"./loginState\"\nimport { readBoundStates, appendBoundState, removeBoundState } from \"./loginBinding\"\nimport { refreshWithinSkew } from \"./refreshWithinSkew\"\nimport { AuthError } from \"./requireAuth\"\nimport { rolesForClient, type ClaimsWithAccess } from \"./appAccess\"\nimport { extractRoles, decodeJwtPayload } from \"../../auth-core/roles\"\nimport { permissionsForRoles, type PermissionMap } from \"../../auth-core/permissions\"\nimport {\n DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n type NextAuthOptions,\n} from \"./types\"\nimport { getLogger } from \"../../telemetry-hooks\"\nimport { runAuthSpan } from \"./authSpan\"\n\nexport type AuthRouteHandlers = {\n GET: (req: NextRequest) => Promise<NextResponse>\n POST: (req: NextRequest) => Promise<NextResponse>\n}\n\n/** Clock-skew allowance between our `login_state.created_at` and Keycloak's\n * `auth_time`. Both are server clocks; 5s is generous. */\nconst AUTH_TIME_SKEW_SEC = 5\n\n/** The SDK owns no UI, so the default is the app's own login page with a reason\n * in the query. Consumers that have a real interstitial override it. */\nconst DEFAULT_IDENTITY_MISMATCH_PATH = \"/login?reason=identity_mismatch\"\n\n/** Where Keycloak sends the browser after ending the SSO session. */\nconst LOGGED_OUT_PATH = \"/api/auth/logged-out\"\n/** Only \"cookies\" — see the note on `handleLoggedOut` for the measurements that\n * ruled out \"cache\", and why \"storage\" was never in scope. */\nconst CLEAR_SITE_DATA = '\"cookies\"'\n\nfunction segment(req: NextRequest): string {\n const parts = req.nextUrl.pathname.split(\"/\").filter(Boolean)\n return parts[parts.length - 1] ?? \"\"\n}\n\n/** A `/me` response describes exactly one identity. Without `no-store` a browser\n * may replay it on history navigation — which is how pressing \"back\" showed a\n * colleague's account on 2026-08-04 — and without `Vary: Cookie` any shared\n * cache would key it by URL alone, i.e. by nothing that identifies the user. */\nfunction noStore<T extends NextResponse>(res: T): T {\n res.headers.set(\"Cache-Control\", \"no-store\")\n res.headers.set(\"Vary\", \"Cookie\")\n return res\n}\n\nexport async function createAuthRouteHandlers(\n opts: NextAuthOptions,\n): Promise<AuthRouteHandlers> {\n const cipher = await createCipher(opts.sessionCookieSecret)\n const cookies = sessionCookie({ secret: opts.sessionCookieSecret })\n const refreshStore = createRefreshStore({ db: opts.db, cipher })\n // Same secret as the session cookie, a different derived key (distinct HKDF\n // info) — this cookie authorizes a different thing and must not be\n // interchangeable with a session cookie.\n const handoff = await createIdentityHandoff({ secret: opts.sessionCookieSecret })\n const kcClient = createKcClient({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n clientId: opts.clientId,\n clientSecret: opts.clientSecret,\n publicOrigin: opts.publicOrigin,\n orgAlias: opts.orgAlias,\n organizationScope: opts.organizationScope,\n })\n const jwks = createJwks({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n audience: opts.clientId,\n })\n\n const ctx = { cookies, refreshStore, handoff, kcClient, jwks, db: opts.db, publicOrigin: opts.publicOrigin, clientId: opts.clientId, requireAppRole: opts.requireAppRole !== false, orgAlias: opts.orgAlias, permissions: opts.permissions, identityMismatchPath: opts.identityMismatchPath, idleTimeoutSec: opts.sessionIdleTimeoutSec ?? DEFAULT_SESSION_IDLE_TIMEOUT_SEC, absoluteMaxSec: opts.sessionAbsoluteMaxSec ?? DEFAULT_SESSION_ABSOLUTE_MAX_SEC }\n\n async function GET(req: NextRequest): Promise<NextResponse> {\n switch (segment(req)) {\n case \"login\":\n return handleLogin(req, ctx)\n case \"me\":\n return handleMe(req, ctx)\n case \"callback\":\n return handleCallback(req, ctx)\n case \"logout\":\n return handleLogoutRedirect(req, ctx)\n case \"logged-out\":\n return handleLoggedOut(req, ctx)\n case \"identity-mismatch\":\n return handleIdentityMismatch(req, ctx)\n default:\n return NextResponse.json({ error: \"not_found\" }, { status: 404 })\n }\n }\n\n async function POST(req: NextRequest): Promise<NextResponse> {\n switch (segment(req)) {\n case \"refresh\":\n return handleRefresh(req, ctx)\n case \"logout\":\n return handleLogout(req, ctx)\n case \"backchannel-logout\":\n return handleBackchannelLogout(req, ctx)\n case \"confirm-identity\":\n return handleConfirmIdentity(req, ctx)\n default:\n return NextResponse.json({ error: \"not_found\" }, { status: 404 })\n }\n }\n\n return { GET, POST }\n}\n\ntype Ctx = {\n cookies: ReturnType<typeof sessionCookie>\n refreshStore: ReturnType<typeof createRefreshStore>\n handoff: Awaited<ReturnType<typeof createIdentityHandoff>>\n kcClient: ReturnType<typeof createKcClient>\n jwks: ReturnType<typeof createJwks>\n db: NextAuthOptions[\"db\"]\n publicOrigin: string\n clientId: string\n requireAppRole: boolean\n orgAlias?: string\n permissions?: PermissionMap\n identityMismatchPath?: string\n idleTimeoutSec: number\n absoluteMaxSec: number\n}\n\nasync function handleLogin(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\n \"Log in\",\n {\n \"qore.auth.event\": \"login_started\",\n ...(ctx.orgAlias ? { \"qore.auth.org_alias\": ctx.orgAlias } : {}),\n },\n async () => {\n const log = getLogger()\n const redirect = sanitizeRedirect(req.nextUrl.searchParams.get(\"redirect\"))\n const loginHint = sanitizeLoginHint(req.nextUrl.searchParams.get(\"login_hint\"))\n const kcIdpHint = sanitizeIdpHint(req.nextUrl.searchParams.get(\"kc_idp_hint\"))\n\n // \"Sign in as someone else\" is just a new login. Whatever was parked for\n // confirmation is abandoned here rather than left waiting: a pending row\n // that outlives the choice it was created for is a login somebody could\n // still complete.\n const stale = await ctx.handoff.read(req.headers.get(\"cookie\"))\n if (stale) await ctx.refreshStore.revokeSession(stale.sessionId)\n\n const state = randomB64Url(32)\n const codeVerifier = randomB64Url(48)\n const codeChallenge = await sha256B64Url(codeVerifier)\n const nonce = randomB64Url(32)\n\n // Set only when THIS request is our own restart hitting /api/auth/login\n // (?retried=1) after the callback bounced Keycloak's authentication_expired\n // / login_required back to us. Stamped on the row rather than carried on\n // the KC authorize URL: the callback's redirect_uri is fixed by Keycloak,\n // which never echoes our own query parameters back to it, so a guard read\n // off the callback's query string alone could never fire in production.\n const retried = req.nextUrl.searchParams.get(\"retried\") === \"1\"\n\n const sessionId = await ctx.cookies.read(req.headers.get(\"cookie\"))\n const liveSession = sessionId ? await ctx.refreshStore.getSession(sessionId) : null\n // Re-authentication is forced by an explicit ?prompt=login — our own\n // restart after Keycloak returned authentication_expired / login_required,\n // but also any other caller that sends it directly (e.g. the SSO wizard's\n // test login, kc_idp_hint=...&prompt=login with no retried flag) — and by\n // staleSessionCookie below.\n //\n // A typed login_hint and a refresh-expired-but-present session do not\n // force it: both write an expectation onto the row below (expectedEmail\n // from the hint, expectedSub from the session — getSession filters only\n // revokedAt and pendingIdentityConfirm, so a refresh-expired row still\n // yields one), and the callback refuses to adopt an identity that does\n // not match it. That is the guard against the 2026-08-04 identity\n // substitution: not \"never let the SSO cookie answer\" but \"check whose\n // answer came back\", which protects without making every user\n // re-authenticate at their upstream IdP.\n //\n // A session cookie whose row is missing, revoked, or parked awaiting\n // identity confirmation (getSession hides all three) forces\n // re-authentication regardless of loginHint: even with a hint,\n // expectedEmail is written onto the row below, but that only lets the\n // callback catch a mismatch after the round trip, so Keycloak is still\n // made to re-assert who is arriving up front rather than trust its SSO\n // cookie. It is not the only expectation-less login either — a request\n // with no session cookie at all and no hint also records nothing below,\n // but that case is mitigated by prompt=select_account instead (see\n // hasExpectation), not by forcing. Without forcing here, a stale\n // session cookie would let Keycloak's SSO cookie silently name whoever\n // it likes, which is the 2026-08-04 shape.\n //\n // Deliberately NOT the old sessionIsDead condition: a row that is\n // present but refresh-expired still yields expectedSub, so the callback\n // verifies it. It falls through to the id_token_hint branch only when\n // that row actually has an idToken — sessions.id_token is nullable, so\n // without one it falls through further, to sending nothing, and still\n // relies on expectedSub for verification.\n const staleSessionCookie = Boolean(sessionId) && !liveSession\n const forcedReauth =\n req.nextUrl.searchParams.get(\"prompt\") === \"login\" || staleSessionCookie\n\n // Whether this login has anything the callback can check the returned\n // identity against: the subject of the session being replaced, or the\n // address the user typed. Logged for triage; it no longer decides the\n // prompt (see below).\n const hasExpectation = Boolean(liveSession?.userSub) || Boolean(loginHint)\n\n // What the authorize URL actually carries, computed once so the URL and\n // the log below cannot disagree. Three outcomes: forcedReauth wins\n // outright; otherwise a live id token asks Keycloak to confirm the\n // existing session; otherwise the IdP is asked to let the user pick an\n // account.\n //\n // That last branch used to be reached only when there was no\n // expectation to verify, so a typed address suppressed the picker. It\n // no longer does. An address says which account the user meant, not\n // that they never want a different one, and someone who has several\n // accounts at the same IdP had no way to reach the others without\n // logging out first. Upstream the two compose rather than conflict:\n // Entra's picker lists the signed-in accounts plus \"use another\n // account\", and login_hint pre-fills the sign-in field behind it.\n //\n // This costs a click on a login that verification already protected, so\n // it is not a security fix — it is a deliberate trade of one click for\n // the ability to choose. It stops short of prompt=login, which would\n // negate SSO and cost a full re-authentication.\n const idTokenHint = liveSession?.idToken ?? undefined\n const authorizePrompt = forcedReauth\n ? \"forced\"\n : idTokenHint\n ? \"id_token_hint\"\n : \"select_account\"\n\n await ctx.db.insert(loginState).values({\n state,\n codeVerifier,\n redirectPath: redirect,\n expiresAt: new Date(Date.now() + 10 * 60_000),\n forcedReauth,\n nonce,\n retried,\n // Whom this login is supposed to end up as. `liveSession` is non-null\n // even for a refresh-expired row (getSession only filters revokedAt\n // and pendingIdentityConfirm), so the common \"session timed out\" case\n // still carries an expectation.\n //\n // A revoked or absent row (with a session cookie present) yields no\n // expected subject, and that is exactly the case `staleSessionCookie`\n // above forces re-authentication for instead: with nothing here for\n // the callback to verify, Keycloak itself is made to re-assert who is\n // arriving. An explicit `GET /logout` clears the session cookie, so\n // that case has no cookie at login time either — no expectedSub, no\n // staleSessionCookie, and `select_account` (not forcing) covers it,\n // same as any other cookie-less login. If a session cookie somehow\n // survives a logout regardless, `staleSessionCookie` still forces,\n // same as any other cookie-with-no-live-row case. A login with no\n // session cookie and no typed email carries no expectation and is\n // not forced: a browser with no cookie is never covered by\n // `staleSessionCookie` above, which only fires when a session cookie\n // is present.\n expectedSub: liveSession?.userSub ?? null,\n expectedEmail: loginHint ?? null,\n })\n\n // Diagnostic: pairs with auth.login.succeeded via `state`. A second\n // auth.login.started for one user-initiated login — especially with\n // had_session=true or a referer of an in-app page — reveals a spurious\n // re-login round-trip (e.g. a redirect loop), which logs as two \"succeeded\".\n log.info(\n {\n event: \"auth.login.started\",\n state,\n redirect_path: redirect,\n has_login_hint: Boolean(loginHint),\n had_session: Boolean(sessionId),\n forced_reauth: forcedReauth,\n has_expectation: hasExpectation,\n prompt_sent: authorizePrompt,\n referer: req.headers.get(\"referer\") ?? undefined,\n },\n \"auth login started\",\n )\n\n const url = ctx.kcClient.buildAuthorizeUrl({\n state,\n codeChallenge,\n nonce,\n loginHint,\n kcIdpHint,\n ...(authorizePrompt === \"forced\"\n ? { prompt: \"login\" as const, maxAge: 0 }\n : // Only useful when we are NOT forcing: it asks the AS to confirm the\n // session is still this user's, and returns login_required if not.\n // Alongside prompt=login it adds nothing — that already re-prompts.\n // This is the path that moves the idle-timeout boundary: a refresh-\n // expired-but-present app session sends id_token_hint instead of\n // forcing, so if Keycloak's own SSO session is still alive the user\n // is logged back in silently as the same user, and Keycloak's SSO\n // Session Idle becomes the effective idle re-authentication\n // boundary in place of this app's refresh lifetime. See\n // docs/LOGBOOK.md for the trade-off this makes.\n authorizePrompt === \"id_token_hint\"\n ? { idTokenHint }\n : { prompt: \"select_account\" as const }),\n })\n const res = NextResponse.redirect(url, 302)\n res.headers.append(\"Set-Cookie\", appendBoundState(req.headers.get(\"cookie\"), state))\n // Unconditional: the browser must not carry a handoff into the new login\n // either way, and clearing a cookie that was never there costs nothing.\n res.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return res\n },\n )\n}\n\nasync function handleMe(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n try {\n return noStore(await handleMeInner(req, ctx))\n } catch (err) {\n // Defensive: any unexpected error — a corrupt stored access token making\n // decodeJwt() throw, a transient refresh-store failure — must not escape as\n // a bare unhandled 500. Log it with the stack so it stays diagnosable, and\n // return a controlled response. This is not a dead session, so the cookie is\n // left intact (no logout loop on a transient blip).\n getLogger().error({ err }, \"auth /me failed unexpectedly\")\n return noStore(NextResponse.json({ error: \"internal\" }, { status: 500 }))\n }\n}\n\nasync function handleMeInner(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n const id = await ctx.cookies.read(req.headers.get(\"cookie\"))\n if (!id) return NextResponse.json({ error: \"unauthenticated\" }, { status: 401 })\n const existing = await ctx.refreshStore.getSession(id)\n if (!existing) return NextResponse.json({ error: \"unauthenticated\" }, { status: 401 })\n\n let session\n try {\n session = await refreshWithinSkew(\n {\n refreshStore: ctx.refreshStore,\n kcClient: ctx.kcClient,\n skewSec: 30,\n orgAlias: ctx.orgAlias,\n idleTimeoutSec: ctx.idleTimeoutSec,\n absoluteMaxSec: ctx.absoluteMaxSec,\n },\n id,\n existing,\n )\n } catch (err) {\n if (!(err instanceof AuthError)) throw err\n if (err.reason === \"refresh_unavailable\") {\n // Keycloak hiccup, not a dead session: let the client retry; do NOT\n // clear the cookie or the user gets logged out by a transient blip.\n return NextResponse.json({ error: \"refresh_unavailable\" }, { status: 503 })\n }\n // Dead session: refreshWithinSkew already revoked the row. Clear the\n // cookie so /login stops seeing us as authenticated and stops looping.\n const res = NextResponse.json({ error: \"session_expired\" }, { status: 401 })\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n return res\n }\n\n // nosemgrep: qdc.jwt.unverified-decode -- access token was read out of this app's own server-side session store, not from the request\n const claims = decodeJwt(session.accessToken) as ClaimsWithAccess\n if (ctx.requireAppRole && rolesForClient(claims, ctx.clientId).length === 0) {\n return NextResponse.json({ error: \"no_app_access\" }, { status: 403 })\n }\n\n return NextResponse.json({\n user: { id: session.userSub, email: session.userEmail },\n organization: { id: session.organizationId, alias: session.organizationAlias },\n permissions: resolveMePermissions(ctx.permissions, session.accessToken, ctx.clientId),\n })\n}\n\nconst BACKCHANNEL_EVENT = \"http://schemas.openid.net/event/backchannel-logout\"\n\nasync function handleRefresh(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\"Refresh session\", {}, async (auth) => {\n const id = await ctx.cookies.read(req.headers.get(\"cookie\"))\n if (!id) {\n auth.set(\"qore.auth.event\", \"refresh_skipped\")\n return NextResponse.json({ error: \"unauthenticated\" }, { status: 401 })\n }\n const session = await ctx.refreshStore.getSession(id)\n if (!session) {\n auth.set(\"qore.auth.event\", \"refresh_skipped\")\n return NextResponse.json({ error: \"unauthenticated\" }, { status: 401 })\n }\n try {\n await refreshWithinSkew(\n {\n refreshStore: ctx.refreshStore,\n kcClient: ctx.kcClient,\n skewSec: 30,\n orgAlias: ctx.orgAlias,\n idleTimeoutSec: ctx.idleTimeoutSec,\n absoluteMaxSec: ctx.absoluteMaxSec,\n },\n id,\n session,\n )\n } catch (err) {\n if (!(err instanceof AuthError)) throw err\n auth.set(\"qore.auth.event\", \"refresh_failed\")\n if (err.reason === \"refresh_unavailable\") {\n auth.fail(\"refresh_unavailable\")\n return NextResponse.json({ error: \"refresh_unavailable\" }, { status: 503 })\n }\n auth.fail(\"session_expired\")\n const res = NextResponse.json({ error: \"session_expired\" }, { status: 401 })\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n return res\n }\n auth.set(\"qore.auth.event\", \"session_refreshed\")\n return new NextResponse(null, { status: 204 })\n })\n}\n\nasync function handleLogout(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\"Log out\", { \"qore.auth.event\": \"logout\", \"qore.auth.method\": \"post\" }, async () => {\n const log = getLogger()\n const id = await ctx.cookies.read(req.headers.get(\"cookie\"))\n if (id) {\n const session = await ctx.refreshStore.getSession(id)\n if (session) {\n try { await ctx.kcClient.logout(session.refreshToken) } catch { /* best-effort */ }\n await ctx.refreshStore.revokeSession(id)\n log.info(\n {\n event: \"auth.logout\",\n method: \"post\",\n user_sub: session.userSub,\n user_email: session.userEmail,\n org_id: session.organizationId,\n org_alias: session.organizationAlias,\n },\n \"auth logout\",\n )\n }\n }\n const res = new NextResponse(null, { status: 204 })\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n // Site data is cleared by GET /api/auth/logged-out, not here — see the note\n // on CLEAR_SITE_DATA. A caller navigates the moment this resolves, and a\n // clear blocks whatever navigation is waiting on the response that carries\n // it.\n return res\n })\n}\n\nasync function handleLogoutRedirect(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\"Log out\", { \"qore.auth.event\": \"logout\", \"qore.auth.method\": \"redirect\" }, async () => {\n const log = getLogger()\n const id = await ctx.cookies.read(req.headers.get(\"cookie\"))\n let idTokenHint: string | undefined\n if (id) {\n const session = await ctx.refreshStore.getSession(id)\n if (session) {\n idTokenHint = session.idToken ?? undefined\n try { await ctx.kcClient.logout(session.refreshToken) } catch { /* best-effort */ }\n await ctx.refreshStore.revokeSession(id)\n log.info(\n {\n event: \"auth.logout\",\n method: \"redirect\",\n user_sub: session.userSub,\n user_email: session.userEmail,\n org_id: session.organizationId,\n org_alias: session.organizationAlias,\n },\n \"auth logout\",\n )\n }\n // A logout that arrives a second time — the user clicked again, the page\n // was reloaded, the first attempt already revoked the row — finds nothing\n // above. Without a hint Keycloak asks the user to confirm, and a\n // confirmation page is a place where ending the SSO session can simply\n // not happen. The ID token of a revoked row is still a truthful statement\n // about who was logged in, and that is all a hint has to be.\n idTokenHint ??= (await ctx.refreshStore.getIdTokenHint(id)) ?? undefined\n }\n const rawRedirect = req.nextUrl.searchParams.get(\"redirect\")\n const target = rawRedirect ? sanitizeRedirect(rawRedirect) : \"/login\"\n // Come back through our own terminal page rather than straight to `target`:\n // that page is the last stop of the chain and the only response that can\n // afford to clear site data. Keycloak accepts it without a config change —\n // both the portal client and the per-app clients register post-logout\n // redirect URIs as `{origin}/*`.\n const landing = new URL(LOGGED_OUT_PATH, ctx.publicOrigin)\n landing.searchParams.set(\"redirect\", target)\n const endSessionUrl = ctx.kcClient.buildEndSessionUrl({\n postLogoutRedirectUri: landing.toString(),\n idTokenHint,\n })\n const res = NextResponse.redirect(endSessionUrl, 302)\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n return res\n })\n}\n\n/** The browser lands here from Keycloak with the logout already done, so nothing\n * is queued behind this response and it is safe to spend time clearing.\n *\n * Measured 2026-08-10 against a real, heavily used Chrome profile: clearing\n * blocks whatever navigation is waiting on the response that carries the\n * header, and the wait scales with how much there is to clear. On the 302 that\n * cost showed up as a logout that never arrived (34s in staging before the user\n * gave up). The same clear on a terminal page costs 74ms — *without* \"cache\".\n * With \"cache\" it was 93_174ms on the same page, which is why only \"cookies\"\n * survives here; \"cache\" also reaches far past the HTTP cache (bfcache,\n * prerender, script and shader caches), and `noStore()` is what actually keeps\n * a personalized response from being replayed on history navigation.\n *\n * Still deliberately not \"storage\": that is origin-wide and would wipe every\n * tenant app's localStorage/IndexedDB on every logout. */\nasync function handleLoggedOut(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n const target = sanitizeRedirect(req.nextUrl.searchParams.get(\"redirect\"))\n // `target` is already a same-origin path, but it still lands inside an HTML\n // attribute and a JS string literal, so escape it for each context.\n const attr = target.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/\"/g, \""\")\n const literal = JSON.stringify(target).replace(/</g, \"\\\\u003c\")\n const res = new NextResponse(\n `<!doctype html><html><head><meta charset=\"utf-8\">` +\n `<meta http-equiv=\"refresh\" content=\"0;url=${attr}\">` +\n `<title>Signing out…</title></head><body>` +\n `<p>Signing out… <a href=\"${attr}\">Continue</a></p>` +\n `<script>location.replace(${literal})</script>` +\n `</body></html>`,\n { status: 200, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } },\n )\n res.headers.set(\"Cache-Control\", \"no-store\")\n res.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n res.headers.set(\"Clear-Site-Data\", CLEAR_SITE_DATA)\n return res\n}\n\nasync function handleBackchannelLogout(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\n \"Log out (backchannel)\",\n { \"qore.auth.event\": \"logout\", \"qore.auth.method\": \"backchannel\" },\n async (auth) => {\n const log = getLogger()\n const form = await req.formData()\n const logoutToken = form.get(\"logout_token\")\n if (typeof logoutToken !== \"string\") {\n auth.fail(\"missing_logout_token\")\n return new NextResponse(\"missing logout_token\", { status: 400 })\n }\n let claims: { sub?: string; events?: Record<string, unknown> }\n try {\n claims = (await ctx.jwks.verify(logoutToken)) as never\n } catch {\n auth.fail(\"invalid_logout_token\")\n return new NextResponse(\"invalid logout_token\", { status: 400 })\n }\n if (!claims.events || !(BACKCHANNEL_EVENT in claims.events)) {\n auth.fail(\"invalid_events\")\n return new NextResponse(\"invalid logout_token events\", { status: 400 })\n }\n if (!claims.sub) {\n auth.fail(\"missing_sub\")\n return new NextResponse(\"missing sub\", { status: 400 })\n }\n await ctx.refreshStore.revokeByUser(claims.sub)\n log.info({ event: \"auth.logout\", method: \"backchannel\", user_sub: claims.sub }, \"auth logout\")\n return new NextResponse(\"ok\", { status: 200 })\n },\n )\n}\n\n/** What the interstitial needs to render, and nothing more: two email\n * addresses. The pending session id is deliberately not returned — the browser\n * never needs it, and the handoff cookie is the only thing allowed to name it. */\nasync function handleIdentityMismatch(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n const handoff = await ctx.handoff.read(req.headers.get(\"cookie\"))\n if (!handoff) {\n return noStore(NextResponse.json({ error: \"no_pending_identity\" }, { status: 404 }))\n }\n const pending = await ctx.refreshStore.getPendingSession(handoff.sessionId)\n if (!pending) {\n return noStore(NextResponse.json({ error: \"no_pending_identity\" }, { status: 404 }))\n }\n return noStore(\n NextResponse.json({\n expected: { email: handoff.expectedEmail },\n actual: { email: pending.userEmail },\n }),\n )\n}\n\nasync function handleConfirmIdentity(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\n \"Confirm identity\",\n { \"qore.auth.event\": \"identity_confirmed\" },\n async (auth) => {\n const log = getLogger()\n // No CSRF token needed and none invented: this is a POST, and the handoff\n // cookie is SameSite=Lax, so a cross-site POST arrives without it and\n // lands here as 409. The cookie is both the authorization and the pointer.\n const handoff = await ctx.handoff.read(req.headers.get(\"cookie\"))\n if (!handoff) {\n auth.fail(\"no_pending_identity\")\n return NextResponse.json({ error: \"no_pending_identity\" }, { status: 409 })\n }\n const pending = await ctx.refreshStore.getPendingSession(handoff.sessionId)\n if (!pending) {\n auth.fail(\"no_pending_identity\")\n const res = NextResponse.json({ error: \"no_pending_identity\" }, { status: 409 })\n res.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return res\n }\n\n // The callback parked this row BEFORE it ran requireAppRole — Task 3 puts\n // the mismatch branch in front of that check on purpose, so an identity\n // the login did not expect is never measured for app access, because a\n // /no-access answer would itself disclose that the account exists here.\n // The consequence is that nothing has yet asked whether this person may\n // use the app, and confirming without asking would hand a session to\n // somebody the callback would have bounced. So the same check runs here,\n // against the same access token, and refuses in the same terms. Nothing\n // is disclosed that an ordinary login by this person would not have\n // disclosed: by this point the interstitial has already shown who came\n // back, and the answer is about the identity now being adopted.\n if (ctx.requireAppRole) {\n // nosemgrep: qdc.jwt.unverified-decode -- pending row's access token was stored by this handler after its own token exchange\n const claims = decodeJwt(pending.accessToken) as ClaimsWithAccess\n if (rolesForClient(claims, ctx.clientId).length === 0) {\n // Revoke rather than leave it pending: the choice this row existed\n // for has been made, and it must not stay confirmable behind the\n // refusal.\n await ctx.refreshStore.revokeSession(handoff.sessionId)\n log.warn(\n {\n event: \"auth.identity.confirm_failed\",\n reason: \"no_app_access\",\n user_sub: pending.userSub,\n user_email: pending.userEmail ?? undefined,\n },\n \"auth identity confirm failed\",\n )\n auth.fail(\"no_app_access\")\n const denied = NextResponse.json({ error: \"no_app_access\" }, { status: 403 })\n denied.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return denied\n }\n }\n\n if (!(await ctx.refreshStore.confirmSession(handoff.sessionId))) {\n // Lost a race, or the row was revoked between the two reads. Returning\n // false here is what stops a replay minting a second session cookie.\n auth.fail(\"no_pending_identity\")\n const res = NextResponse.json({ error: \"no_pending_identity\" }, { status: 409 })\n res.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return res\n }\n log.info(\n {\n event: \"auth.identity.confirmed\",\n user_sub: pending.userSub,\n user_email: pending.userEmail,\n expected_email: handoff.expectedEmail ?? undefined,\n },\n \"auth identity confirmed\",\n )\n const res = new NextResponse(null, { status: 204 })\n // The cookie cannot outlive the row behind it: past the refresh token's\n // own expiry there is nothing left to refresh, so a longer cookie would\n // only produce the \"cookie alive, session dead\" state again.\n res.headers.append(\n \"Set-Cookie\",\n await ctx.cookies.issue(handoff.sessionId, {\n expiresAt: pending.refreshExpiresAt,\n }),\n )\n res.headers.append(\"Set-Cookie\", ctx.handoff.clear())\n return res\n },\n )\n}\n\n/** Resolve the permissions a token grants under the given map. Returns [] when\n * no map is configured (identity-only consumers). Exported for unit testing. */\nexport function resolveMePermissions(\n map: PermissionMap | undefined,\n accessToken: string,\n clientId: string,\n): string[] {\n if (!map) return []\n const roles = extractRoles(decodeJwtPayload(accessToken), clientId)\n return permissionsForRoles(map, roles)\n}\n\nasync function handleCallback(req: NextRequest, ctx: Ctx): Promise<NextResponse> {\n return runAuthSpan(\"Complete login\", {}, async (auth) => {\n const log = getLogger()\n const code = req.nextUrl.searchParams.get(\"code\")\n const state = req.nextUrl.searchParams.get(\"state\")\n\n // Every exit below the browser-binding check consumes the transaction:\n // the row leaves `login_state` and the state leaves the browser's\n // qdc_login cookie. Defined this early (rather than only after the\n // code/state check) so the OIDC-error branch — which never has a `code`\n // — can use it too; a restart that left a stale state behind would waste\n // one of the three binding slots for no benefit once the row itself is\n // gone. Tolerant of a missing `state`: consume() is simply a no-op then,\n // since there is nothing to clear.\n //\n // The one exception is the binding check itself, below: an unbound state\n // is not proven to belong to this browser, so it returns directly\n // without ever touching the row or the cookie — that is the entire point\n // of the check.\n const consume = async (res: NextResponse): Promise<NextResponse> => {\n if (state) {\n await ctx.db.delete(loginState).where(eq(loginState.state, state))\n res.headers.append(\"Set-Cookie\", removeBoundState(req.headers.get(\"cookie\"), state))\n }\n return res\n }\n\n // RFC 9700 §2.1.1 — the transaction must be bound to the user agent that\n // started it. Without this, whoever opens the callback URL with a stolen\n // (non-secret — it appears in history/referrers) `state` could delete\n // another transaction's row and force this browser into an unrequested\n // redirect. Returns null when there is nothing to check (no `state` at\n // all — e.g. an OIDC error with no `state` echoed back), letting the\n // caller fall through to its normal missing-state handling. Must run\n // before ANY branch reads or deletes the row, which is why it is called\n // both at the top of the OIDC-error branch below and again ahead of the\n // ordinary row lookup further down.\n const checkBinding = (): NextResponse | null => {\n if (!state) return null\n if (readBoundStates(req.headers.get(\"cookie\")).includes(state)) return null\n log.warn({ event: \"auth.login.failed\", reason: \"state_not_bound\" }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"state_not_bound\")\n return new NextResponse(\"state_not_bound\", { status: 400 })\n }\n\n // Keycloak's authorization-error response: `error` (+ optionally\n // `error_description`, and usually `state`) instead of `code`. Read this\n // BEFORE the `missing code or state` check below — every KC error has no\n // `code`, so without this branch every one of them fell into that generic\n // 400 and logged as the same undiagnosable `missing_params` (production:\n // seven failed logins in ten minutes, all logged as `missing_params`, the\n // real reason — `already_logged_in` — visible only in the KC admin\n // console; qdc-roadmap#136).\n const oidcError = req.nextUrl.searchParams.get(\"error\")\n if (oidcError) {\n // Gate this branch on the binding check too: a `state` on an error\n // callback is exactly as forgeable as one on a success callback, and\n // this branch is the one that reads and deletes the row. When the\n // binding cookie has itself expired (Max-Age 600s) before an error\n // callback arrives, this now yields state_not_bound instead of the\n // auto-restart — fail-closed, and the user simply sees a failed-login\n // page instead of being bounced back to /login; they can retry by hand.\n const bindingFailure = checkBinding()\n if (bindingFailure) return bindingFailure\n\n const description = req.nextUrl.searchParams.get(\"error_description\") ?? undefined\n log.warn(\n { event: \"auth.login.failed\", reason: oidcError, error_description: description },\n \"auth login failed\",\n )\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(oidcError)\n\n // authentication_expired / login_required are Keycloak's explicit\n // \"start over\" signals, not \"show the user a 400\". One restart only:\n // the bound comes from `login_state.retried`, NOT from a `retried`\n // query parameter echoed back on this callback — the callback's\n // `redirect_uri` is fixed by Keycloak, which never echoes our own query\n // parameters back to it, so a guard read only off this URL could never\n // fire in production and the redirect would bounce between the two\n // servers forever. `handleLogin` stamps `retried: true` on the NEW row\n // it inserts when ITS OWN request carries `retried=1` — that request is\n // our own restart hitting /api/auth/login, so the parameter really is\n // present there. The query-string check below is kept anyway (the\n // brief's tests rely on it, and it costs nothing): it is a\n // belt-and-braces check, not the real bound.\n const restartable = oidcError === \"authentication_expired\" || oidcError === \"login_required\"\n const queryRetried = req.nextUrl.searchParams.get(\"retried\") === \"1\"\n\n let alreadyRetried = queryRetried\n let retryRow: typeof loginState.$inferSelect | undefined\n if (!state) {\n // No `state` at all means there is no row to consult and nothing to\n // bind a restart to. Defaulting to \"allow a restart\" here is exactly\n // the unbounded case this guard exists to prevent, so a missing\n // transaction is treated the same as an already-retried one: fail\n // closed rather than license a blind restart.\n alreadyRetried = true\n } else {\n const [row] = await ctx.db\n .select().from(loginState).where(eq(loginState.state, state)).limit(1)\n // Same reasoning for a `state` that IS present but matches no row\n // (expired, already consumed by an earlier exit, or simply invalid):\n // we cannot prove this is a fresh, first attempt, so we do not grant\n // another restart on the strength of a row we can't read.\n if (!row || row.retried) alreadyRetried = true\n retryRow = row\n }\n\n if (restartable && !alreadyRetried) {\n // Carry the original login's destination and typed address through\n // the restart, or this bounces the user to \"/\" and drops the address\n // they typed instead of resuming what they asked for — the ordinary\n // outcome now that a refresh-expired session (routine, not an odd\n // path) sends id_token_hint and lands here whenever Keycloak's own\n // SSO session is also dead. `retryRow` is defined whenever this\n // branch is reached (alreadyRetried is only false when `state` was\n // present and its row existed and was unread), so this is not a\n // fallback for a missing row, just satisfying the type.\n const restart = new URL(\"/api/auth/login\", ctx.publicOrigin)\n restart.searchParams.set(\"prompt\", \"login\")\n restart.searchParams.set(\"retried\", \"1\")\n if (retryRow?.redirectPath) restart.searchParams.set(\"redirect\", retryRow.redirectPath)\n if (retryRow?.expectedEmail) restart.searchParams.set(\"login_hint\", retryRow.expectedEmail)\n return await consume(NextResponse.redirect(restart, 302))\n }\n return await consume(new NextResponse(`login failed: ${oidcError}`, { status: 400 }))\n }\n\n // RFC 9700 §2.1.1 — the transaction must be bound to the user agent that\n // started it. Without this, whoever opens the callback URL gets the\n // session — or, on the `missing_params` branch just below, gets to\n // delete another transaction's row by supplying its `state` with no\n // `code`. Must run BEFORE that branch, not after: `missing_params` also\n // calls `consume()`, and a check placed after an early return is dead\n // code. (checkBinding() is a no-op when `state` is absent, so this does\n // not disturb the ordinary missing-both-params case.) The `oidcError`\n // branch above already ran its own copy of this same check ahead of\n // touching the row, so this second call is unreachable whenever `error`\n // was present — it exists for every other request shape.\n const bindingFailure = checkBinding()\n if (bindingFailure) return bindingFailure\n\n if (!code || !state) {\n log.warn({ event: \"auth.login.failed\", reason: \"missing_params\" }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"missing_params\")\n // A bound `state` with no `code` (and no `error`) is a leftover\n // transaction that will never be finished; consume() closes it out\n // (a no-op below when `state` is absent, the common case here). Safe\n // to consume unconditionally here: checkBinding() above already\n // proved a present `state` belongs to this browser.\n return await consume(new NextResponse(\"missing code or state\", { status: 400 }))\n }\n\n // `consume` was already defined above, ahead of the OIDC-error branch —\n // it is safe to reuse here since `state` is guaranteed non-null past this\n // point (the `missing code or state` check above returned already).\n\n const [row] = await ctx.db\n .select().from(loginState).where(eq(loginState.state, state)).limit(1)\n if (!row) {\n log.warn({ event: \"auth.login.failed\", reason: \"invalid_state\" }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"invalid_state\")\n return await consume(new NextResponse(\"invalid state\", { status: 400 }))\n }\n if (row.expiresAt < new Date()) {\n log.warn({ event: \"auth.login.failed\", reason: \"expired_state\" }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"expired_state\")\n return await consume(new NextResponse(\"expired state\", { status: 400 }))\n }\n\n // Whatever identity the browser carried into this callback is being\n // replaced — either by the new one below, or by nothing at all on the\n // no-access path. Either way the old session must not survive it: the\n // cookie is about to be overwritten or cleared, but the row would stay\n // valid with a working refresh token behind it.\n const previousSessionId = await ctx.cookies.read(req.headers.get(\"cookie\"))\n\n // RFC 9207 / RFC 9700 §2.1 — mix-up defence. Absent is allowed: an AS that\n // does not send `iss` is not a mismatch. A wrong one is.\n const iss = req.nextUrl.searchParams.get(\"iss\")\n if (iss && iss !== ctx.kcClient.issuer) {\n log.warn({ event: \"auth.login.failed\", reason: \"iss_mismatch\", iss }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"iss_mismatch\")\n return await consume(new NextResponse(\"iss_mismatch\", { status: 400 }))\n }\n\n try {\n const tokens = await ctx.kcClient.exchangeCode({ code, codeVerifier: row.codeVerifier })\n\n // Not a missing signature check: OIDC 3.1.3.7 point 6 allows TLS server\n // validation in place of verifying the signature when the token came\n // straight from the token endpoint over a direct connection, which is this\n // flow. Tokens that arrive any other way (backchannel logout) do go\n // through jwks.verify — see handleBackchannelLogout. Decoded lazily: a\n // row that needs neither check (no nonce, no forced reauth) must not\n // pay for parsing the ID token.\n const idClaims: { nonce?: string; auth_time?: number } | undefined =\n row.nonce || row.forcedReauth\n // nosemgrep: qdc.jwt.unverified-decode -- id token came back from the authorization server's token endpoint in this same request\n ? (decodeJwt(tokens.idToken) as { nonce?: string; auth_time?: number })\n : undefined\n\n // RFC 9700 §4.5.3.2 — until this check passes, every token from this\n // exchange is disregarded. A row without a nonce predates this build.\n if (row.nonce) {\n if (idClaims?.nonce !== row.nonce) {\n log.warn(\n { event: \"auth.login.failed\", reason: \"nonce_mismatch\" },\n \"auth login failed\",\n )\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"nonce_mismatch\")\n return await consume(new NextResponse(\"nonce_mismatch\", { status: 400 }))\n }\n }\n\n // nosemgrep: qdc.jwt.unverified-decode -- access token came back from the authorization server's token endpoint in this same request\n const claims = decodeJwt(tokens.accessToken) as ClaimsWithAccess & { sub: string; email?: string; organization?: unknown }\n\n // OIDC 3.1.2.1: when max_age is sent the ID token MUST carry auth_time.\n // We only sent it on a forced login, and there it is the proof that the\n // authorization server really re-authenticated instead of replaying its\n // SSO session. A missing or stale claim means the identity in front of us\n // was never re-proven, so it is not adopted.\n //\n // This MUST be read from the ID token, not the access token: OIDC only\n // mandates auth_time on the ID token, and Keycloak does not necessarily\n // mirror it onto the access token. Reading it off `claims` (decoded from\n // tokens.accessToken) would make this check fail closed on every forced\n // login in production. No fallback to the access token either — the ID\n // token is the normative source; a fallback would let a non-conforming\n // AS satisfy the check.\n if (row.forcedReauth) {\n const authTime = idClaims?.auth_time\n const requestedAt = Math.floor(row.createdAt.getTime() / 1000) - AUTH_TIME_SKEW_SEC\n // Also reject an auth_time in the future (beyond the same skew\n // allowance): a value \"verified against the request time\" must be\n // bounded on both sides, or an AS/token bug that reports a\n // far-future auth_time would pass unconditionally.\n const nowAt = Math.floor(Date.now() / 1000) + AUTH_TIME_SKEW_SEC\n if (typeof authTime !== \"number\" || authTime < requestedAt || authTime > nowAt) {\n log.warn(\n { event: \"auth.login.failed\", reason: \"stale_auth_time\", user_sub: claims.sub },\n \"auth login failed\",\n )\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"stale_auth_time\")\n return await consume(new NextResponse(\"re-authentication was not performed\", { status: 400 }))\n }\n }\n\n // Whom this login was supposed to end up as, recorded by handleLogin.\n // Checked after the nonce and auth_time checks — those decide whether\n // the tokens may be looked at at all, and this one reads claims out of\n // them — and before requireAppRole, so a mismatching identity is never\n // measured for app access, let alone told whether it has any.\n const expectedSub = row.expectedSub\n const expectedEmail = row.expectedEmail\n const actualEmail = claims.email ?? null\n const subMismatch = Boolean(expectedSub) && claims.sub !== expectedSub\n const emailMismatch =\n Boolean(expectedEmail) &&\n expectedEmail!.toLowerCase() !== (actualEmail ?? \"\").toLowerCase()\n\n if (subMismatch || emailMismatch) {\n // Do not adopt this identity. The tokens are real and the login is\n // valid — it is simply not the person this browser was acting as, or\n // the person who typed their address. Park it and let a human decide.\n const org = parseOrganizationClaim(claims.organization)\n const pendingId = await ctx.refreshStore.createSession({\n userSub: claims.sub,\n userEmail: claims.email,\n organizationId: org.id,\n organizationAlias: org.alias,\n accessToken: tokens.accessToken,\n accessExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),\n idToken: tokens.idToken,\n refreshToken: tokens.refreshToken,\n refreshExpiresAt: new Date(Date.now() + tokens.refreshExpiresIn * 1000),\n pendingIdentityConfirm: true,\n })\n if (previousSessionId) await ctx.refreshStore.revokeSession(previousSessionId)\n log.warn(\n {\n event: \"auth.login.identity_mismatch\",\n reason: subMismatch ? \"sub_mismatch\" : \"email_mismatch\",\n expected_sub: expectedSub ?? undefined,\n expected_email: expectedEmail ?? undefined,\n user_sub: claims.sub,\n user_email: actualEmail ?? undefined,\n },\n \"auth login identity mismatch\",\n )\n auth.set(\"qore.auth.event\", \"login_identity_mismatch\")\n auth.fail(\"identity_mismatch\")\n const target = new URL(\n ctx.identityMismatchPath ?? DEFAULT_IDENTITY_MISMATCH_PATH,\n ctx.publicOrigin,\n )\n const res = NextResponse.redirect(target, 302)\n // The old identity does not survive this either: its row is revoked\n // above and its cookie goes now. Nobody is signed in until someone\n // chooses.\n res.headers.append(\"Set-Cookie\", ctx.cookies.clear())\n res.headers.append(\n \"Set-Cookie\",\n await ctx.handoff.issue({\n sessionId: pendingId,\n expectedSub,\n expectedEmail,\n actualEmail,\n }),\n )\n return await consume(res)\n }\n\n if (ctx.requireAppRole && rolesForClient(claims, ctx.clientId).length === 0) {\n log.warn(\n { event: \"auth.login.failed\", reason: \"no_app_access\", user_sub: claims.sub, user_email: claims.email },\n \"auth login failed\",\n )\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"no_app_access\")\n if (previousSessionId) await ctx.refreshStore.revokeSession(previousSessionId)\n const denied = NextResponse.redirect(new URL(\"/no-access\", ctx.publicOrigin), 302)\n // Without this the previous identity's cookie survives the redirect:\n // one click to / and the browser is that person again.\n denied.headers.set(\"Set-Cookie\", ctx.cookies.clear())\n return await consume(denied)\n }\n if (previousSessionId) await ctx.refreshStore.revokeSession(previousSessionId)\n const org = parseOrganizationClaim(claims.organization)\n const refreshExpiresAt = new Date(Date.now() + tokens.refreshExpiresIn * 1000)\n const sessionId = await ctx.refreshStore.createSession({\n userSub: claims.sub,\n userEmail: claims.email,\n organizationId: org.id,\n organizationAlias: org.alias,\n accessToken: tokens.accessToken,\n accessExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),\n idToken: tokens.idToken,\n refreshToken: tokens.refreshToken,\n refreshExpiresAt,\n })\n\n log.info(\n {\n event: \"auth.login.succeeded\",\n user_sub: claims.sub,\n user_email: claims.email,\n org_id: org.id,\n org_alias: org.alias,\n },\n \"auth login succeeded\",\n )\n auth.set(\"qore.auth.event\", \"login_succeeded\")\n if (org.alias) auth.set(\"qore.auth.org_alias\", org.alias)\n\n // Resolve the stored redirect path against the configured public origin, not\n // req.url: behind a proxy (Northflank) the container sees the internal bind\n // address (e.g. http://0.0.0.0:8080), which would leak into the Location.\n const res = NextResponse.redirect(new URL(row.redirectPath, ctx.publicOrigin), 302)\n // The cookie dies with the refresh token: a cookie that outlives the row\n // is what makes \"cookie alive, session dead\" — and a silent re-login — the\n // normal state instead of the exception.\n res.headers.set(\"Set-Cookie\", await ctx.cookies.issue(sessionId, { expiresAt: refreshExpiresAt }))\n return await consume(res)\n } catch (err) {\n log.error({ event: \"auth.login.failed\", reason: \"token_exchange\", err }, \"auth login failed\")\n auth.set(\"qore.auth.event\", \"login_failed\")\n auth.fail(\"token_exchange\")\n // This path rethrows — there is no response to attach a cleared\n // qdc_login cookie to — but the row must still not survive a failed\n // exchange, or a `state` whose token exchange failed stays a valid,\n // replayable transaction for up to 10 minutes.\n await ctx.db.delete(loginState).where(eq(loginState.state, state))\n throw err\n }\n })\n}\n","import { webcrypto } from \"node:crypto\"\n\nconst SUBTLE = webcrypto.subtle\n\nexport type Cipher = { key: CryptoKey }\n\n// HKDF salt/info are fixed, so the derived key is a pure function of the hex\n// secret — safe to memoize per-secret across the process lifetime. Cache the\n// in-flight promise (not just the resolved value) so concurrent callers that\n// race in before the first derivation settles still dedupe onto one derive.\nconst cipherCache = new Map<string, Promise<Cipher>>()\n\nasync function deriveCipher(hexSecret: string): Promise<Cipher> {\n const raw = Buffer.from(hexSecret, \"hex\")\n const baseKey = await SUBTLE.importKey(\"raw\", raw, \"HKDF\", false, [\"deriveKey\"])\n const key = await SUBTLE.deriveKey(\n {\n name: \"HKDF\",\n hash: \"SHA-256\",\n salt: new Uint8Array(16),\n info: new TextEncoder().encode(\"qdc-sdk:refresh-token:v1\"),\n },\n baseKey,\n { name: \"AES-GCM\", length: 256 },\n false,\n [\"encrypt\", \"decrypt\"],\n )\n return { key }\n}\n\nexport async function createCipher(hexSecret: string): Promise<Cipher> {\n if (hexSecret.length < 32) {\n throw new Error(\"SESSION_COOKIE_SECRET must be at least 32 hex chars\")\n }\n const cached = cipherCache.get(hexSecret)\n if (cached) return cached\n const promise = deriveCipher(hexSecret)\n cipherCache.set(hexSecret, promise)\n return promise\n}\n\nexport async function encrypt(cipher: Cipher, plaintext: string) {\n const iv = webcrypto.getRandomValues(new Uint8Array(12))\n const enc = await SUBTLE.encrypt(\n { name: \"AES-GCM\", iv },\n cipher.key,\n new TextEncoder().encode(plaintext),\n )\n return { ciphertext: new Uint8Array(enc), iv }\n}\n\nexport async function decrypt(\n cipher: Cipher,\n ciphertext: Uint8Array,\n iv: Uint8Array,\n): Promise<string> {\n const dec = await SUBTLE.decrypt({ name: \"AES-GCM\", iv }, cipher.key, ciphertext)\n return new TextDecoder().decode(dec)\n}\n","import { webcrypto } from \"node:crypto\"\n\nexport const COOKIE_NAME = \"qdc_session\"\n\n/** Absolute ceiling on a session cookie when the caller gives no expiry. The old\n * value was 30 days, which is how \"cookie alive, session dead\" became the normal\n * state rather than the exception — and that state is what triggered the silent\n * re-login behind the 2026-08-04 identity incident. */\nexport const DEFAULT_ABSOLUTE_MAX_SEC = 12 * 60 * 60\n\nconst SUBTLE = webcrypto.subtle\n\nfunction b64url(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString(\"base64url\")\n}\nfunction b64urlDecode(s: string): Uint8Array {\n return new Uint8Array(Buffer.from(s, \"base64url\"))\n}\n\nasync function hmacKey(hexSecret: string): Promise<CryptoKey> {\n const raw = Buffer.from(hexSecret, \"hex\")\n return SUBTLE.importKey(\n \"raw\",\n raw,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\", \"verify\"],\n )\n}\n\nexport function sessionCookie(opts: { secret: string }) {\n if (opts.secret.length < 32) throw new Error(\"session cookie secret too short\")\n\n async function sign(id: string): Promise<string> {\n const key = await hmacKey(opts.secret)\n const mac = await SUBTLE.sign(\"HMAC\", key, new TextEncoder().encode(id))\n return `${b64url(new TextEncoder().encode(id))}.${b64url(new Uint8Array(mac))}`\n }\n\n async function verify(value: string): Promise<string | null> {\n const parts = value.split(\".\")\n if (parts.length !== 2) return null\n const [encId, encMac] = parts as [string, string]\n let idBytes: Uint8Array\n let macBytes: Uint8Array\n try {\n idBytes = b64urlDecode(encId)\n macBytes = b64urlDecode(encMac)\n } catch { return null }\n const key = await hmacKey(opts.secret)\n const ok = await SUBTLE.verify(\"HMAC\", key, macBytes, idBytes)\n return ok ? new TextDecoder().decode(idBytes) : null\n }\n\n async function issue(id: string, opts?: { expiresAt?: Date }): Promise<string> {\n const signed = await sign(id)\n const maxAge = opts?.expiresAt\n ? Math.max(0, Math.floor((opts.expiresAt.getTime() - Date.now()) / 1000))\n : DEFAULT_ABSOLUTE_MAX_SEC\n return `${COOKIE_NAME}=${signed}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`\n }\n\n function clear(): string {\n return `${COOKIE_NAME}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`\n }\n\n async function read(cookieHeader: string | null | undefined): Promise<string | null> {\n if (!cookieHeader) return null\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim()\n if (!trimmed.startsWith(`${COOKIE_NAME}=`)) continue\n const value = trimmed.slice(COOKIE_NAME.length + 1)\n return verify(value)\n }\n return null\n }\n\n return { issue, clear, read }\n}\n","export type KcConfig = {\n keycloakUrl: string\n realm: string\n clientId: string\n clientSecret: string\n publicOrigin: string\n orgAlias?: string\n /** \"auto\" (default): request `organization:<orgAlias>` when orgAlias is set,\n * else the bare `organization` scope. \"none\": never request the organization\n * scope — for identity-first consumers (the portal) where Keycloak's\n * org-selection prompt must not appear for multi-org users. */\n organizationScope?: \"auto\" | \"none\"\n /** Upper bound (ms) on the Keycloak token- and logout-endpoint fetches.\n * Bounded because refresh runs inside an open pg transaction holding a row\n * lock (a hung KC would pin the connection) and because logout runs inside\n * the request the browser is waiting on (a hung KC would leave the logout\n * navigation never returning). Defaults to DEFAULT_TOKEN_TIMEOUT_MS. */\n tokenTimeoutMs?: number\n}\n\nconst DEFAULT_TOKEN_TIMEOUT_MS = 10_000\n\nexport type KcTokens = {\n accessToken: string\n refreshToken: string\n idToken: string\n expiresIn: number\n refreshExpiresIn: number\n}\n\nexport type KcClient = ReturnType<typeof createKcClient>\n\nexport class KcTokenError extends Error {\n constructor(\n public status: number,\n public oauthError: string | undefined,\n body: string,\n ) {\n super(`KC token endpoint ${status}: ${body}`)\n this.name = \"KcTokenError\"\n }\n}\n\nexport function createKcClient(cfg: KcConfig) {\n const issuer = `${cfg.keycloakUrl}/realms/${cfg.realm}`\n const tokenUrl = `${issuer}/protocol/openid-connect/token`\n const logoutUrl = `${issuer}/protocol/openid-connect/logout`\n const authorizeUrl = `${issuer}/protocol/openid-connect/auth`\n const redirectUri = `${cfg.publicOrigin}/api/auth/callback`\n\n function scope(): string {\n const base = \"openid profile email\"\n if (cfg.organizationScope === \"none\") return base\n return cfg.orgAlias ? `${base} organization:${cfg.orgAlias}` : `${base} organization`\n }\n\n function buildAuthorizeUrl(args: {\n state: string\n codeChallenge: string\n nonce?: string\n loginHint?: string\n kcIdpHint?: string\n prompt?: string\n maxAge?: number\n idTokenHint?: string\n }) {\n const params = new URLSearchParams({\n response_type: \"code\",\n client_id: cfg.clientId,\n redirect_uri: redirectUri,\n scope: scope(),\n state: args.state,\n code_challenge: args.codeChallenge,\n code_challenge_method: \"S256\",\n })\n if (args.nonce) params.set(\"nonce\", args.nonce)\n if (args.loginHint) params.set(\"login_hint\", args.loginHint)\n if (args.kcIdpHint) params.set(\"kc_idp_hint\", args.kcIdpHint)\n if (args.prompt) params.set(\"prompt\", args.prompt)\n // max_age is not redundant next to prompt=login: it is what obliges the AS\n // to return an auth_time claim (OIDC 3.1.2.1), which is the only way the\n // callback can verify that a re-authentication actually happened rather\n // than trust that Keycloak honoured the prompt.\n if (args.maxAge !== undefined) params.set(\"max_age\", String(args.maxAge))\n // Asks the AS to confirm the existing session still belongs to this user\n // and to answer login_required when it does not, instead of silently\n // handing back whoever its SSO cookie names. Only meaningful when we are\n // NOT forcing a prompt — alongside prompt=login it adds nothing.\n if (args.idTokenHint) params.set(\"id_token_hint\", args.idTokenHint)\n return `${authorizeUrl}?${params.toString().replace(/\\+/g, \"%20\")}`\n }\n\n function buildEndSessionUrl(args: { postLogoutRedirectUri: string; idTokenHint?: string }) {\n const params = new URLSearchParams({\n client_id: cfg.clientId,\n post_logout_redirect_uri: args.postLogoutRedirectUri,\n })\n // Without id_token_hint, Keycloak's end-session endpoint shows a \"do you\n // want to log out?\" confirmation page instead of ending the SSO session\n // directly.\n if (args.idTokenHint) params.set(\"id_token_hint\", args.idTokenHint)\n return `${logoutUrl}?${params.toString()}`\n }\n\n async function postToken(body: URLSearchParams): Promise<KcTokens> {\n body.set(\"client_id\", cfg.clientId)\n body.set(\"client_secret\", cfg.clientSecret)\n // Bounded because refresh runs under a pg row lock — a hung KC must not\n // pin the checked-out connection for the full hang duration.\n const timeoutMs = cfg.tokenTimeoutMs ?? DEFAULT_TOKEN_TIMEOUT_MS\n const res = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body,\n signal: AbortSignal.timeout(timeoutMs),\n })\n if (!res.ok) {\n const body = await res.text()\n let oauthError: string | undefined\n try {\n oauthError = (JSON.parse(body) as { error?: string }).error\n } catch {\n /* non-JSON error body */\n }\n throw new KcTokenError(res.status, oauthError, body)\n }\n const j = (await res.json()) as {\n access_token: string\n refresh_token: string\n id_token: string\n expires_in: number\n refresh_expires_in: number\n }\n return {\n accessToken: j.access_token,\n refreshToken: j.refresh_token,\n idToken: j.id_token,\n expiresIn: j.expires_in,\n refreshExpiresIn: j.refresh_expires_in,\n }\n }\n\n async function exchangeCode(args: { code: string; codeVerifier: string }) {\n const body = new URLSearchParams({\n grant_type: \"authorization_code\",\n code: args.code,\n redirect_uri: redirectUri,\n code_verifier: args.codeVerifier,\n })\n return postToken(body)\n }\n\n async function refresh(refreshToken: string) {\n const body = new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n })\n return postToken(body)\n }\n\n async function logout(refreshToken: string): Promise<void> {\n const body = new URLSearchParams({\n client_id: cfg.clientId,\n client_secret: cfg.clientSecret,\n refresh_token: refreshToken,\n })\n // Bounded for a different reason than postToken: this runs inside the\n // request the browser is waiting on, so an unbounded fetch turns a slow\n // Keycloak into a logout navigation that never returns. Callers treat the\n // revocation as best-effort, so timing out is the right outcome — the\n // session row is revoked either way and Keycloak expires its own.\n const res = await fetch(logoutUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body,\n signal: AbortSignal.timeout(cfg.tokenTimeoutMs ?? DEFAULT_TOKEN_TIMEOUT_MS),\n })\n if (!res.ok && res.status !== 204) {\n throw new Error(`KC logout ${res.status}: ${await res.text()}`)\n }\n }\n\n return { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, refresh, logout, redirectUri, scope, issuer }\n}\n","import { createRemoteJWKSet, jwtVerify, type JWTPayload } from \"jose\"\n\nexport type JwksOptions = {\n keycloakUrl: string\n realm: string\n audience: string\n cooldownMs?: number\n}\n\nexport type Jwks = {\n verify: (token: string) => Promise<JWTPayload>\n}\n\nexport function createJwks(opts: JwksOptions): Jwks {\n const url = new URL(`${opts.keycloakUrl}/realms/${opts.realm}/protocol/openid-connect/certs`)\n const jwkset = createRemoteJWKSet(url, {\n cooldownDuration: opts.cooldownMs ?? 30_000,\n })\n const issuer = `${opts.keycloakUrl}/realms/${opts.realm}`\n\n async function verify(token: string): Promise<JWTPayload> {\n const { payload } = await jwtVerify(token, jwkset, {\n audience: opts.audience,\n issuer,\n })\n return payload\n }\n\n return { verify }\n}\n","import { and, eq, isNull } from \"drizzle-orm\"\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\"\nimport { sessions } from \"./schema\"\nimport { decrypt, encrypt, type Cipher } from \"./crypto\"\n\nexport type CreateSessionInput = {\n userSub: string\n userEmail?: string\n organizationId?: string\n organizationAlias?: string\n accessToken: string\n accessExpiresAt: Date\n idToken?: string\n refreshToken: string\n refreshExpiresAt: Date\n pendingIdentityConfirm?: boolean\n}\n\nexport type Session = {\n id: string\n userSub: string\n userEmail: string | null\n organizationId: string | null\n organizationAlias: string | null\n accessToken: string\n accessExpiresAt: Date\n idToken: string | null\n refreshToken: string\n refreshExpiresAt: Date\n createdAt: Date\n updatedAt: Date\n}\n\nexport type RotateInput = {\n accessToken: string\n accessExpiresAt: Date\n idToken?: string\n refreshToken: string\n refreshExpiresAt: Date\n}\n\nexport type LockedSession = {\n /** Row state after acquiring the row lock; null if revoked/deleted meanwhile. */\n session: Session | null\n rotate: (t: RotateInput) => Promise<void>\n revoke: () => Promise<void>\n}\n\nexport type RefreshStore = ReturnType<typeof createRefreshStore>\n\n/** How long a read `getSession()` result may be served from the in-memory\n * cache before the next call re-queries the DB. Bounds the staleness window\n * for out-of-band revocations from other process instances (see module doc\n * below); refresh-within-skew logic still runs against the cached row on\n * every call, so an actually-expired access token is never served stale —\n * only revocation-detection latency is bounded by this TTL. */\nexport const SESSION_CACHE_TTL_MS = 5000\n\ntype SessionCacheEntry = { session: Session; at: number }\n\nexport function createRefreshStore(opts: {\n db: NodePgDatabase\n cipher: Cipher\n /** Clock source for the session cache, in arbitrary monotonic units\n * (default: `performance.now()`, milliseconds). Overridable in tests so\n * TTL expiry can be exercised without a real 5s sleep. */\n clock?: () => number\n}) {\n const { db, cipher } = opts\n const now = opts.clock ?? (() => performance.now())\n\n // Short-TTL read cache for getSession(), keyed by session id. Populated on\n // a real DB hit; invalidated on any write that can change the row (rotate,\n // single-session revoke) or that can affect many rows at once (revoke-all,\n // which just clears the whole cache — simpler than per-user bookkeeping and\n // rare on the hot path).\n const sessionCache = new Map<string, SessionCacheEntry>()\n\n async function createSession(input: CreateSessionInput): Promise<string> {\n const { ciphertext, iv } = await encrypt(cipher, input.refreshToken)\n const [row] = await db\n .insert(sessions)\n .values({\n userSub: input.userSub,\n userEmail: input.userEmail,\n organizationId: input.organizationId,\n organizationAlias: input.organizationAlias,\n accessToken: input.accessToken,\n accessExpiresAt: input.accessExpiresAt,\n idToken: input.idToken,\n refreshCt: ciphertext,\n refreshIv: iv,\n refreshExpiresAt: input.refreshExpiresAt,\n pendingIdentityConfirm: input.pendingIdentityConfirm ?? false,\n })\n .returning({ id: sessions.id })\n return row!.id\n }\n\n async function rowToSession(row: typeof sessions.$inferSelect): Promise<Session> {\n const refreshToken = await decrypt(cipher, row.refreshCt, row.refreshIv)\n return {\n id: row.id,\n userSub: row.userSub,\n userEmail: row.userEmail,\n organizationId: row.organizationId,\n organizationAlias: row.organizationAlias,\n accessToken: row.accessToken,\n accessExpiresAt: row.accessExpiresAt,\n idToken: row.idToken,\n refreshToken,\n refreshExpiresAt: row.refreshExpiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }\n }\n\n async function getSession(id: string): Promise<Session | null> {\n const cached = sessionCache.get(id)\n if (cached && now() - cached.at < SESSION_CACHE_TTL_MS) {\n return cached.session\n }\n const [row] = await db\n .select()\n .from(sessions)\n .where(\n and(\n eq(sessions.id, id),\n isNull(sessions.revokedAt),\n // A pending row is invisible here on purpose: this is the read every\n // guard funnels through (requireAuth, getAccessToken, /me, refresh),\n // so filtering once is what makes \"pending grants nothing\" true\n // everywhere instead of at each call site.\n eq(sessions.pendingIdentityConfirm, false),\n ),\n )\n .limit(1)\n if (!row) {\n sessionCache.delete(id)\n return null\n }\n const session = await rowToSession(row)\n sessionCache.set(id, { session, at: now() })\n return session\n }\n\n /** Read a row that is still awaiting identity confirmation. Deliberately\n * uncached: it is read once, by the confirm/inspect endpoints, and caching a\n * row whose whole point is that it is about to change state is a trap. */\n async function getPendingSession(id: string): Promise<Session | null> {\n const [row] = await db\n .select()\n .from(sessions)\n .where(\n and(\n eq(sessions.id, id),\n isNull(sessions.revokedAt),\n eq(sessions.pendingIdentityConfirm, true),\n ),\n )\n .limit(1)\n return row ? rowToSession(row) : null\n }\n\n /** The session's stored ID token, for the sole purpose of putting it in\n * `id_token_hint` on Keycloak's end-session URL.\n *\n * Deliberately ignores `revokedAt` and `pendingIdentityConfirm`, which every\n * other read filters on. Logging out is the one operation that must still\n * work *after* the row is revoked: without the hint Keycloak shows a\n * \"do you want to log out?\" page (maintainers, keycloak#12183: a valid hint\n * is always honoured, an absent one always prompts), and that page's button\n * is one the user can simply never press — leaving the SSO session alive.\n * So a retried or second-pass logout must still be able to produce it.\n *\n * Returns the ID token and nothing else. An ID token is an assertion about a\n * past authentication, not a credential, and RP-Initiated Logout 1.0 says an\n * expired one is acceptable here — while the refresh and access tokens stay\n * behind `getSession()`, so a revoked row still grants nothing.\n *\n * Uncached on purpose: it is read at most once per logout, and caching a row\n * whose whole point is that it has just been revoked is a trap. */\n async function getIdTokenHint(id: string): Promise<string | null> {\n const [row] = await db\n .select({ idToken: sessions.idToken })\n .from(sessions)\n .where(eq(sessions.id, id))\n .limit(1)\n return row?.idToken ?? null\n }\n\n /** Flip a pending row to confirmed. Returns false when there was nothing to\n * confirm — the caller issues a session cookie on true, so this must never\n * report success twice for one row. */\n async function confirmSession(id: string): Promise<boolean> {\n const rows = await db\n .update(sessions)\n .set({ pendingIdentityConfirm: false, updatedAt: new Date() })\n .where(\n and(\n eq(sessions.id, id),\n isNull(sessions.revokedAt),\n eq(sessions.pendingIdentityConfirm, true),\n ),\n )\n .returning({ id: sessions.id })\n sessionCache.delete(id)\n return rows.length === 1\n }\n\n async function rotateTokens(id: string, t: RotateInput): Promise<void> {\n const { ciphertext, iv } = await encrypt(cipher, t.refreshToken)\n await db\n .update(sessions)\n .set({\n accessToken: t.accessToken,\n accessExpiresAt: t.accessExpiresAt,\n ...(t.idToken !== undefined ? { idToken: t.idToken } : {}),\n refreshCt: ciphertext,\n refreshIv: iv,\n refreshExpiresAt: t.refreshExpiresAt,\n updatedAt: new Date(),\n })\n .where(eq(sessions.id, id))\n sessionCache.delete(id)\n }\n\n async function revokeSession(id: string): Promise<void> {\n await db.update(sessions).set({ revokedAt: new Date() }).where(eq(sessions.id, id))\n sessionCache.delete(id)\n }\n\n async function revokeByUser(userSub: string): Promise<void> {\n await db\n .update(sessions)\n .set({ revokedAt: new Date() })\n .where(and(eq(sessions.userSub, userSub), isNull(sessions.revokedAt)))\n // Revoking every session for a user could invalidate several cache\n // entries; clearing the whole cache is simpler than per-user bookkeeping\n // and this path isn't hot enough to justify it.\n sessionCache.clear()\n }\n\n /** Run fn while holding a `SELECT … FOR UPDATE` row lock on the session.\n * Serializes refresh per session: concurrent callers block on the lock,\n * then see the winner's rotated tokens in `locked.session`. The KC refresh\n * call deliberately happens inside the transaction — that's the point. */\n async function withSessionLock<T>(\n id: string,\n fn: (locked: LockedSession) => Promise<T>,\n ): Promise<T> {\n return db.transaction(async (tx) => {\n const [row] = await tx\n .select()\n .from(sessions)\n .where(and(eq(sessions.id, id), isNull(sessions.revokedAt)))\n .for(\"update\")\n .limit(1)\n const session = row ? await rowToSession(row) : null\n return fn({\n session,\n rotate: async (t: RotateInput) => {\n const { ciphertext, iv } = await encrypt(cipher, t.refreshToken)\n await tx\n .update(sessions)\n .set({\n accessToken: t.accessToken,\n accessExpiresAt: t.accessExpiresAt,\n ...(t.idToken !== undefined ? { idToken: t.idToken } : {}),\n refreshCt: ciphertext,\n refreshIv: iv,\n refreshExpiresAt: t.refreshExpiresAt,\n updatedAt: new Date(),\n })\n .where(eq(sessions.id, id))\n // This bypasses rotateTokens() (runs inside the tx), so invalidate\n // the read cache here too — otherwise a subsequent getSession()\n // within the TTL would serve the pre-rotation row.\n sessionCache.delete(id)\n },\n revoke: async () => {\n await tx.update(sessions).set({ revokedAt: new Date() }).where(eq(sessions.id, id))\n sessionCache.delete(id)\n },\n })\n })\n }\n\n return {\n createSession,\n getSession,\n getPendingSession,\n getIdTokenHint,\n confirmSession,\n rotateTokens,\n revokeSession,\n revokeByUser,\n withSessionLock,\n }\n}\n","import {\n boolean,\n customType,\n index,\n pgSchema,\n text,\n timestamp,\n uuid,\n varchar,\n} from \"drizzle-orm/pg-core\"\n\nexport const authSchema = pgSchema(\"auth\")\n\nconst bytea = customType<{ data: Uint8Array; driverData: Buffer }>({\n dataType: () => \"bytea\",\n toDriver: (v) => Buffer.from(v),\n fromDriver: (v) => new Uint8Array(v),\n})\n\nexport const sessions = authSchema.table(\n \"sessions\",\n {\n id: uuid(\"id\").primaryKey().defaultRandom(),\n userSub: varchar(\"user_sub\", { length: 64 }).notNull(),\n userEmail: varchar(\"user_email\", { length: 320 }),\n organizationId: varchar(\"organization_id\", { length: 64 }),\n organizationAlias: varchar(\"organization_alias\", { length: 255 }),\n accessToken: text(\"access_token\").notNull(),\n accessExpiresAt: timestamp(\"access_expires_at\", { withTimezone: true }).notNull(),\n idToken: text(\"id_token\"),\n refreshCt: bytea(\"refresh_ct\").notNull(),\n refreshIv: bytea(\"refresh_iv\").notNull(),\n refreshExpiresAt: timestamp(\"refresh_expires_at\", { withTimezone: true }).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull().defaultNow(),\n revokedAt: timestamp(\"revoked_at\", { withTimezone: true }),\n pendingIdentityConfirm: boolean(\"pending_identity_confirm\").notNull().default(false),\n },\n (t) => ({\n userSubIdx: index(\"ix_auth_sessions_user_sub\").on(t.userSub),\n }),\n)\n\nexport const loginState = authSchema.table(\"login_state\", {\n state: varchar(\"state\", { length: 128 }).primaryKey(),\n codeVerifier: varchar(\"code_verifier\", { length: 255 }).notNull(),\n redirectPath: text(\"redirect_path\").notNull().default(\"/\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true }).notNull(),\n forcedReauth: boolean(\"forced_reauth\").notNull().default(false),\n nonce: varchar(\"nonce\", { length: 128 }),\n // Set by handleLogin when its own request carries retried=1 (our restart\n // hitting /api/auth/login). handleCallback reads it back to bound the\n // one-and-only restart on Keycloak's explicit \"start over\" signals — see\n // MIGRATION_0005_RETRIED for why this lives on the row and not on the KC\n // callback URL.\n retried: boolean(\"retried\").notNull().default(false),\n // Whom this login was supposed to end up as: the subject of the session\n // being replaced and/or the address the user typed. The callback compares\n // Keycloak's answer against these instead of adopting whatever comes back.\n expectedSub: varchar(\"expected_sub\", { length: 64 }),\n expectedEmail: varchar(\"expected_email\", { length: 320 }),\n})\n","import { webcrypto } from \"node:crypto\"\n\n/** Points at a session row that is waiting for a human to say whose it is.\n *\n * Signed and HttpOnly, so the browser can neither read nor forge it: the\n * session id it carries is the only way to reach a pending row, and a caller\n * who could name an arbitrary id could confirm somebody else's pending login.\n *\n * It also carries the expected identity, because the `login_state` row is\n * deleted on every exit from the callback — by the time the interstitial asks\n * \"who did you expect?\", the database no longer knows.\n */\nexport const HANDOFF_COOKIE_NAME = \"qdc_identity_handoff\"\n\n/** Five minutes: long enough to read two sentences and click, short enough that\n * an abandoned mismatch stops being an option rather than lingering. */\nconst TTL_SEC = 300\n\nconst SUBTLE = webcrypto.subtle\n\nexport type IdentityHandoff = {\n sessionId: string\n expectedSub: string | null\n expectedEmail: string | null\n actualEmail: string | null\n exp: number\n}\n\n/** Distinct HKDF info from the refresh-token cipher (`qdc-sdk:refresh-token:v1`)\n * and from the session cookie's MAC, so this key cannot be substituted for\n * either. Same secret, different purpose, different key. */\nconst HKDF_INFO = \"qdc-sdk:identity-handoff:v1\"\n\nasync function macKey(hexSecret: string): Promise<CryptoKey> {\n const raw = Buffer.from(hexSecret, \"hex\")\n const baseKey = await SUBTLE.importKey(\"raw\", raw, \"HKDF\", false, [\"deriveKey\"])\n return SUBTLE.deriveKey(\n {\n name: \"HKDF\",\n hash: \"SHA-256\",\n salt: new Uint8Array(16),\n info: new TextEncoder().encode(HKDF_INFO),\n },\n baseKey,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\", \"verify\"],\n )\n}\n\nfunction attributes(maxAge: number): string {\n return `Path=/api/auth; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`\n}\n\nexport async function createIdentityHandoff(opts: { secret: string }) {\n if (opts.secret.length < 32) throw new Error(\"session cookie secret too short\")\n const key = await macKey(opts.secret)\n\n async function issue(payload: Omit<IdentityHandoff, \"exp\">): Promise<string> {\n const full: IdentityHandoff = {\n ...payload,\n exp: Math.floor(Date.now() / 1000) + TTL_SEC,\n }\n const body = Buffer.from(JSON.stringify(full)).toString(\"base64url\")\n const mac = await SUBTLE.sign(\"HMAC\", key, new TextEncoder().encode(body))\n const value = `${body}.${Buffer.from(new Uint8Array(mac)).toString(\"base64url\")}`\n return `${HANDOFF_COOKIE_NAME}=${value}; ${attributes(TTL_SEC)}`\n }\n\n async function read(\n cookieHeader: string | null | undefined,\n ): Promise<IdentityHandoff | null> {\n if (!cookieHeader) return null\n let value: string | null = null\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${HANDOFF_COOKIE_NAME}=`)) {\n value = trimmed.slice(HANDOFF_COOKIE_NAME.length + 1)\n break\n }\n }\n if (!value) return null\n const parts = value.split(\".\")\n if (parts.length !== 2) return null\n const [body, mac] = parts as [string, string]\n let ok: boolean\n try {\n ok = await SUBTLE.verify(\n \"HMAC\",\n key,\n new Uint8Array(Buffer.from(mac, \"base64url\")),\n new TextEncoder().encode(body),\n )\n } catch {\n return null\n }\n if (!ok) return null\n let parsed: IdentityHandoff\n try {\n parsed = JSON.parse(Buffer.from(body, \"base64url\").toString(\"utf8\")) as IdentityHandoff\n } catch {\n return null\n }\n if (typeof parsed.exp !== \"number\" || parsed.exp < Math.floor(Date.now() / 1000)) {\n return null\n }\n if (typeof parsed.sessionId !== \"string\" || !parsed.sessionId) return null\n return parsed\n }\n\n function clear(): string {\n return `${HANDOFF_COOKIE_NAME}=; ${attributes(0)}`\n }\n\n return { issue, read, clear }\n}\n","export type OrganizationClaim = { id?: string; alias?: string }\n\n/**\n * Normalize Keycloak's `organization` token claim into `{ id, alias }`.\n *\n * Keycloak emits this claim in several shapes depending on the realm's\n * organization-mapper configuration:\n *\n * - **multivalued array of aliases** — `[\"qdc-dev\"]` (the qdc realm's current\n * config). Only the alias is available; there is no id in the token.\n * - **map keyed by alias** — `{ \"qdc-dev\": { id: \"<uuid>\" } }`.\n * - **explicit object** — `{ id, alias }`.\n *\n * Anything else (missing, empty, a bare string, malformed) yields `{}`.\n * The first organization wins when several are present.\n */\nexport function parseOrganizationClaim(raw: unknown): OrganizationClaim {\n if (!raw) return {}\n\n // Array of alias strings: [\"qdc-dev\", ...]\n if (Array.isArray(raw)) {\n const alias = raw.find((x): x is string => typeof x === \"string\" && x.length > 0)\n return alias ? { alias, id: undefined } : {}\n }\n\n if (typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>\n\n // Explicit { id, alias } object.\n if (typeof obj.alias === \"string\" || typeof obj.id === \"string\") {\n return {\n id: typeof obj.id === \"string\" ? obj.id : undefined,\n alias: typeof obj.alias === \"string\" ? obj.alias : undefined,\n }\n }\n\n // Map keyed by alias: { \"<alias>\": { id, ... } }\n const alias = Object.keys(obj)[0]\n if (alias) {\n const value = obj[alias]\n const id =\n value && typeof value === \"object\" && typeof (value as { id?: unknown }).id === \"string\"\n ? ((value as { id: string }).id)\n : undefined\n return { alias, id }\n }\n }\n\n return {}\n}\n\n/** True when the claim names the given alias, across every shape Keycloak\n * emits (array of aliases, map keyed by alias, explicit {id, alias}). */\nexport function organizationClaimHasAlias(raw: unknown, alias: string): boolean {\n if (!raw) return false\n if (Array.isArray(raw)) return raw.includes(alias)\n if (typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>\n if (typeof obj.alias === \"string\") return obj.alias === alias\n return alias in obj\n }\n return false\n}\n","import { webcrypto } from \"node:crypto\"\n\nexport function randomB64Url(bytes: number): string {\n const buf = new Uint8Array(bytes)\n webcrypto.getRandomValues(buf)\n return Buffer.from(buf).toString(\"base64url\")\n}\n\nexport async function sha256B64Url(input: string): Promise<string> {\n const data = new TextEncoder().encode(input)\n const digest = await webcrypto.subtle.digest(\"SHA-256\", data)\n return Buffer.from(new Uint8Array(digest)).toString(\"base64url\")\n}\n\n// The three sanitizers moved to `src/auth-core/sanitize.ts`: this module imports\n// `node:crypto` and uses `Buffer`, so nothing in it can be imported from an edge\n// proxy — and `safeRedirect` is precisely what a proxy wants. These aliases keep\n// the existing call sites in createAuthRouteHandlers.ts unchanged.\nexport {\n safeRedirect as sanitizeRedirect,\n safeLoginHint as sanitizeLoginHint,\n safeIdpHint as sanitizeIdpHint,\n} from \"../../auth-core/sanitize\"\n","// Input sanitizers for values that come off a URL and go back out as a\n// redirect target or an upstream query parameter.\n//\n// This module is deliberately pure: no `node:crypto`, no `Buffer`, no `next`.\n// It lives in auth-core rather than next/auth because a Next proxy runs on the\n// edge runtime, where neither of those exists — and `safeRedirect` is exactly\n// the helper a proxy wants.\n\n// Control characters (incl. CR/LF/tab) to strip before validating a redirect\n// or login hint — defence in depth against header/URL injection and stray\n// whitespace, and against the WHATWG URL parser's own tab/CR/LF stripping\n// happening after our check instead of before it.\n// eslint-disable-next-line no-control-regex -- matching control characters is the point of this regex\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f]/g\n\n// Same-origin relative path only. A prefix check on the raw string (\"starts\n// with / but not //\") is not enough: a value like \"/\\evil.com\" also starts\n// with a single \"/\", but the WHATWG URL parser (the same algorithm browsers\n// use) normalises a backslash right after the leading slash into a second\n// slash for special schemes, turning it protocol-relative. A raw tab between\n// the slashes (\"/\\t/evil.com\") collapses into \"//evil.com\" the same way once\n// the parser strips whitespace. Since this runs server-side on the login\n// path of every tenant app, resolve the candidate against a throwaway\n// absolute base and require the parsed result's origin to match the base's\n// origin, rather than trusting the raw string.\n//\n// Returns null — not a fallback — so the caller decides what \"unsafe\" means.\nfunction normalizePath(raw: string | null | undefined): string | null {\n if (!raw) return null\n const cleaned = raw.replace(CONTROL_CHARS, \"\")\n if (!cleaned.startsWith(\"/\") || cleaned.startsWith(\"//\")) return null\n try {\n const base = new URL(\"https://sanitize.invalid\")\n const resolved = new URL(cleaned, base)\n if (resolved.origin !== base.origin) return null\n return resolved.pathname + resolved.search + resolved.hash\n } catch {\n return null\n }\n}\n\n/**\n * Reduce a caller-supplied redirect target to a same-origin relative path.\n *\n * Anything absolute, protocol-relative, or unparseable resolves to `fallback`.\n * The fallback is validated by the same rule, so passing a tainted value there\n * cannot be used to smuggle an open redirect through the back door; a fallback\n * that fails too yields `\"/\"`.\n */\nexport function safeRedirect(candidate: string | null | undefined, fallback = \"/\"): string {\n return normalizePath(candidate) ?? normalizePath(fallback) ?? \"/\"\n}\n\nconst LOGIN_HINT_MAX = 320 // RFC 5321 max email length\nconst IDP_HINT_MAX = 255\nconst IDP_ALIAS = /^[A-Za-z0-9._-]+$/\n\n/**\n * Sanitize an OIDC `login_hint`.\n *\n * The hint is a login identifier — email OR username — so this strips control\n * characters and caps the length but deliberately does not enforce an email\n * format, which would break username logins.\n */\nexport function safeLoginHint(raw: string | null | undefined): string | undefined {\n if (!raw) return undefined\n const cleaned = raw.replace(CONTROL_CHARS, \"\").trim().slice(0, LOGIN_HINT_MAX)\n return cleaned.length > 0 ? cleaned : undefined\n}\n\n/**\n * Sanitize a Keycloak `kc_idp_hint` (an IdP alias).\n *\n * Rejects anything outside the alias character set rather than mangling it —\n * a half-valid alias would select the wrong identity provider silently.\n */\nexport function safeIdpHint(raw: string | null | undefined): string | undefined {\n if (!raw) return undefined\n const cleaned = raw.trim()\n if (cleaned.length === 0 || cleaned.length > IDP_HINT_MAX) return undefined\n return IDP_ALIAS.test(cleaned) ? cleaned : undefined\n}\n","/** Binds an in-flight authorization request to the user agent that started it.\n *\n * RFC 9700 §2.1.1 requires the PKCE challenge (or nonce) to be \"securely bound\n * to the client and the user agent in which the transaction was started\". Ours\n * was not: `state` and `code_verifier` lived only in `auth.login_state`, so any\n * browser presenting a `state` from a URL could complete someone else's login\n * and receive their session cookie. This cookie is the missing half.\n *\n * `SameSite=Lax`, never `Strict`: the callback arrives as a cross-site\n * top-level navigation from Keycloak, which `Strict` would withhold — breaking\n * every login. Same reasoning as `gh_link_state` in portal-web.\n *\n * Up to three states are carried, newest first, so starting a login in a second\n * tab does not silently invalidate the first.\n */\nexport const LOGIN_COOKIE_NAME = \"qdc_login\"\nexport const MAX_BOUND_STATES = 3\nconst MAX_AGE_SEC = 600\n\nfunction attributes(maxAge: number): string {\n return `Path=/api/auth; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`\n}\n\nexport function readBoundStates(cookieHeader: string | null | undefined): string[] {\n if (!cookieHeader) return []\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim()\n if (!trimmed.startsWith(`${LOGIN_COOKIE_NAME}=`)) continue\n return trimmed\n .slice(LOGIN_COOKIE_NAME.length + 1)\n .split(\" \")\n .map((s) => s.trim())\n .filter(Boolean)\n }\n return []\n}\n\nexport function appendBoundState(\n cookieHeader: string | null | undefined,\n state: string,\n): string {\n const next = [state, ...readBoundStates(cookieHeader).filter((s) => s !== state)]\n .slice(0, MAX_BOUND_STATES)\n .join(\" \")\n return `${LOGIN_COOKIE_NAME}=${next}; ${attributes(MAX_AGE_SEC)}`\n}\n\nexport function removeBoundState(\n cookieHeader: string | null | undefined,\n state: string,\n): string {\n const next = readBoundStates(cookieHeader).filter((s) => s !== state)\n if (next.length === 0) return `${LOGIN_COOKIE_NAME}=; ${attributes(0)}`\n return `${LOGIN_COOKIE_NAME}=${next.join(\" \")}; ${attributes(MAX_AGE_SEC)}`\n}\n","import { decodeJwt } from \"jose\"\nimport { KcTokenError, type KcClient } from \"../../auth-core/kcClient\"\nimport type { RefreshStore, Session } from \"../../auth-core/refreshStore\"\nimport { organizationClaimHasAlias } from \"../../auth-core/organizationClaim\"\nimport { AuthError } from \"./requireAuth\"\n\n/** Only a definitive token rejection may kill the session. Anything else\n * (network errors, KC 5xx, invalid_client misconfig) is transient: revoking\n * on those is exactly what caused the spurious logouts. */\nfunction isDefinitiveRefreshFailure(err: unknown): boolean {\n return err instanceof KcTokenError && err.status === 400 && err.oauthError === \"invalid_grant\"\n}\n\nexport async function refreshWithinSkew(\n opts: {\n refreshStore: RefreshStore\n kcClient: KcClient\n skewSec: number\n orgAlias?: string\n idleTimeoutSec?: number\n absoluteMaxSec?: number\n },\n id: string,\n session: Session,\n): Promise<Session> {\n // Both bounds are checked before the skew shortcut: a session past its\n // deadline must die on the next request, not on the next token refresh.\n //\n // `updatedAt` moves on every token rotation, so \"idle\" here means \"no request\n // needed a refresh\", which within the 10-minute access-token lifespan is a\n // faithful proxy for activity. A tab that keeps polling is genuinely active.\n const idleSec = opts.idleTimeoutSec\n const absoluteSec = opts.absoluteMaxSec\n const tooIdle =\n idleSec !== undefined && Date.now() - session.updatedAt.getTime() > idleSec * 1000\n const tooOld =\n absoluteSec !== undefined && Date.now() - session.createdAt.getTime() > absoluteSec * 1000\n if (tooIdle || tooOld) {\n await opts.refreshStore.revokeSession(id)\n throw new AuthError(\"session_expired\")\n }\n\n const skewMs = opts.skewSec * 1000\n if (session.accessExpiresAt.getTime() - Date.now() >= skewMs) {\n return session\n }\n\n type Outcome =\n | { kind: \"ok\"; session: Session }\n | { kind: \"dead\" }\n | { kind: \"transient\" }\n\n const result: Outcome = await opts.refreshStore.withSessionLock(id, async (locked) => {\n if (!locked.session) return { kind: \"dead\" as const }\n // A concurrent request may have rotated while we waited for the lock —\n // single-flight: use its tokens instead of burning ours.\n if (locked.session.accessExpiresAt.getTime() - Date.now() >= skewMs) {\n return { kind: \"ok\" as const, session: locked.session }\n }\n try {\n const tokens = await opts.kcClient.refresh(locked.session.refreshToken)\n if (opts.orgAlias) {\n const claims = decodeJwt(tokens.accessToken) as { organization?: unknown }\n if (!organizationClaimHasAlias(claims.organization, opts.orgAlias)) {\n // Definitive: the user is no longer a member of this app's\n // organization — kill the session, don't persist the new tokens.\n await locked.revoke()\n return { kind: \"dead\" as const }\n }\n }\n const rotated = {\n accessToken: tokens.accessToken,\n accessExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),\n idToken: tokens.idToken,\n refreshToken: tokens.refreshToken,\n refreshExpiresAt: new Date(Date.now() + tokens.refreshExpiresIn * 1000),\n }\n await locked.rotate(rotated)\n return { kind: \"ok\" as const, session: { ...locked.session, ...rotated } }\n } catch (err) {\n if (isDefinitiveRefreshFailure(err)) {\n await locked.revoke()\n return { kind: \"dead\" as const }\n }\n return { kind: \"transient\" as const }\n }\n })\n\n if (result.kind === \"ok\") return result.session\n if (result.kind === \"dead\") throw new AuthError(\"session_expired\")\n // Transient KC failure: keep serving while the access token is still valid;\n // the next request retries the refresh. Only fail once it actually expired.\n if (session.accessExpiresAt.getTime() > Date.now()) return session\n throw new AuthError(\"refresh_unavailable\")\n}\n","import { cookies } from \"next/headers\"\nimport { decodeJwt } from \"jose\"\nimport { COOKIE_NAME } from \"../../auth-core/session\"\nimport type { KcClient } from \"../../auth-core/kcClient\"\nimport type { RefreshStore } from \"../../auth-core/refreshStore\"\nimport { refreshWithinSkew } from \"./refreshWithinSkew\"\nimport { rolesForClient, type ClaimsWithAccess } from \"./appAccess\"\nimport { setUserContext } from \"../../telemetry-hooks\"\n\ntype Cookies = {\n read: (cookieHeader: string | null | undefined) => Promise<string | null>\n}\n\nexport type AuthUser = {\n id: string\n email: string | null\n organization: { id: string | null; alias: string | null }\n}\n\nexport class AuthError extends Error {\n constructor(\n public reason: \"unauthenticated\" | \"session_expired\" | \"no_app_access\" | \"refresh_unavailable\",\n ) {\n super(reason)\n this.name = \"AuthError\"\n }\n}\n\nexport function requireAuth(opts: {\n cookies: Cookies\n refreshStore: RefreshStore\n kcClient: KcClient\n refreshSkewSec?: number\n clientId?: string\n orgAlias?: string\n /** Same two bounds `/me` enforces. A guard that enforced a weaker policy\n * would silently become the session's real lifetime. */\n idleTimeoutSec?: number\n absoluteMaxSec?: number\n}): () => Promise<AuthUser> {\n const skewSec = opts.refreshSkewSec ?? 30\n return async () => {\n const jar = await cookies()\n const raw = jar.get(COOKIE_NAME)?.value\n const id = await opts.cookies.read(raw ? `${COOKIE_NAME}=${raw}` : null)\n if (!id) throw new AuthError(\"unauthenticated\")\n\n let session = await opts.refreshStore.getSession(id)\n if (!session) throw new AuthError(\"unauthenticated\")\n\n session = await refreshWithinSkew(\n {\n refreshStore: opts.refreshStore,\n kcClient: opts.kcClient,\n skewSec,\n orgAlias: opts.orgAlias,\n idleTimeoutSec: opts.idleTimeoutSec,\n absoluteMaxSec: opts.absoluteMaxSec,\n },\n id,\n session,\n )\n\n if (opts.clientId) {\n const claims = decodeJwt(session.accessToken) as ClaimsWithAccess\n if (rolesForClient(claims, opts.clientId).length === 0) {\n throw new AuthError(\"no_app_access\")\n }\n }\n\n setUserContext(session.userSub)\n return {\n id: session.userSub,\n email: session.userEmail,\n organization: { id: session.organizationId, alias: session.organizationAlias },\n }\n }\n}\n","export type ClaimsWithAccess = { resource_access?: Record<string, { roles?: string[] }> }\n\n/** Roles the token carries for a specific app client (its own audience). */\nexport function rolesForClient(claims: ClaimsWithAccess, clientId: string): string[] {\n return claims.resource_access?.[clientId]?.roles ?? []\n}\n","// Pure helpers for reading roles out of a Keycloak access token. No `next` or\n// React imports — this is auth-core shared logic, imported by both sides.\n\nexport type JwtPayload = {\n realm_access?: { roles?: string[] }\n resource_access?: Record<string, { roles?: string[] }>\n}\n\n// Roles Keycloak adds to every token; not meaningful as app roles.\nconst KC_DEFAULT_ROLES = new Set([\"offline_access\", \"uma_authorization\"])\n\n// Decode the payload (middle) segment of a JWT. Signature is NOT verified — the\n// token is read from our own trusted session store, never from the client.\nexport function decodeJwtPayload(token: string): JwtPayload {\n const segment = token.split(\".\")[1]\n if (!segment) throw new Error(\"Malformed JWT: missing payload segment\")\n const json = Buffer.from(segment, \"base64url\").toString(\"utf8\")\n return JSON.parse(json) as JwtPayload\n}\n\n// Merge realm + client roles, drop Keycloak defaults, dedupe.\nexport function extractRoles(payload: JwtPayload, clientId: string | undefined): string[] {\n const realm = payload.realm_access?.roles ?? []\n const client = clientId ? (payload.resource_access?.[clientId]?.roles ?? []) : []\n const meaningful = [...realm, ...client].filter(\n (role) => !KC_DEFAULT_ROLES.has(role) && !role.startsWith(\"default-roles-\"),\n )\n return [...new Set(meaningful)]\n}\n","// Pure permission model. No `next`/React imports. A permission map binds each\n// role name to the permissions that role grants. The map lives in the app; the\n// SDK only provides the shape, the type helper, and the resolver.\n\nexport type PermissionMap = Record<string, readonly string[]>\n\n// The union of every permission string in the map, e.g. \"app:rename\" | \"profile:edit\".\nexport type PermissionOf<M extends PermissionMap> = M[keyof M][number] & string\n\n// Identity at runtime; exists to capture literal types via the `const` type\n// parameter so `PermissionOf<typeof map>` yields a precise string-literal union.\nexport function definePermissions<const M extends PermissionMap>(map: M): M {\n return map\n}\n\n// Resolve a user's roles to the set of permissions they hold. Roles not present\n// in the map contribute nothing (fail-closed). Result is deduped.\nexport function permissionsForRoles(map: PermissionMap, roles: string[]): string[] {\n const set = new Set<string>()\n for (const role of roles) {\n for (const perm of map[role] ?? []) set.add(perm)\n }\n return [...set]\n}\n","import type { NodePgDatabase } from \"drizzle-orm/node-postgres\"\nimport type { PermissionMap } from \"../../auth-core/permissions\"\n\nexport type NextAuthOptions = {\n keycloakUrl: string\n realm: string\n clientId: string\n clientSecret: string\n orgAlias?: string\n /** \"auto\" (default): request `organization:<orgAlias>` when set, else the\n * bare `organization` scope. \"none\": never request it — identity-first\n * consumers (the portal) authorize relationally and must not trigger\n * Keycloak's org-selection prompt for multi-org users. */\n organizationScope?: \"auto\" | \"none\"\n publicOrigin: string\n sessionCookieSecret: string\n db: NodePgDatabase\n /** Enforce per-app client-role default-deny at the BFF. Default true.\n * Set false for identity-only consumers (e.g. the multi-tenant portal),\n * which authorize relationally at their own API. */\n requireAppRole?: boolean\n /** Role→permission map for this app. When set, `/api/auth/me` returns the\n * caller's resolved permissions and the permission guards resolve against it.\n * Omitted by identity-only consumers (the portal). */\n permissions?: PermissionMap\n /** Where to send the browser when Keycloak returns an identity the login did\n * not expect. The SDK ships no UI: the default lands on the app's own login\n * page with a reason, which is correct for a generated tenant app. The portal\n * points this at its two-button interstitial. */\n identityMismatchPath?: string\n /** Session dies after this long without a token refresh. Default 8h. */\n sessionIdleTimeoutSec?: number\n /** Hard ceiling no activity can extend. Default 12h. */\n sessionAbsoluteMaxSec?: number\n}\n\n/** Session dies after 8h without a token refresh unless the app says otherwise. */\nexport const DEFAULT_SESSION_IDLE_TIMEOUT_SEC = 28800\n/** Hard ceiling no activity can extend: 12h. */\nexport const DEFAULT_SESSION_ABSOLUTE_MAX_SEC = 43200\n","import { SpanStatusCode, trace } from \"@opentelemetry/api\"\n\n/** Auth-flow spans need failure semantics observe() can't express: a failed\n * login RETURNS a 4xx/redirect instead of throwing, and observe() stamps OK\n * unconditionally after fn returns. This wrapper lets the handler mark the\n * span failed (with a reason) while still returning its response. Internal to\n * next/auth — the public observe() contract stays untouched. */\nexport type AuthSpanController = {\n set(key: string, value: string): void\n fail(reason: string): void\n}\n\nexport function runAuthSpan<T>(\n label: string,\n attributes: Record<string, string>,\n fn: (auth: AuthSpanController) => Promise<T>,\n): Promise<T> {\n return trace.getTracer(\"@quatronic/sdk\").startActiveSpan(label, async (span) => {\n span.setAttributes(attributes)\n span.setAttribute(\"qore.span.label\", label)\n span.setAttribute(\"qore.span.type\", \"task\")\n let failed = false\n const auth: AuthSpanController = {\n set: (key, value) => void span.setAttribute(key, value),\n fail: (reason) => {\n failed = true\n span.setAttribute(\"qore.auth.failure_reason\", reason)\n span.setStatus({ code: SpanStatusCode.ERROR, message: reason })\n },\n }\n try {\n const result = await fn(auth)\n if (!failed) span.setStatus({ code: SpanStatusCode.OK })\n return result\n } catch (err) {\n span.recordException(err as Error)\n span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error)?.message })\n throw err\n } finally {\n span.end()\n }\n })\n}\n","import { createCipher } from \"../../auth-core/crypto\"\nimport { sessionCookie } from \"../../auth-core/session\"\nimport { createKcClient } from \"../../auth-core/kcClient\"\nimport { createRefreshStore } from \"../../auth-core/refreshStore\"\nimport { requireAuth, type AuthUser } from \"./requireAuth\"\nimport {\n DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n type NextAuthOptions,\n} from \"./types\"\n\nexport async function requireAuthFor(\n opts: NextAuthOptions,\n): Promise<() => Promise<AuthUser>> {\n const cipher = await createCipher(opts.sessionCookieSecret)\n const cookies = sessionCookie({ secret: opts.sessionCookieSecret })\n const refreshStore = createRefreshStore({ db: opts.db, cipher })\n const kcClient = createKcClient({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n clientId: opts.clientId,\n clientSecret: opts.clientSecret,\n publicOrigin: opts.publicOrigin,\n orgAlias: opts.orgAlias,\n organizationScope: opts.organizationScope,\n })\n const clientId = opts.requireAppRole === false ? undefined : opts.clientId\n return requireAuth({\n cookies,\n refreshStore,\n kcClient,\n clientId,\n orgAlias: opts.orgAlias,\n idleTimeoutSec: opts.sessionIdleTimeoutSec ?? DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n absoluteMaxSec: opts.sessionAbsoluteMaxSec ?? DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n })\n}\n","import { cookies } from \"next/headers\"\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\"\nimport { COOKIE_NAME, sessionCookie } from \"../../auth-core/session\"\nimport { createCipher } from \"../../auth-core/crypto\"\nimport { createKcClient } from \"../../auth-core/kcClient\"\nimport { createRefreshStore } from \"../../auth-core/refreshStore\"\nimport type { KcClient } from \"../../auth-core/kcClient\"\nimport type { RefreshStore } from \"../../auth-core/refreshStore\"\nimport { AuthError } from \"./requireAuth\"\nimport { refreshWithinSkew } from \"./refreshWithinSkew\"\nimport {\n DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n type NextAuthOptions,\n} from \"./types\"\n\ntype Cookies = {\n read: (cookieHeader: string | null | undefined) => Promise<string | null>\n}\n\ntype GetAccessTokenDeps = {\n cookies: Cookies\n refreshStore: RefreshStore\n kcClient: KcClient\n refreshSkewSec?: number\n orgAlias?: string\n /** Same two bounds `/me` enforces. A guard that enforced a weaker policy\n * would silently become the session's real lifetime. */\n idleTimeoutSec?: number\n absoluteMaxSec?: number\n}\n\nasync function resolveAccessToken(\n opts: GetAccessTokenDeps,\n): Promise<string | null> {\n const skewSec = opts.refreshSkewSec ?? 30\n const jar = await cookies()\n const raw = jar.get(COOKIE_NAME)?.value\n const id = await opts.cookies.read(raw ? `${COOKIE_NAME}=${raw}` : null)\n if (!id) return null\n\n let session = await opts.refreshStore.getSession(id)\n if (!session) return null\n\n session = await refreshWithinSkew(\n {\n refreshStore: opts.refreshStore,\n kcClient: opts.kcClient,\n skewSec,\n orgAlias: opts.orgAlias,\n idleTimeoutSec: opts.idleTimeoutSec,\n absoluteMaxSec: opts.absoluteMaxSec,\n },\n id,\n session,\n )\n\n return session.accessToken\n}\n\nexport function getAccessToken(\n opts: GetAccessTokenDeps,\n): () => Promise<string | null> {\n return () => resolveAccessToken(opts)\n}\n\nexport function requireAccessToken(\n opts: GetAccessTokenDeps,\n): () => Promise<string> {\n return async () => {\n const token = await resolveAccessToken(opts)\n if (token === null) throw new AuthError(\"unauthenticated\")\n return token\n }\n}\n\ntype BuildDepsResult = {\n cookies: Cookies\n refreshStore: RefreshStore\n kcClient: KcClient\n orgAlias?: string\n idleTimeoutSec: number\n absoluteMaxSec: number\n}\n\n// buildDeps constructs a cipher, a refresh-store (wraps the pg pool handed in\n// via opts.db — a long-lived pool, safe to share) and a kcClient (pure config\n// closure, no per-request state) every call. None of that needs redoing per\n// request for the same effective config, so memoize the built-deps promise\n// keyed on the non-function config fields buildDeps actually reads. `db` is\n// an object (not JSON-serializable/stable), so it keys an outer WeakMap —\n// entries fall out of the cache automatically when a db handle is GC'd.\nconst depsCache = new WeakMap<NodePgDatabase, Map<string, Promise<BuildDepsResult>>>()\n\nfunction depsCacheKey(opts: NextAuthOptions): string {\n return JSON.stringify({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n clientId: opts.clientId,\n clientSecret: opts.clientSecret,\n orgAlias: opts.orgAlias,\n organizationScope: opts.organizationScope,\n publicOrigin: opts.publicOrigin,\n sessionCookieSecret: opts.sessionCookieSecret,\n requireAppRole: opts.requireAppRole,\n permissions: opts.permissions,\n sessionIdleTimeoutSec: opts.sessionIdleTimeoutSec,\n sessionAbsoluteMaxSec: opts.sessionAbsoluteMaxSec,\n })\n}\n\nasync function buildDepsUncached(opts: NextAuthOptions): Promise<BuildDepsResult> {\n const cipher = await createCipher(opts.sessionCookieSecret)\n const cookies = sessionCookie({ secret: opts.sessionCookieSecret })\n const refreshStore = createRefreshStore({ db: opts.db, cipher })\n const kcClient = createKcClient({\n keycloakUrl: opts.keycloakUrl,\n realm: opts.realm,\n clientId: opts.clientId,\n clientSecret: opts.clientSecret,\n publicOrigin: opts.publicOrigin,\n orgAlias: opts.orgAlias,\n organizationScope: opts.organizationScope,\n })\n return {\n cookies,\n refreshStore,\n kcClient,\n orgAlias: opts.orgAlias,\n idleTimeoutSec: opts.sessionIdleTimeoutSec ?? DEFAULT_SESSION_IDLE_TIMEOUT_SEC,\n absoluteMaxSec: opts.sessionAbsoluteMaxSec ?? DEFAULT_SESSION_ABSOLUTE_MAX_SEC,\n }\n}\n\nasync function buildDeps(opts: NextAuthOptions): Promise<BuildDepsResult> {\n let byKey = depsCache.get(opts.db)\n if (!byKey) {\n byKey = new Map()\n depsCache.set(opts.db, byKey)\n }\n const key = depsCacheKey(opts)\n const cached = byKey.get(key)\n if (cached) return cached\n const promise = buildDepsUncached(opts)\n byKey.set(key, promise)\n return promise\n}\n\nexport async function getAccessTokenFor(\n opts: NextAuthOptions,\n): Promise<() => Promise<string | null>> {\n return getAccessToken(await buildDeps(opts))\n}\n\nexport async function requireAccessTokenFor(\n opts: NextAuthOptions,\n): Promise<() => Promise<string>> {\n return requireAccessToken(await buildDeps(opts))\n}\n","import { sql } from \"drizzle-orm\"\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\"\nimport { grantRuntimeDefaultPrivileges } from \"../db-core/migration-db\"\n\n// Migrations are inlined as string constants rather than read from .sql files\n// at runtime. A runtime `readFile` does not survive bundlers that only trace\n// JS — notably Next.js standalone output, where the .sql is omitted and the\n// read ENOENTs, crashing the instrumentation hook and 500-ing the whole app.\n// Inlining keeps the SQL in the bundled JS so it works in every consumer.\n\n// Split out of what used to be a single MIGRATION_0001_INIT on purpose, and\n// the reason generalises to every statement below it: **Postgres checks the\n// connecting role's privileges before it evaluates `IF NOT EXISTS`.** So an\n// already-applied statement is not a no-op for a role that lacks the right —\n// it is an error:\n//\n// - `CREATE SCHEMA IF NOT EXISTS auth` needs CREATE on the *database* →\n// `permission denied for database <db>` (SQLSTATE 42501)\n// - `CREATE INDEX IF NOT EXISTS` and `ALTER TABLE ... ADD COLUMN IF NOT\n// EXISTS` need *ownership* of the table → `must be owner of table\n// sessions` (also 42501)\n//\n// That is the normal shape of a least-privileged runtime role against an\n// already-provisioned database, and because migrations run in order one such\n// error aborts every statement after it. It has now bitten staging twice: the\n// first time `login_state.forced_reauth` never landed and every\n// `/api/auth/login` 500'd; the second time `0001_init:tables` failed on its\n// `CREATE INDEX` with `must be owner of table sessions`, again blocking every\n// later ALTER even though there was nothing at all to do.\n//\n// So each migration below carries an `isApplied` catalog probe and the runner\n// skips it when its effect is already present. Catalog lookups need nothing\n// beyond CONNECT, so a role with only DML inside an up-to-date `auth` schema\n// gets through cleanly, while a migration that genuinely still has work to do\n// is attempted and — if the role cannot do it — fails loudly, by name.\nconst MIGRATION_0001_SCHEMA = `\nCREATE SCHEMA IF NOT EXISTS auth;\n`\n\nconst MIGRATION_0001_TABLES = `\nCREATE TABLE IF NOT EXISTS auth.sessions (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n user_sub varchar(64) NOT NULL,\n user_email varchar(320),\n organization_id varchar(64),\n organization_alias varchar(255),\n access_token text NOT NULL,\n access_expires_at timestamptz NOT NULL,\n refresh_ct bytea NOT NULL,\n refresh_iv bytea NOT NULL,\n refresh_expires_at timestamptz NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n revoked_at timestamptz\n);\n\nCREATE INDEX IF NOT EXISTS ix_auth_sessions_user_sub\n ON auth.sessions(user_sub) WHERE revoked_at IS NULL;\n\nCREATE TABLE IF NOT EXISTS auth.login_state (\n state varchar(128) PRIMARY KEY,\n code_verifier varchar(255) NOT NULL,\n redirect_path text NOT NULL DEFAULT '/',\n created_at timestamptz NOT NULL DEFAULT now(),\n expires_at timestamptz NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS ix_auth_login_state_expires\n ON auth.login_state(expires_at);\n`\n\n// The KC ID token, so RP-initiated logout can pass it as `id_token_hint` —\n// without it, Keycloak's end-session endpoint falls back to showing a logout\n// confirmation page. Nullable: existing sessions predate this column until\n// their next login or token refresh backfills it.\nconst MIGRATION_0002_ID_TOKEN = `\nALTER TABLE auth.sessions ADD COLUMN IF NOT EXISTS id_token text;\n`\n\n// Records that this login asked Keycloak to re-authenticate (prompt=login +\n// max_age=0). The callback needs it to know whether the ID token's auth_time\n// MUST prove a fresh authentication (OIDC 3.1.2.1) — without the flag it cannot\n// tell a forced login from an ordinary SSO continuation.\nconst MIGRATION_0003_FORCED_REAUTH = `\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS forced_reauth boolean NOT NULL DEFAULT false;\n`\n\n// OIDC 3.1.3.7 (11): the nonce sent on the authorization request must be\n// checked against the ID token's claim. Nullable because rows written by an\n// older build have none; the callback treats a null nonce on the row as\n// \"nothing to check\" so an in-flight login survives the deploy.\nconst MIGRATION_0004_NONCE = `\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS nonce varchar(128);\n`\n\n// Bounds the one-and-only restart the callback grants on Keycloak's explicit\n// \"start over\" signals (authentication_expired / login_required). The\n// callback URL is Keycloak's fixed redirect_uri, so Keycloak never echoes our\n// own query parameters back to it — a guard read off the callback's query\n// string can never fire in production. The restarted login (our own request\n// to /api/auth/login?retried=1) stamps this column on the NEW row instead, so\n// the second callback can see it on the row rather than in a URL Keycloak\n// controls. NOT NULL DEFAULT false: every row, old or new, starts un-retried.\nconst MIGRATION_0005_RETRIED = `\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS retried boolean NOT NULL DEFAULT false;\n`\n\n// A session created for an identity that did not match what the login expected\n// exists, holds real tokens, and grants nothing until a human confirms it.\n//\n// DELIBERATELY `DEFAULT false` rather than a nullable column with a backfill:\n// these migrations re-run on every app start, so an `UPDATE ... WHERE\n// pending_identity_confirm IS NULL` would confirm every row still awaiting a\n// human on the next deploy — silently adopting exactly the identity this\n// mechanism exists to stop. A default means pre-existing rows are already\n// confirmed, which is correct: they were issued under the old rules.\nconst MIGRATION_0006_PENDING_CONFIRM = `\nALTER TABLE auth.sessions\n ADD COLUMN IF NOT EXISTS pending_identity_confirm boolean NOT NULL DEFAULT false;\n`\n\n// Who this login was supposed to end up as: the subject of the session being\n// replaced, and/or the address the user typed. The callback compares Keycloak's\n// answer against these instead of adopting whatever comes back. Nullable: a\n// clean first login on a fresh browser expects nobody in particular.\nconst MIGRATION_0007_EXPECTED_IDENTITY = `\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS expected_sub varchar(64);\nALTER TABLE auth.login_state ADD COLUMN IF NOT EXISTS expected_email varchar(320);\n`\n\ninterface Migration {\n // Named so a failure can say *which* statement broke — see the catch below.\n readonly name: string\n readonly sql: string\n // \"Is this migration's effect already present?\" Answered from the catalogs\n // only, so it never needs a privilege the migration itself would need. A\n // partially-applied migration must answer `false` (the SQL is written to be\n // idempotent, so re-running the applied half is harmless).\n readonly isApplied: (db: NodePgDatabase) => Promise<boolean>\n // A migration may only add to the schema: a prod rollback keeps the\n // database, and the previous release must still work on it\n // (scripts/migration-lint.mjs refuses anything else). `true` marks the rare\n // migration that removes or tightens something on purpose; ship it only once\n // no prod still runs a version that reads the old shape.\n readonly contract?: boolean\n}\n\n// All probes funnel through here: one boolean expression, evaluated by the\n// server. Every one of them reads `pg_catalog` directly rather than going\n// through `to_regclass`/`to_regnamespace` or `information_schema`, both of\n// which are privilege-sensitive in ways that defeat the purpose: `to_regclass`\n// resolves the name and so raises `permission denied for schema auth` for a\n// role without USAGE, and `information_schema.columns` silently hides columns\n// the role holds no privilege on — reporting an applied migration as missing\n// and sending us straight into the ALTER we were trying to avoid. The catalogs\n// themselves are world-readable.\nasync function probe(db: NodePgDatabase, expression: string): Promise<boolean> {\n const result = await db.execute<{ applied: boolean }>(\n sql.raw(`SELECT (${expression}) AS \"applied\"`),\n )\n return result.rows[0]?.applied === true\n}\n\nconst schemaExists = (schema: string) => (db: NodePgDatabase) =>\n probe(db, `EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = '${schema}')`)\n\n// Tables and indexes alike — both live in `pg_class`.\nconst relationsExist =\n (...relations: readonly (readonly [schema: string, name: string])[]) =>\n (db: NodePgDatabase) => {\n const tuples = relations.map(([schema, name]) => `('${schema}','${name}')`).join(\", \")\n return probe(\n db,\n `(SELECT count(*) FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE (n.nspname, c.relname) IN (${tuples})) = ${relations.length}`,\n )\n }\n\nconst columnsExist =\n (...columns: readonly (readonly [schema: string, table: string, column: string])[]) =>\n (db: NodePgDatabase) => {\n const tuples = columns\n .map(([schema, table, column]) => `('${schema}','${table}','${column}')`)\n .join(\", \")\n return probe(\n db,\n `(SELECT count(*) FROM pg_attribute a\n JOIN pg_class c ON c.oid = a.attrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE a.attnum > 0 AND NOT a.attisdropped\n AND (n.nspname, c.relname, a.attname) IN (${tuples})) = ${columns.length}`,\n )\n }\n\nconst MIGRATIONS: readonly Migration[] = [\n {\n name: \"0001_init:schema\",\n sql: MIGRATION_0001_SCHEMA,\n isApplied: schemaExists(\"auth\"),\n },\n {\n name: \"0001_init:tables\",\n sql: MIGRATION_0001_TABLES,\n isApplied: relationsExist(\n [\"auth\", \"sessions\"],\n [\"auth\", \"ix_auth_sessions_user_sub\"],\n [\"auth\", \"login_state\"],\n [\"auth\", \"ix_auth_login_state_expires\"],\n ),\n },\n {\n name: \"0002_id_token\",\n sql: MIGRATION_0002_ID_TOKEN,\n isApplied: columnsExist([\"auth\", \"sessions\", \"id_token\"]),\n },\n {\n name: \"0003_forced_reauth\",\n sql: MIGRATION_0003_FORCED_REAUTH,\n isApplied: columnsExist([\"auth\", \"login_state\", \"forced_reauth\"]),\n },\n {\n name: \"0004_nonce\",\n sql: MIGRATION_0004_NONCE,\n isApplied: columnsExist([\"auth\", \"login_state\", \"nonce\"]),\n },\n {\n name: \"0005_retried\",\n sql: MIGRATION_0005_RETRIED,\n isApplied: columnsExist([\"auth\", \"login_state\", \"retried\"]),\n },\n {\n name: \"0006_pending_confirm\",\n sql: MIGRATION_0006_PENDING_CONFIRM,\n isApplied: columnsExist([\"auth\", \"sessions\", \"pending_identity_confirm\"]),\n },\n {\n name: \"0007_expected_identity\",\n sql: MIGRATION_0007_EXPECTED_IDENTITY,\n isApplied: columnsExist(\n [\"auth\", \"login_state\", \"expected_sub\"],\n [\"auth\", \"login_state\", \"expected_email\"],\n ),\n },\n]\n\nexport interface RunAuthMigrationsOptions {\n // When provided, grants this role SELECT/INSERT/UPDATE/DELETE on every\n // table in the `auth` schema — both ones this call just created and ones a\n // future boot's migrations will create. Omit for a single-role setup (the\n // fallback state of an environment that hasn't opted into a separate\n // migration role yet); passing it is what makes it safe for `db` to\n // connect as an admin/owner role while the app itself never does.\n readonly runtimeRole?: string\n}\n\nexport async function runAuthMigrations(\n db: NodePgDatabase,\n opts: RunAuthMigrationsOptions = {},\n): Promise<void> {\n for (const migration of MIGRATIONS) {\n try {\n if (await migration.isApplied(db)) {\n // Already there — do not issue the statement at all. See the comment\n // above MIGRATION_0001_SCHEMA for why relying on `IF NOT EXISTS` to make\n // it a no-op is not safe under every role.\n continue\n }\n await db.execute(sql.raw(migration.sql))\n } catch (err) {\n // Re-thrown, not swallowed: the caller decides what to do with a failed\n // migration. Naming the statement is what turns a bare \"permission\n // denied for database x\" into something you can actually act on; the\n // `cause` keeps the original Postgres error (code, detail, etc.) intact.\n const message = err instanceof Error ? err.message : String(err)\n throw new Error(`auth migration \"${migration.name}\" failed: ${message}`, { cause: err })\n }\n }\n\n if (opts.runtimeRole) {\n await grantRuntimeDefaultPrivileges(db, \"auth\", opts.runtimeRole)\n }\n}\n","import { NextResponse } from \"next/server\"\nimport { requireAccessTokenFor } from \"./getAccessToken\"\nimport { requireAuthFor } from \"./requireAuthFor\"\nimport { AuthError, type AuthUser } from \"./requireAuth\"\nimport { extractRoles, decodeJwtPayload } from \"../../auth-core/roles\"\nimport { permissionsForRoles, type PermissionMap, type PermissionOf } from \"../../auth-core/permissions\"\nimport type { NextAuthOptions } from \"./types\"\n\nexport type PermissionGuards<M extends PermissionMap> = {\n can: (permission: PermissionOf<M>) => Promise<boolean>\n requirePermission: (permission: PermissionOf<M>) => Promise<AuthUser>\n withPermission: <A extends unknown[]>(\n permission: PermissionOf<M>,\n handler: (...args: A) => Promise<Response> | Response,\n ) => (...args: A) => Promise<Response>\n}\n\n// Dependency-injected core (no next/headers) — see permissionGuards.test.ts.\nexport function makeGuards<M extends PermissionMap>(\n map: M,\n getPermissions: () => Promise<string[]>,\n // requireUser defaults to throwing-unauthenticated; injected in real use.\n requireUser: () => Promise<AuthUser> = async () => {\n throw new AuthError(\"unauthenticated\")\n },\n): PermissionGuards<M> {\n const can = async (permission: PermissionOf<M>): Promise<boolean> =>\n (await getPermissions()).includes(permission)\n\n const requirePermission = async (permission: PermissionOf<M>): Promise<AuthUser> => {\n const user = await requireUser()\n if (!(await can(permission))) throw new AuthError(\"no_app_access\")\n return user\n }\n\n const withPermission = <A extends unknown[]>(\n permission: PermissionOf<M>,\n handler: (...args: A) => Promise<Response> | Response,\n ) =>\n async (...args: A): Promise<Response> => {\n if (!(await can(permission))) return new NextResponse(null, { status: 403 })\n return handler(...args)\n }\n\n return { can, requirePermission, withPermission }\n}\n\n/** Bind the guards once per app. `getOpts` is the app's `authOptions` thunk so\n * options (and env) are read lazily per request, never at module scope. */\nexport function createPermissionGuards<M extends PermissionMap>(\n map: M,\n getOpts: () => NextAuthOptions,\n): PermissionGuards<M> {\n const getPermissions = async (): Promise<string[]> => {\n const opts = getOpts()\n const requireAccessToken = await requireAccessTokenFor(opts)\n const token = await requireAccessToken()\n const roles = extractRoles(decodeJwtPayload(token), opts.clientId)\n return permissionsForRoles(map, roles)\n }\n const requireUser = async (): Promise<AuthUser> => {\n const requireAuth = await requireAuthFor(getOpts())\n return requireAuth()\n }\n return makeGuards(map, getPermissions, requireUser)\n}\n","import { timingSafeEqual } from \"node:crypto\";\n\ntype Handler = (req: Request) => Promise<Response> | Response;\n\nfunction expectedTokens(): string[] {\n return Object.entries(process.env)\n .filter(([k, v]) => /^CONN_.+_TOKEN$/.test(k) && !!v)\n .map(([, v]) => v as string);\n}\n\nfunction safeEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a);\n const bb = Buffer.from(b);\n if (ab.length !== bb.length) return false;\n return timingSafeEqual(ab, bb);\n}\n\nexport function withConnectionToken(handler: Handler): Handler {\n return async (req: Request): Promise<Response> => {\n const header =\n req.headers.get(\"authorization\")?.replace(/^Bearer\\s+/i, \"\") ??\n req.headers.get(\"x-qdc-connection-token\") ??\n \"\";\n const ok = !!header && expectedTokens().some((t) => safeEqual(header, t));\n if (!ok) return new Response(\"unauthorized\", { status: 401 });\n return handler(req);\n };\n}\n"],"mappings":"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","names":["eq","decodeJwt","webcrypto","SUBTLE","opts","body","webcrypto","SUBTLE","webcrypto","webcrypto","attributes","decodeJwt","decodeJwt","segment","attributes","cookies","decodeJwt","res","eq","bindingFailure","row","org","cookies","cookies","cookies","NextResponse","NextResponse","requireAccessToken","requireAuth"]}
|