@incorta/auth 1.9.2 → 1.9.3
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/index.cjs +13 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -260,9 +260,19 @@ function deleteCookie(name, options = {}) {
|
|
|
260
260
|
var import_jose = require("jose");
|
|
261
261
|
var endpointsCache = /* @__PURE__ */ new Map();
|
|
262
262
|
function toInternal(config, url) {
|
|
263
|
-
const internal = config.internalIncortaUrl;
|
|
264
|
-
if (!internal
|
|
265
|
-
|
|
263
|
+
const internal = config.internalIncortaUrl?.replace(/\/+$/, "");
|
|
264
|
+
if (!internal) return url;
|
|
265
|
+
let target;
|
|
266
|
+
let contextPath;
|
|
267
|
+
try {
|
|
268
|
+
target = new URL(url);
|
|
269
|
+
contextPath = new URL(config.incortaUrl).pathname.replace(/\/+$/, "");
|
|
270
|
+
} catch {
|
|
271
|
+
return url;
|
|
272
|
+
}
|
|
273
|
+
const path = target.pathname;
|
|
274
|
+
if (path !== contextPath && !path.startsWith(`${contextPath}/`)) return url;
|
|
275
|
+
return `${internal}${path.slice(contextPath.length)}${target.search}`;
|
|
266
276
|
}
|
|
267
277
|
function getEndpoints(config) {
|
|
268
278
|
const cacheKey = `${config.issuer}|${config.internalIncortaUrl ?? ""}`;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/core/config.ts","../src/core/handler.ts","../src/core/app-access.ts","../src/core/cookies.ts","../src/core/discovery.ts","../src/core/pkce.ts","../src/core/session.ts","../src/core/tokens.ts","../src/core/register.ts"],"sourcesContent":["import type { AppAccess } from \"./core/app-access.js\";\nimport { resolveConfig } from \"./core/config.js\";\nimport type { IncortaAuthConfig, ResolvedConfig } from \"./core/config.js\";\nimport { createHandler } from \"./core/handler.js\";\nimport type { AuthGate, AuthHandler, AuthSession } from \"./core/handler.js\";\n\nexport {\n AppAccessTokenRejectedError,\n AppAccessUnavailableError,\n EDIT_BIT,\n SHARE_BIT,\n VIEW_BIT,\n} from \"./core/app-access.js\";\nexport type { AppAccess, AppAccessPolicy } from \"./core/app-access.js\";\nexport type {\n IncortaAuthConfig,\n IncortaUser,\n SessionData,\n SessionInfo,\n} from \"./core/config.js\";\nexport type { AuthGate, AuthHandler, AuthSession } from \"./core/handler.js\";\nexport { IncortaAuthError } from \"./core/tokens.js\";\nexport {\n deleteClient,\n getClient,\n registerClient,\n updateClientRedirectUris,\n} from \"./core/register.js\";\nexport type {\n ClientInfo,\n ManageClientOptions,\n RegisterClientOptions,\n RegisteredClient,\n UpdateRedirectUrisOptions,\n} from \"./core/register.js\";\n\nexport interface IncortaAuth {\n /**\n * Web-standard request handler serving the auth routes under\n * `config.basePath` (default `/auth`): `/login`, `/callback`, `/session`,\n * `/logout` (+ `/token` when `exposeAccessToken` is on).\n *\n * Wire it to anything that speaks `Request`/`Response` — a TanStack Start\n * server route, Bun.serve, or via `@incorta/auth/node` / `@incorta/auth/express`\n * for Node servers.\n */\n handler: AuthHandler;\n /**\n * Reads the session from a web-standard `Request` (server-side gating, API\n * routes, server functions). Returns null when the user is not signed in.\n */\n getSession: (request: Request) => Promise<AuthSession | null>;\n /**\n * The whole-app gate: serve the returned `Response` when non-null (auth\n * routes, login redirects, access-denied/unavailable pages), continue to\n * your app on `null`. Enforces `config.appAccess` — `\"public\"` lets every\n * request through, `\"authenticated\"` requires a session, `\"catalog\"`\n * additionally requires at least VIEW access to this app in the Incorta\n * catalog (fails CLOSED with a 503 page when Incorta cannot answer).\n */\n gate: AuthGate;\n /**\n * The Incorta catalog's verdict about `session`'s user and THIS app —\n * registration, permission bits, and the gate's `allowed` outcome. Served\n * from a 30s per-user cache (the same one `gate` uses in `\"catalog\"`\n * mode). Throws `AppAccessUnavailableError` when Incorta cannot answer and\n * `AppAccessTokenRejectedError` when it rejects the session token. Useful\n * for permission-aware UX (e.g. hiding editor-only affordances) in any\n * `appAccess` mode.\n */\n getAppAccess: (session: AuthSession) => Promise<AppAccess>;\n /** The resolved configuration (secrets included — do not log). */\n config: ResolvedConfig;\n}\n\n/**\n * Creates an IncortaAuth instance: OAuth 2.0 authorization-code flow against\n * Incorta's built-in authorization server. Unauthenticated users are redirected\n * to the Incorta login page and returned to your app once signed in.\n */\nexport function createIncortaAuth(config: IncortaAuthConfig = {}): IncortaAuth {\n const resolved = resolveConfig(config);\n const { handler, getSession, gate, getAppAccess } = createHandler(resolved);\n return { handler, getSession, gate, getAppAccess, config: resolved };\n}\n","import type { JWTVerifyGetKey } from \"jose\";\nimport type { AppAccessPolicy } from \"./app-access.js\";\n\n/** The Incorta user identity carried by the session (claims from the ID token). */\nexport interface IncortaUser {\n /** Incorta login name (`sub` claim). */\n sub: string;\n name?: string;\n email?: string;\n /** Incorta role names, as minted into the ID token. */\n roles: string[];\n /** Tenant the user authenticated against. */\n tenant: string;\n}\n\n/** What the sealed session cookie holds. */\nexport interface SessionData {\n user: IncortaUser;\n /** Incorta-issued access token (RS256 JWT, aud = client_id). */\n accessToken: string;\n /** Epoch millis when the access token expires. */\n accessTokenExpiresAt: number;\n /** Rotating refresh token, present when the client has the refresh_token grant. */\n refreshToken?: string;\n /** Epoch millis when this session was first created (login time). */\n createdAt: number;\n}\n\nexport interface SessionInfo {\n user: IncortaUser;\n /** Epoch millis when the session cookie expires (sliding). */\n expiresAt: number;\n /** Epoch millis when the access token expires. */\n accessTokenExpiresAt: number;\n}\n\nexport interface IncortaAuthConfig {\n /**\n * Base URL of the Incorta web application, e.g. `https://acme.cloud.incorta.com/incorta`\n * or `http://localhost:8080/incorta`. Defaults to `INCORTA_URL`.\n */\n incortaUrl?: string;\n /**\n * Split-horizon override: base URL this SERVER should use to reach Incorta\n * when the public `incortaUrl` is not routable from where the app runs\n * (containers, cluster-internal DNS) — e.g.\n * `http://host.docker.internal:8080/incorta` from a local container. Only\n * server-to-server calls (discovery, token exchange, JWKS) use it; browser\n * redirects and token `iss` validation stay on the public `incortaUrl`.\n * Defaults to `INCORTA_INTERNAL_URL`; unset means no rewrite.\n */\n internalIncortaUrl?: string;\n /** Incorta tenant name, e.g. `demo`. Defaults to `INCORTA_TENANT`. */\n tenant?: string;\n /** OAuth client id (see `incorta-auth register`). Defaults to `INCORTA_CLIENT_ID`. */\n clientId?: string;\n /**\n * OAuth client secret — only for a CONFIDENTIAL client. Incorta's dynamic\n * registration issues app clients as PUBLIC clients (no secret; see\n * `registerClient`), and the SDK then authenticates token requests with\n * `client_id` alone and relies on PKCE. Defaults to `INCORTA_CLIENT_SECRET`;\n * unset or empty means public.\n */\n clientSecret?: string;\n /**\n * Secret used to encrypt the session cookie (any long random string, >= 32 chars).\n * Defaults to `INCORTA_AUTH_SECRET`.\n */\n secret?: string;\n /**\n * Path prefix the auth routes are mounted under. The SDK serves\n * `{basePath}/login`, `{basePath}/callback`, `{basePath}/session`,\n * `{basePath}/logout` (and optionally `{basePath}/token`).\n * @default \"/auth\"\n */\n basePath?: string;\n /**\n * External origin of THIS app, e.g. `http://localhost:4000`. Used to build the\n * OAuth callback URL and to validate `redirect_to` targets. When omitted it is\n * derived per request from `x-forwarded-proto`/`x-forwarded-host`/`host`.\n * Defaults to `INCORTA_APP_URL`.\n */\n appUrl?: string;\n /** OAuth scopes to request. @default \"openid profile email\" */\n scope?: string;\n session?: {\n /** @default \"incorta_auth_session\" */\n cookieName?: string;\n /** Session lifetime in seconds (sliding — renewed on activity). @default 43200 (12h) */\n maxAge?: number;\n };\n cookies?: {\n /**\n * `Secure` attribute for cookies. `\"auto\"` sets it when the request is HTTPS.\n * @default \"auto\"\n */\n secure?: boolean | \"auto\";\n /**\n * `SameSite` for the session/state cookies. The `\"lax\"` default is right\n * for top-level apps. Set `\"none\"` when the app runs inside a CROSS-SITE\n * iframe (e.g. an embedded preview) — Lax cookies are never sent there,\n * which loops the login flow forever. `\"none\"` forces the `Secure`\n * attribute (browsers require the pair; Chrome still accepts it on\n * trustworthy plain-http origins like `*.localhost`). Defaults to\n * `INCORTA_COOKIE_SAMESITE`.\n * @default \"lax\"\n */\n sameSite?: \"lax\" | \"none\";\n /**\n * CHIPS: adds the `Partitioned` attribute, giving each embedding\n * top-level site its own cookie jar — what modern Chrome requires for\n * cookies in third-party iframes. Use together with `sameSite: \"none\"`.\n * Defaults to `INCORTA_COOKIE_PARTITIONED` (`1`/`true`).\n * @default false\n */\n partitioned?: boolean;\n };\n /**\n * Expose `GET {basePath}/token` returning the raw Incorta access token to the\n * browser (for calling Incorta public APIs directly from the frontend). Off by\n * default — prefer proxying through your backend.\n * @default false\n */\n exposeAccessToken?: boolean;\n /**\n * Who may use the app — enforced by the whole-app gate (`auth.gate`, and\n * the framework middlewares built on it):\n *\n * - `\"public\"`: anyone with the URL, signed in or not (login stays\n * available, sessions become optional);\n * - `\"authenticated\"` (default): any signed-in Incorta user;\n * - `\"catalog\"`: signed-in AND at least VIEW access to this app in the\n * Incorta catalog, verified against `/apps-catalog/check` with a 30s\n * per-user cache. Unregistered apps stay open to authenticated users\n * (legacy-open); an unanswerable check fails CLOSED (503).\n *\n * Normally stamped by the Incorta Apps platform via `INCORTA_APP_ACCESS`.\n * An unrecognized env value falls back to `\"authenticated\"` (never to\n * `\"public\"` — a typo must not open the app) with a console warning.\n */\n appAccess?: AppAccessPolicy;\n /**\n * How to sign in when the app runs inside a cross-site iframe (the Incorta\n * builder preview). `\"redirect\"` (default) bounces the frame to Incorta —\n * correct top-level, but when the Incorta cluster has iframe inclusion OFF\n * (its login is `X-Frame-Options: SAMEORIGIN` and its session cookie is\n * `SameSite=Lax`) the login can't render/complete in the frame. `\"new-tab\"`\n * serves an interstitial that, inside a frame, offers a \"Sign in with\n * Incorta\" button opening the app top-level in a new tab (where login\n * works); at the top level it still redirects straight to Incorta.\n * Env fallback: `INCORTA_EMBEDDED_SIGN_IN`.\n * @default \"redirect\"\n */\n embeddedSignIn?: \"redirect\" | \"new-tab\";\n advanced?: {\n /** Override the OIDC discovery document URL (rarely needed). */\n discoveryUrl?: string;\n /** Override JWKS resolution (used by tests to verify against a local key). */\n jwks?: JWTVerifyGetKey;\n /** Override fetch (used by tests). */\n fetch?: typeof fetch;\n };\n}\n\nexport interface ResolvedConfig {\n incortaUrl: string;\n /** Server-side fetch base when set (split horizon) — see the config doc. */\n internalIncortaUrl?: string;\n tenant: string;\n clientId: string;\n /** Absent for a public client: token requests then carry `client_id` instead of Basic auth. */\n clientSecret?: string;\n secret: string;\n basePath: string;\n appUrl?: string;\n scope: string;\n cookieName: string;\n stateCookieName: string;\n sessionMaxAge: number;\n secureCookies: boolean | \"auto\";\n cookieSameSite: \"lax\" | \"none\";\n cookiePartitioned: boolean;\n exposeAccessToken: boolean;\n appAccess: AppAccessPolicy;\n embeddedSignIn: \"redirect\" | \"new-tab\";\n /** `{incortaUrl}/oauth/{tenant}` — the OIDC issuer for this tenant. */\n issuer: string;\n discoveryUrl: string;\n jwksOverride?: JWTVerifyGetKey;\n fetch: typeof fetch;\n}\n\nfunction resolveAppAccess(\n input: AppAccessPolicy | undefined,\n envValue: string | undefined,\n): AppAccessPolicy {\n if (input) return input;\n const raw = envValue?.trim().toLowerCase();\n if (!raw) return \"authenticated\";\n if (raw === \"public\" || raw === \"authenticated\" || raw === \"catalog\") return raw;\n // Fail SAFE, not open: garbage in the platform-stamped env keeps the\n // login-only posture rather than silently exposing or bricking the app.\n console.warn(\n `[incorta-auth] Unrecognized INCORTA_APP_ACCESS value \"${envValue}\" — using \"authenticated\".`,\n );\n return \"authenticated\";\n}\n\nfunction required(value: string | undefined, name: string, envVar: string): string {\n if (!value) {\n throw new Error(\n `[incorta-auth] Missing required config \"${name}\" (or env var ${envVar}).`,\n );\n }\n return value;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nexport function resolveConfig(input: IncortaAuthConfig = {}): ResolvedConfig {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env ?? {};\n\n const incortaUrl = stripTrailingSlash(\n required(input.incortaUrl ?? env.INCORTA_URL, \"incortaUrl\", \"INCORTA_URL\"),\n );\n const internalIncortaUrl = input.internalIncortaUrl ?? env.INCORTA_INTERNAL_URL;\n const tenant = required(input.tenant ?? env.INCORTA_TENANT, \"tenant\", \"INCORTA_TENANT\");\n const clientId = required(\n input.clientId ?? env.INCORTA_CLIENT_ID,\n \"clientId\",\n \"INCORTA_CLIENT_ID\",\n );\n // Optional: a public client (Incorta's default for registered apps) has none.\n const clientSecret = (input.clientSecret ?? env.INCORTA_CLIENT_SECRET)?.trim() || undefined;\n const secret = required(\n input.secret ?? env.INCORTA_AUTH_SECRET,\n \"secret\",\n \"INCORTA_AUTH_SECRET\",\n );\n if (secret.length < 32) {\n throw new Error(\n \"[incorta-auth] `secret` must be at least 32 characters. Generate one with: openssl rand -base64 32\",\n );\n }\n\n const basePath = input.basePath ?? \"/auth\";\n if (!basePath.startsWith(\"/\") || basePath.endsWith(\"/\")) {\n throw new Error(\n `[incorta-auth] basePath must start with \"/\" and not end with \"/\" (got \"${basePath}\").`,\n );\n }\n\n const appUrl = input.appUrl ?? env.INCORTA_APP_URL;\n const cookieName = input.session?.cookieName ?? \"incorta_auth_session\";\n const issuer = `${incortaUrl}/oauth/${tenant}`;\n\n const envSameSite = env.INCORTA_COOKIE_SAMESITE?.toLowerCase();\n const cookieSameSite =\n input.cookies?.sameSite ??\n (envSameSite === \"none\" || envSameSite === \"lax\" ? envSameSite : \"lax\");\n const envPartitioned = env.INCORTA_COOKIE_PARTITIONED?.toLowerCase();\n const cookiePartitioned =\n input.cookies?.partitioned ??\n (envPartitioned === \"1\" || envPartitioned === \"true\");\n if (cookiePartitioned && cookieSameSite !== \"none\") {\n throw new Error(\n '[incorta-auth] `cookies.partitioned` requires `cookies.sameSite: \"none\"` — browsers only partition cross-site cookies.',\n );\n }\n\n return {\n incortaUrl,\n internalIncortaUrl: internalIncortaUrl\n ? stripTrailingSlash(internalIncortaUrl)\n : undefined,\n tenant,\n clientId,\n clientSecret,\n secret,\n basePath,\n appUrl: appUrl ? stripTrailingSlash(appUrl) : undefined,\n scope: input.scope ?? \"openid profile email\",\n cookieName,\n stateCookieName: `${cookieName}_state`,\n sessionMaxAge: input.session?.maxAge ?? 12 * 60 * 60,\n secureCookies: input.cookies?.secure ?? \"auto\",\n cookieSameSite,\n cookiePartitioned,\n exposeAccessToken: input.exposeAccessToken ?? false,\n appAccess: resolveAppAccess(input.appAccess, env.INCORTA_APP_ACCESS),\n embeddedSignIn:\n input.embeddedSignIn ??\n (env.INCORTA_EMBEDDED_SIGN_IN?.toLowerCase() === \"new-tab\"\n ? \"new-tab\"\n : \"redirect\"),\n issuer,\n discoveryUrl:\n input.advanced?.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`,\n jwksOverride: input.advanced?.jwks,\n fetch: input.advanced?.fetch ?? fetch,\n };\n}\n","import { randomBytes } from \"node:crypto\";\nimport {\n type AppAccess,\n AppAccessTokenRejectedError,\n AppAccessUnavailableError,\n createAppAccessChecker,\n} from \"./app-access.js\";\nimport type { ResolvedConfig, SessionData, SessionInfo } from \"./config.js\";\nimport { deleteCookie, parseCookies, serializeCookie } from \"./cookies.js\";\nimport { getEndpoints } from \"./discovery.js\";\nimport { CODE_CHALLENGE_METHOD, codeChallengeFor, generateCodeVerifier } from \"./pkce.js\";\nimport {\n sealLoginState,\n sealSession,\n unsealLoginState,\n unsealSession,\n} from \"./session.js\";\nimport {\n IncortaAuthError,\n exchangeCode,\n refreshTokens,\n userFromAccessToken,\n verifyIdToken,\n} from \"./tokens.js\";\n\n/** A read-only view of the current session for server-side app code. */\nexport interface AuthSession extends SessionInfo {\n /** Incorta access token — pass as `Authorization: Bearer` to Incorta APIs. */\n accessToken: string;\n /**\n * The catalog verdict for this user and app, when a gate already resolved\n * it on this request (`appAccess: \"catalog\"` policy). Absent otherwise —\n * fetch on demand with `auth.getAppAccess(session)`.\n */\n appAccess?: AppAccess;\n}\n\nexport type AuthHandler = (request: Request) => Promise<Response>;\n\n/**\n * Whole-app gate: a `Response` to short-circuit with (auth routes, login\n * redirects, deny/unavailable pages) or `null` for \"let the request through\n * to the app\".\n */\nexport type AuthGate = (request: Request) => Promise<Response | null>;\n\n/** Refresh the access token when it has less than this long left to live. */\nconst ACCESS_TOKEN_REFRESH_LEEWAY_MS = 60 * 1000;\n\nfunction requestOrigin(config: ResolvedConfig, request: Request): string {\n if (config.appUrl) return config.appUrl;\n const url = new URL(request.url);\n const proto =\n request.headers.get(\"x-forwarded-proto\")?.split(\",\")[0]?.trim() ??\n url.protocol.replace(\":\", \"\");\n const host =\n request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim() ??\n request.headers.get(\"host\") ??\n url.host;\n return `${proto}://${host}`;\n}\n\nfunction isSecureRequest(config: ResolvedConfig, request: Request): boolean {\n if (config.secureCookies !== \"auto\") return config.secureCookies;\n return requestOrigin(config, request).startsWith(\"https://\");\n}\n\n/**\n * Only same-origin destinations are allowed after login/logout — anything else\n * would let a crafted link bounce users through this app to an attacker page.\n */\nfunction validateReturnTo(raw: string | null, origin: string): string {\n if (!raw) return \"/\";\n if (raw.startsWith(\"/\") && !raw.startsWith(\"//\")) return raw;\n try {\n const url = new URL(raw);\n if (url.origin === origin) return `${url.pathname}${url.search}${url.hash}` || \"/\";\n } catch {\n // Not a URL — fall through.\n }\n return \"/\";\n}\n\nfunction json(status: number, body: unknown, cookies: string[] = []): Response {\n const headers = new Headers({\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(JSON.stringify(body), { status, headers });\n}\n\nfunction redirect(location: string, cookies: string[] = []): Response {\n const headers = new Headers({ location, \"cache-control\": \"no-store\" });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(null, { status: 302, headers });\n}\n\nfunction withErrorParam(target: string, code: string): string {\n const separator = target.includes(\"?\") ? \"&\" : \"?\";\n return `${target}${separator}incorta_auth_error=${encodeURIComponent(code)}`;\n}\n\nfunction randomToken(): string {\n return randomBytes(16).toString(\"base64url\");\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/** Browser navigations get pages/redirects; data requests get JSON. */\nfunction wantsHtml(request: Request): boolean {\n return (\n request.method.toUpperCase() === \"GET\" &&\n (request.headers.get(\"accept\") ?? \"\").includes(\"text/html\")\n );\n}\n\nfunction requestPath(url: URL): string {\n return `${url.pathname}${url.search}`;\n}\n\nexport function createHandler(config: ResolvedConfig): {\n handler: AuthHandler;\n getSession: (request: Request) => Promise<AuthSession | null>;\n gate: AuthGate;\n getAppAccess: (session: AuthSession) => Promise<AppAccess>;\n} {\n const appAccessChecker = createAppAccessChecker(config);\n\n const sessionCookieOptions = (secure: boolean) => ({\n httpOnly: true,\n // SameSite=None requires Secure — force the pair even on plain http\n // (Chrome accepts Secure cookies from trustworthy origins like\n // `*.localhost`; real cross-site embedding needs https anyway).\n secure: config.cookieSameSite === \"none\" ? true : secure,\n sameSite: config.cookieSameSite,\n partitioned: config.cookiePartitioned,\n path: \"/\",\n });\n\n async function login(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const secure = isSecureRequest(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n\n // Already signed in — skip the OAuth round trip entirely.\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const existing = await unsealSession(config, cookies[config.cookieName]);\n if (existing) return redirect(returnTo);\n\n const state = randomToken();\n const nonce = randomToken();\n // PKCE binds the code to this server (pkce.ts) — the verifier stays in the\n // sealed cookie, only its challenge goes to Incorta.\n const codeVerifier = generateCodeVerifier();\n const stateCookie = serializeCookie(\n config.stateCookieName,\n await sealLoginState(config, { state, nonce, returnTo, codeVerifier }),\n { ...sessionCookieOptions(secure), maxAge: 600 },\n );\n\n const { authorizationEndpoint } = await getEndpoints(config);\n const authorizeUrl = new URL(authorizationEndpoint);\n authorizeUrl.searchParams.set(\"response_type\", \"code\");\n authorizeUrl.searchParams.set(\"client_id\", config.clientId);\n authorizeUrl.searchParams.set(\"redirect_uri\", `${origin}${config.basePath}/callback`);\n authorizeUrl.searchParams.set(\"scope\", config.scope);\n authorizeUrl.searchParams.set(\"state\", state);\n authorizeUrl.searchParams.set(\"nonce\", nonce);\n authorizeUrl.searchParams.set(\"code_challenge\", codeChallengeFor(codeVerifier));\n authorizeUrl.searchParams.set(\"code_challenge_method\", CODE_CHALLENGE_METHOD);\n\n // Embedded apps that can't complete Incorta login in a cross-site iframe\n // get a framing-aware interstitial instead of a doomed in-frame redirect.\n if (config.embeddedSignIn === \"new-tab\") {\n return newTabSignInPage(authorizeUrl.toString(), `${origin}/`, [stateCookie]);\n }\n return redirect(authorizeUrl.toString(), [stateCookie]);\n }\n\n async function callback(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const secure = isSecureRequest(config, request);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const clearState = deleteCookie(config.stateCookieName, sessionCookieOptions(secure));\n\n const loginState = await unsealLoginState(config, cookies[config.stateCookieName]);\n const returnTo = validateReturnTo(loginState?.returnTo ?? null, origin);\n\n const fail = (code: string) => redirect(withErrorParam(returnTo, code), [clearState]);\n\n const oauthError = url.searchParams.get(\"error\");\n if (oauthError) return fail(oauthError);\n\n const code = url.searchParams.get(\"code\");\n const state = url.searchParams.get(\"state\");\n if (!code || !state) return fail(\"missing_code\");\n if (!loginState) return fail(\"state_missing\");\n if (state !== loginState.state) return fail(\"state_mismatch\");\n\n try {\n const tokens = await exchangeCode(\n config,\n code,\n `${origin}${config.basePath}/callback`,\n loginState.codeVerifier,\n );\n const user = tokens.idToken\n ? await verifyIdToken(config, tokens.idToken, loginState.nonce)\n : await userFromAccessToken(config, tokens.accessToken);\n\n const session: SessionData = {\n user,\n accessToken: tokens.accessToken,\n accessTokenExpiresAt: tokens.accessTokenExpiresAt,\n refreshToken: tokens.refreshToken,\n createdAt: Date.now(),\n };\n const sessionCookie = serializeCookie(\n config.cookieName,\n await sealSession(config, session),\n { ...sessionCookieOptions(secure), maxAge: config.sessionMaxAge },\n );\n return redirect(returnTo, [sessionCookie, clearState]);\n } catch (error) {\n const code = error instanceof IncortaAuthError ? error.code : \"callback_failed\";\n return fail(code);\n }\n }\n\n /**\n * Loads the session from the request cookie; refreshes the Incorta access\n * token (rotating the refresh token) when it is about to expire, and renews\n * the sliding session cookie past its half-life. Returns the cookies to set.\n */\n async function loadSession(request: Request): Promise<{\n session: SessionData | null;\n sessionExpiresAt: number;\n setCookies: string[];\n }> {\n const secure = isSecureRequest(config, request);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unsealed = await unsealSession(config, cookies[config.cookieName]);\n if (!unsealed) return { session: null, sessionExpiresAt: 0, setCookies: [] };\n\n let { session } = unsealed;\n let mustReseal = false;\n const now = Date.now();\n\n if (session.accessTokenExpiresAt - now < ACCESS_TOKEN_REFRESH_LEEWAY_MS) {\n if (session.refreshToken) {\n try {\n const tokens = await refreshTokens(config, session.refreshToken);\n session = {\n ...session,\n accessToken: tokens.accessToken,\n accessTokenExpiresAt: tokens.accessTokenExpiresAt,\n refreshToken: tokens.refreshToken ?? session.refreshToken,\n };\n mustReseal = true;\n } catch {\n // Refresh failed (expired/rotated elsewhere). Keep the session while\n // the current access token is still usable; otherwise sign out.\n if (session.accessTokenExpiresAt <= now) {\n return {\n session: null,\n sessionExpiresAt: 0,\n setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))],\n };\n }\n }\n } else if (session.accessTokenExpiresAt <= now) {\n return {\n session: null,\n sessionExpiresAt: 0,\n setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))],\n };\n }\n }\n\n const halfLifeMs = (config.sessionMaxAge * 1000) / 2;\n if (now - unsealed.issuedAt > halfLifeMs) mustReseal = true;\n\n if (!mustReseal) {\n return { session, sessionExpiresAt: unsealed.expiresAt, setCookies: [] };\n }\n const resealed = serializeCookie(config.cookieName, await sealSession(config, session), {\n ...sessionCookieOptions(secure),\n maxAge: config.sessionMaxAge,\n });\n return {\n session,\n sessionExpiresAt: now + config.sessionMaxAge * 1000,\n setCookies: [resealed],\n };\n }\n\n async function sessionRoute(request: Request): Promise<Response> {\n const { session, sessionExpiresAt, setCookies } = await loadSession(request);\n if (!session) return json(200, { user: null }, setCookies);\n return json(\n 200,\n {\n user: session.user,\n session: {\n expiresAt: sessionExpiresAt,\n accessTokenExpiresAt: session.accessTokenExpiresAt,\n },\n },\n setCookies,\n );\n }\n\n async function tokenRoute(request: Request): Promise<Response> {\n if (!config.exposeAccessToken) return json(404, { error: \"not_found\" });\n const { session, setCookies } = await loadSession(request);\n if (!session) return json(401, { error: \"unauthenticated\" }, setCookies);\n return json(\n 200,\n { accessToken: session.accessToken, expiresAt: session.accessTokenExpiresAt },\n setCookies,\n );\n }\n\n /**\n * `GET {basePath}/refresh?redirect_to=…` — refresh the access token (and\n * reseal the rotated cookie) via a browser round trip, then bounce back.\n * The catalog gate sends navigations here when the session's access token\n * has already expired: the gate itself returns `null` to continue, so it\n * has no response to carry a rotated refresh-token cookie on — a redirect\n * does. A session that cannot be refreshed is sent to login instead\n * (loadSession already cleared its cookie).\n */\n async function refreshRoute(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n const { session, setCookies } = await loadSession(request);\n if (!session) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(returnTo)}`,\n setCookies,\n );\n }\n return redirect(returnTo, setCookies);\n }\n\n async function logout(request: Request, url: URL): Promise<Response> {\n const secure = isSecureRequest(config, request);\n const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));\n if (request.method === \"GET\") {\n // No explicit redirect_to → the signed-out page, NOT \"/\": in an\n // SSO-gated app, landing on a gated page silently re-runs the login\n // flow and (while the Incorta session lives) signs the user straight\n // back in — sign-out must land somewhere UNGATED to visibly stick.\n const raw = url.searchParams.get(\"redirect_to\");\n if (!raw) return redirect(`${config.basePath}/signed-out`, [clearSession]);\n const origin = requestOrigin(config, request);\n return redirect(validateReturnTo(raw, origin), [clearSession]);\n }\n return json(200, { ok: true }, [clearSession]);\n }\n\n /**\n * Ungated landing page after sign-out (it lives under basePath, so app\n * gates pass it through). Without it there is nowhere for a fully-gated\n * app to show \"you are signed out\" — every app page would bounce through\n * the IdP and re-login. Self-contained HTML, no assets.\n */\n function signedOutPage(): Response {\n const signInHref = `${config.basePath}/login?redirect_to=%2F`;\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Signed out</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n a { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; font-size: .9rem;\n text-decoration: none; }\n </style>\n </head>\n <body>\n <main>\n <h1>You’ve signed out of this app</h1>\n <p>\n Your Incorta account may still be signed in elsewhere — signing back\n in here is one click.\n </p>\n <a href=\"${signInHref}\">Sign in with Incorta</a>\n </main>\n </body>\n</html>`,\n {\n status: 200,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n },\n );\n }\n\n /**\n * `embeddedSignIn: \"new-tab\"` interstitial. The inline script decides\n * client-side: at the top level it redirects straight to Incorta; inside a\n * cross-site iframe (where Incorta login can't render/complete when the\n * cluster has iframe inclusion off) it reveals a button that opens the app\n * in a new top-level tab. Carries the same state cookie the plain redirect\n * would set — used by the top-level path; harmless in the frame, where the\n * new tab writes its own first-party state cookie.\n */\n function newTabSignInPage(\n authorizeUrl: string,\n appRoot: string,\n cookies: string[],\n ): Response {\n const body = `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Sign in with Incorta</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.2rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n a.btn { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; text-decoration: none;\n font-weight: 600; }\n </style>\n </head>\n <body>\n <main id=\"frame\" hidden>\n <h1>Sign in with Incorta</h1>\n <p>\n This app signs you in with your Incorta account. The embedded preview\n can’t complete sign-in here — continue in a new tab.\n </p>\n <a class=\"btn\" href=\"${appRoot}\" target=\"_blank\" rel=\"noopener noreferrer\">Sign in with Incorta ↗</a>\n </main>\n <script>\n (function () {\n var authorize = ${JSON.stringify(authorizeUrl)};\n if (window.top === window.self) {\n window.location.replace(authorize);\n } else {\n document.getElementById(\"frame\").hidden = false;\n }\n })();\n </script>\n <noscript><a href=\"${appRoot}\">Continue to sign in</a></noscript>\n </body>\n</html>`;\n const headers = new Headers({\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(body, { status: 200, headers });\n }\n\n /**\n * `appAccess: \"catalog\"` — the user is signed in but the catalog says no:\n * an honest 403 with who they are and a way out (the wrong-account case is\n * the most common real cause). Non-HTML requests get the JSON twin.\n */\n function accessDeniedResponse(request: Request, session: AuthSession): Response {\n if (!wantsHtml(request)) {\n return json(403, { error: \"app_access_denied\" });\n }\n const identity = escapeHtml(session.user.name?.trim() || session.user.sub);\n const detail =\n session.user.name?.trim() && session.user.name.trim() !== session.user.sub\n ? `${identity} (${escapeHtml(session.user.sub)})`\n : identity;\n const switchHref = `${config.basePath}/logout`;\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>No access</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n strong { color: #18181b; font-weight: 600; }\n a { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; font-size: .9rem;\n text-decoration: none; }\n </style>\n </head>\n <body>\n <main>\n <h1>You don’t have access to this application</h1>\n <p>\n Ask the app’s owner to share it with you in Incorta.\n You are signed in as <strong>${detail}</strong>.\n </p>\n <a href=\"${switchHref}\">Sign in with a different account</a>\n </main>\n </body>\n</html>`,\n {\n status: 403,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n },\n );\n }\n\n /**\n * The catalog could not answer (Incorta unreachable/failing) — deny, but\n * make clear this is an outage, not a sharing decision. Distinct from the\n * 403 page so users never conclude they were unshared. Auto-retries: the\n * verdict cache holds no failures, so the next load re-asks.\n */\n function accessUnavailableResponse(request: Request): Response {\n if (!wantsHtml(request)) {\n return json(503, { error: \"app_access_unavailable\" });\n }\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta http-equiv=\"refresh\" content=\"15\" />\n <title>Temporarily unavailable</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0; }\n </style>\n </head>\n <body>\n <main>\n <h1>This app is temporarily unavailable</h1>\n <p>\n Your access could not be verified because Incorta did not respond.\n This page retries automatically — you can also refresh in a\n moment.\n </p>\n </main>\n </body>\n</html>`,\n {\n status: 503,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n \"retry-after\": \"30\",\n },\n },\n );\n }\n\n async function handler(request: Request): Promise<Response> {\n const url = new URL(request.url);\n if (url.pathname !== config.basePath && !url.pathname.startsWith(`${config.basePath}/`)) {\n return json(404, { error: \"not_found\" });\n }\n const route = url.pathname.slice(config.basePath.length) || \"/\";\n const method = request.method.toUpperCase();\n\n try {\n if (route === \"/login\" && method === \"GET\") return await login(request, url);\n if (route === \"/callback\" && method === \"GET\") return await callback(request, url);\n if (route === \"/session\" && method === \"GET\") return await sessionRoute(request);\n if (route === \"/token\" && method === \"GET\") return await tokenRoute(request);\n if (route === \"/refresh\" && method === \"GET\") return await refreshRoute(request, url);\n if (route === \"/logout\" && (method === \"POST\" || method === \"GET\")) {\n return await logout(request, url);\n }\n if (route === \"/signed-out\" && method === \"GET\") return signedOutPage();\n return json(404, { error: \"not_found\" });\n } catch (error) {\n const code = error instanceof IncortaAuthError ? error.code : \"internal_error\";\n const message =\n error instanceof Error ? error.message : \"Unexpected incorta-auth handler error\";\n console.error(`[incorta-auth] ${route} failed:`, message);\n return json(500, { error: code });\n }\n }\n\n async function getSession(request: Request): Promise<AuthSession | null> {\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unsealed = await unsealSession(config, cookies[config.cookieName]);\n if (!unsealed) return null;\n const { session, expiresAt } = unsealed;\n if (session.accessTokenExpiresAt <= Date.now() && !session.refreshToken) return null;\n return {\n user: session.user,\n accessToken: session.accessToken,\n accessTokenExpiresAt: session.accessTokenExpiresAt,\n expiresAt,\n };\n }\n\n /** The catalog verdict for this session's user, via the 30s per-user cache. */\n function getAppAccess(session: AuthSession): Promise<AppAccess> {\n return appAccessChecker.check(session.accessToken, session.user.sub);\n }\n\n /**\n * The whole-app gate, enforcing `config.appAccess` in front of every\n * request (see `IncortaAuthConfig.appAccess`). Auth routes are always\n * served — login stays reachable even for public apps. `null` means\n * \"continue to the app\"; in `\"public\"` mode that is every non-auth route,\n * signed in or not.\n */\n async function gate(request: Request): Promise<Response | null> {\n const url = new URL(request.url);\n if (url.pathname === config.basePath || url.pathname.startsWith(`${config.basePath}/`)) {\n return handler(request);\n }\n if (config.appAccess === \"public\") return null;\n\n const session = await getSession(request);\n if (!session) {\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`,\n );\n }\n return json(401, { error: \"unauthenticated\" });\n }\n if (config.appAccess !== \"catalog\") return null;\n\n // The check replays the session's access token to Incorta — an already\n // expired token would 401 there, so refresh it first via the browser\n // round trip (which is what persists the rotated cookie).\n if (session.accessTokenExpiresAt <= Date.now()) {\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/refresh?redirect_to=${encodeURIComponent(requestPath(url))}`,\n );\n }\n return json(401, { error: \"token_expired\" });\n }\n\n try {\n const verdict = await getAppAccess(session);\n if (verdict.allowed) return null;\n return accessDeniedResponse(request, session);\n } catch (error) {\n if (error instanceof AppAccessTokenRejectedError) {\n // Incorta no longer honors this session's token — sign out and let\n // the login flow mint a fresh one (SSO makes this a silent bounce).\n const secure = isSecureRequest(config, request);\n const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`,\n [clearSession],\n );\n }\n return json(401, { error: \"unauthenticated\" }, [clearSession]);\n }\n if (!(error instanceof AppAccessUnavailableError)) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(\"[incorta-auth] app access check failed unexpectedly:\", message);\n }\n // Verdicts unavailable — NEVER fail open.\n return accessUnavailableResponse(request);\n }\n }\n\n return { handler, getSession, gate, getAppAccess };\n}\n","import type { ResolvedConfig } from \"./config.js\";\n\n/**\n * Platform-stamped access policy for the app (`INCORTA_APP_ACCESS`):\n *\n * - `\"public\"` — anyone with the URL; the whole-app gate is inert\n * (login routes stay mounted, sessions are optional);\n * - `\"authenticated\"`— any signed-in Incorta user (the pre-policy behavior,\n * and the default when the platform stamped nothing);\n * - `\"catalog\"` — signed-in AND at least VIEW access to this app in the\n * Incorta catalog (`/apps-catalog/check`).\n */\nexport type AppAccessPolicy = \"public\" | \"authenticated\" | \"catalog\";\n\n/** Incorta permission bits (com.incorta.api.Action). */\nexport const VIEW_BIT = 1;\nexport const SHARE_BIT = 2;\nexport const EDIT_BIT = 4;\n\n/** The catalog's verdict about the session user's access to THIS app. */\nexport interface AppAccess {\n /**\n * The app is registered in the Incorta catalog. `false` means it predates\n * the catalog integration — the gate treats it as legacy-open (any\n * authenticated tenant user may enter).\n */\n registered: boolean;\n /** Effective Incorta permission bitmask (VIEW=1, SHARE=2, EDIT=4). */\n access: number;\n canView: boolean;\n canEdit: boolean;\n /** What the runtime gate enforces: unregistered (legacy-open) or ≥ VIEW. */\n allowed: boolean;\n}\n\n/** Incorta could not answer (unreachable, 5xx, disabled) — gates fail CLOSED. */\nexport class AppAccessUnavailableError extends Error {\n constructor(cause: string) {\n super(`[incorta-auth] App access check unavailable: ${cause}`);\n this.name = \"AppAccessUnavailableError\";\n }\n}\n\n/** Incorta rejected the session's access token (expired/revoked) — re-login. */\nexport class AppAccessTokenRejectedError extends Error {\n constructor() {\n super(\"[incorta-auth] Incorta rejected the session token during the app access check\");\n this.name = \"AppAccessTokenRejectedError\";\n }\n}\n\n/** Verdicts are cached (allow AND deny) this long per user. */\nexport const APP_ACCESS_CACHE_TTL_MS = 30_000;\nconst CHECK_TIMEOUT_MS = 5_000;\n/** Lazy prune threshold — entries are (user × 30s), so this is generous. */\nconst CACHE_MAX_ENTRIES = 5_000;\n\ninterface CacheEntry {\n verdict: AppAccess;\n expiresAt: number;\n}\n\nexport interface AppAccessChecker {\n /**\n * The catalog verdict for the user behind `accessToken`, served from the\n * per-user cache when fresh. Throws {@link AppAccessUnavailableError} when\n * Incorta cannot answer and {@link AppAccessTokenRejectedError} when it\n * rejects the token itself.\n */\n check(accessToken: string, userKey: string): Promise<AppAccess>;\n /** Test hook: drops all cached verdicts. */\n clearCache(): void;\n}\n\nfunction toVerdict(registered: boolean, access: number): AppAccess {\n const canView = (access & VIEW_BIT) === VIEW_BIT;\n return {\n registered,\n access,\n canView,\n canEdit: (access & EDIT_BIT) === EDIT_BIT,\n allowed: !registered || canView,\n };\n}\n\n/**\n * Client for `GET {incortaUrl}/apps-catalog/check` — the backend resolves\n * which app the token belongs to from its OAuth client (`software_id`), so\n * the request carries nothing but the user's own Bearer token. Server-side\n * call, so the split-horizon `internalIncortaUrl` wins when set.\n *\n * Verdicts (allow and deny alike) are cached per user for\n * {@link APP_ACCESS_CACHE_TTL_MS}; failures are NOT cached — the next\n * request retries, and callers fail closed in the meantime. Concurrent\n * checks for the same user share one in-flight request.\n */\nexport function createAppAccessChecker(config: ResolvedConfig): AppAccessChecker {\n const base = config.internalIncortaUrl ?? config.incortaUrl;\n const checkUrl = `${base}/apps-catalog/check`;\n const cache = new Map<string, CacheEntry>();\n const inFlight = new Map<string, Promise<AppAccess>>();\n\n function prune(now: number): void {\n if (cache.size < CACHE_MAX_ENTRIES) return;\n for (const [key, entry] of cache) {\n if (entry.expiresAt <= now) cache.delete(key);\n }\n }\n\n async function fetchVerdict(accessToken: string): Promise<AppAccess> {\n let response: Response;\n try {\n response = await config.fetch(checkUrl, {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${accessToken}`,\n },\n signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),\n });\n } catch (error) {\n throw new AppAccessUnavailableError((error as Error).message);\n }\n\n if (response.status === 401) throw new AppAccessTokenRejectedError();\n if (!response.ok) {\n // 404 covers both \"Incorta Apps disabled for the tenant\" and an older\n // Incorta without the /check command — verdicts are unavailable either\n // way, and gates never fail open.\n throw new AppAccessUnavailableError(`HTTP ${response.status}`);\n }\n\n let body: { registered?: unknown; access?: unknown };\n try {\n body = (await response.json()) as { registered?: unknown; access?: unknown };\n } catch {\n throw new AppAccessUnavailableError(\"invalid JSON response\");\n }\n if (typeof body.registered !== \"boolean\" || typeof body.access !== \"number\") {\n throw new AppAccessUnavailableError(\"malformed check response\");\n }\n return toVerdict(body.registered, body.access);\n }\n\n async function check(accessToken: string, userKey: string): Promise<AppAccess> {\n const now = Date.now();\n const cached = cache.get(userKey);\n if (cached && cached.expiresAt > now) return cached.verdict;\n\n const pending = inFlight.get(userKey);\n if (pending) return pending;\n\n const promise = fetchVerdict(accessToken)\n .then((verdict) => {\n prune(now);\n cache.set(userKey, { verdict, expiresAt: Date.now() + APP_ACCESS_CACHE_TTL_MS });\n return verdict;\n })\n .finally(() => {\n inFlight.delete(userKey);\n });\n inFlight.set(userKey, promise);\n return promise;\n }\n\n return {\n check,\n clearCache() {\n cache.clear();\n inFlight.clear();\n },\n };\n}\n","export interface CookieOptions {\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: \"lax\" | \"strict\" | \"none\";\n /** CHIPS `Partitioned` attribute — per-top-site cookie jar for cross-site\n * iframe embedding. Browsers require Secure alongside it. */\n partitioned?: boolean;\n path?: string;\n /** Seconds. 0 deletes the cookie. */\n maxAge?: number;\n}\n\nexport function parseCookies(header: string | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const name = part.slice(0, eq).trim();\n if (!name) continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"')) value = value.slice(1, -1);\n try {\n out[name] = decodeURIComponent(value);\n } catch {\n out[name] = value;\n }\n }\n return out;\n}\n\nexport function serializeCookie(\n name: string,\n value: string,\n options: CookieOptions = {},\n): string {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n parts.push(`Path=${options.path ?? \"/\"}`);\n if (options.maxAge !== undefined) {\n parts.push(`Max-Age=${Math.floor(options.maxAge)}`);\n // Expires for compatibility with clients that ignore Max-Age.\n parts.push(`Expires=${new Date(Date.now() + options.maxAge * 1000).toUTCString()}`);\n }\n if (options.httpOnly ?? true) parts.push(\"HttpOnly\");\n if (options.secure) parts.push(\"Secure\");\n const sameSite = options.sameSite ?? \"lax\";\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n // Deleting a partitioned cookie needs the attribute too — callers pass the\n // same options for set and delete, so this covers both.\n if (options.partitioned) parts.push(\"Partitioned\");\n return parts.join(\"; \");\n}\n\nexport function deleteCookie(name: string, options: CookieOptions = {}): string {\n return serializeCookie(name, \"\", { ...options, maxAge: 0 });\n}\n","import { createRemoteJWKSet } from \"jose\";\nimport type { JWTVerifyGetKey } from \"jose\";\nimport type { ResolvedConfig } from \"./config.js\";\n\ninterface DiscoveryDocument {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n jwks_uri: string;\n}\n\nexport interface Endpoints {\n issuer: string;\n authorizationEndpoint: string;\n tokenEndpoint: string;\n jwks: JWTVerifyGetKey;\n}\n\nconst endpointsCache = new Map<string, Promise<Endpoints>>();\n\n/**\n * Split horizon: URLs the SERVER fetches (discovery, token endpoint, JWKS)\n * are rebased from the public `incortaUrl` onto `internalIncortaUrl` when\n * set. Values the BROWSER navigates to (authorization endpoint) and values\n * used for VALIDATION (issuer — must equal the token's `iss` claim, which\n * Incorta mints from its public URL) are never rewritten. URLs outside the\n * public base pass through untouched.\n */\nfunction toInternal(config: ResolvedConfig, url: string): string {\n const internal = config.internalIncortaUrl;\n if (!internal || !url.startsWith(config.incortaUrl)) return url;\n return internal + url.slice(config.incortaUrl.length);\n}\n\n/**\n * Resolves the tenant's OAuth endpoints, preferring the OIDC discovery document\n * and falling back to Incorta's fixed `/oauth/{tenant}/...` layout when the\n * document is unreachable. Cached per issuer (and internal-URL horizon) for\n * the process lifetime.\n */\nexport function getEndpoints(config: ResolvedConfig): Promise<Endpoints> {\n const cacheKey = `${config.issuer}|${config.internalIncortaUrl ?? \"\"}`;\n const cached = endpointsCache.get(cacheKey);\n if (cached) return cached;\n\n const promise = resolve(config).catch((error) => {\n // Do not cache failures — a temporarily unreachable Incorta should not\n // poison the process.\n endpointsCache.delete(cacheKey);\n throw error;\n });\n endpointsCache.set(cacheKey, promise);\n return promise;\n}\n\nasync function resolve(config: ResolvedConfig): Promise<Endpoints> {\n const fallback: DiscoveryDocument = {\n issuer: config.issuer,\n authorization_endpoint: `${config.issuer}/authorize`,\n token_endpoint: `${config.issuer}/token`,\n jwks_uri: `${config.issuer}/.well-known/jwks.json`,\n };\n\n let doc = fallback;\n try {\n const response = await config.fetch(toInternal(config, config.discoveryUrl), {\n headers: { accept: \"application/json\" },\n });\n if (response.ok) {\n const body = (await response.json()) as Partial<DiscoveryDocument>;\n doc = {\n issuer: body.issuer ?? fallback.issuer,\n authorization_endpoint: body.authorization_endpoint ?? fallback.authorization_endpoint,\n token_endpoint: body.token_endpoint ?? fallback.token_endpoint,\n jwks_uri: body.jwks_uri ?? fallback.jwks_uri,\n };\n }\n } catch {\n // Fall through to the fixed layout.\n }\n\n const jwks =\n config.jwksOverride ??\n createRemoteJWKSet(new URL(toInternal(config, doc.jwks_uri)), {\n cacheMaxAge: 10 * 60 * 1000,\n cooldownDuration: 30 * 1000,\n });\n\n return {\n issuer: doc.issuer,\n authorizationEndpoint: doc.authorization_endpoint,\n tokenEndpoint: toInternal(config, doc.token_endpoint),\n jwks,\n };\n}\n\n/** Test hook: clears the per-process endpoint cache. */\nexport function clearEndpointsCache(): void {\n endpointsCache.clear();\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\n/**\n * PKCE (RFC 7636) for the authorization-code flow. Incorta registers app\n * clients as PUBLIC clients (no client secret — see `registerClient`), so\n * the code verifier is what binds the authorization code to this server:\n * an intercepted code is worthless without it. The verifier lives in the\n * sealed login-state cookie between `/login` and `/callback`; only its\n * S256 challenge travels to Incorta.\n */\nexport function generateCodeVerifier(): string {\n // 32 random bytes → 43 base64url chars, inside RFC 7636's 43–128 range.\n return randomBytes(32).toString(\"base64url\");\n}\n\nexport function codeChallengeFor(codeVerifier: string): string {\n return createHash(\"sha256\").update(codeVerifier, \"ascii\").digest(\"base64url\");\n}\n\nexport const CODE_CHALLENGE_METHOD = \"S256\";\n","import { createHash } from \"node:crypto\";\nimport { EncryptJWT, jwtDecrypt } from \"jose\";\nimport type { ResolvedConfig, SessionData } from \"./config.js\";\n\n/**\n * Session and state cookies are sealed as encrypted JWTs (JWE, dir + A256GCM):\n * tamper-proof AND opaque, since the session carries the Incorta access/refresh\n * tokens. The 256-bit key is derived from the configured secret.\n */\nfunction sealKey(config: ResolvedConfig): Uint8Array {\n return new Uint8Array(\n createHash(\"sha256\").update(`incorta-auth:${config.secret}`).digest(),\n );\n}\n\ninterface SealedPayload {\n /** Guards against replaying one cookie kind as the other. */\n purpose: \"session\" | \"state\";\n data: unknown;\n}\n\nasync function seal(\n config: ResolvedConfig,\n purpose: SealedPayload[\"purpose\"],\n data: unknown,\n maxAgeSeconds: number,\n): Promise<string> {\n return new EncryptJWT({ purpose, data })\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(`${maxAgeSeconds}s`)\n .encrypt(sealKey(config));\n}\n\nasync function unseal<T>(\n config: ResolvedConfig,\n purpose: SealedPayload[\"purpose\"],\n cookieValue: string | undefined,\n): Promise<{ data: T; issuedAt: number; expiresAt: number } | null> {\n if (!cookieValue) return null;\n try {\n const { payload } = await jwtDecrypt(cookieValue, sealKey(config));\n if (payload.purpose !== purpose) return null;\n return {\n data: payload.data as T,\n issuedAt: (payload.iat ?? 0) * 1000,\n expiresAt: (payload.exp ?? 0) * 1000,\n };\n } catch {\n // Expired, tampered, or sealed with a different secret — treat as absent.\n return null;\n }\n}\n\nexport function sealSession(config: ResolvedConfig, session: SessionData): Promise<string> {\n return seal(config, \"session\", session, config.sessionMaxAge);\n}\n\nexport async function unsealSession(\n config: ResolvedConfig,\n cookieValue: string | undefined,\n): Promise<{ session: SessionData; issuedAt: number; expiresAt: number } | null> {\n const result = await unseal<SessionData>(config, \"session\", cookieValue);\n if (!result) return null;\n const { data, issuedAt, expiresAt } = result;\n if (!data || typeof data !== \"object\" || !data.user || !data.accessToken) return null;\n return { session: data, issuedAt, expiresAt };\n}\n\n/** Login round-trip state (CSRF protection + post-login destination). */\nexport interface LoginState {\n state: string;\n nonce: string;\n returnTo: string;\n /**\n * PKCE code verifier for this login attempt (absent only in a state cookie\n * sealed by a pre-PKCE build — the callback then exchanges without one).\n */\n codeVerifier?: string;\n}\n\nconst STATE_MAX_AGE_SECONDS = 10 * 60;\n\nexport function sealLoginState(config: ResolvedConfig, state: LoginState): Promise<string> {\n return seal(config, \"state\", state, STATE_MAX_AGE_SECONDS);\n}\n\nexport async function unsealLoginState(\n config: ResolvedConfig,\n cookieValue: string | undefined,\n): Promise<LoginState | null> {\n const result = await unseal<LoginState>(config, \"state\", cookieValue);\n if (!result) return null;\n const { data } = result;\n if (!data || typeof data.state !== \"string\" || typeof data.nonce !== \"string\") return null;\n if (data.codeVerifier !== undefined && typeof data.codeVerifier !== \"string\") return null;\n return data;\n}\n","import { decodeJwt, jwtVerify } from \"jose\";\nimport type { JWTPayload } from \"jose\";\nimport type { IncortaUser, ResolvedConfig } from \"./config.js\";\nimport { getEndpoints } from \"./discovery.js\";\n\nexport interface TokenSet {\n accessToken: string;\n /** Epoch millis derived from the access token's `exp` claim. */\n accessTokenExpiresAt: number;\n idToken?: string;\n refreshToken?: string;\n}\n\ninterface TokenEndpointResponse {\n access_token?: string;\n id_token?: string;\n refresh_token?: string;\n error?: string;\n error_description?: string;\n}\n\nexport class IncortaAuthError extends Error {\n constructor(\n readonly code: string,\n message: string,\n ) {\n super(`[incorta-auth] ${message}`);\n this.name = \"IncortaAuthError\";\n }\n}\n\nfunction basicAuth(clientId: string, clientSecret: string): string {\n const raw = `${clientId}:${clientSecret}`;\n return `Basic ${Buffer.from(raw, \"utf8\").toString(\"base64\")}`;\n}\n\n/**\n * How the token request identifies the client (RFC 6749 §2.3 / §4.1.3): a\n * confidential client authenticates with `client_secret_basic`; a public\n * client — what Incorta registers apps as — has no secret and sends\n * `client_id` in the body instead.\n */\nfunction clientAuthentication(\n config: ResolvedConfig,\n params: Record<string, string>,\n): { headers: Record<string, string>; params: Record<string, string> } {\n if (config.clientSecret) {\n return { headers: { authorization: basicAuth(config.clientId, config.clientSecret) }, params };\n }\n return { headers: {}, params: { client_id: config.clientId, ...params } };\n}\n\nasync function callTokenEndpoint(\n config: ResolvedConfig,\n params: Record<string, string>,\n): Promise<TokenSet> {\n const { tokenEndpoint } = await getEndpoints(config);\n const auth = clientAuthentication(config, params);\n\n let response: Response;\n try {\n response = await config.fetch(tokenEndpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n accept: \"application/json\",\n ...auth.headers,\n },\n body: new URLSearchParams(auth.params).toString(),\n });\n } catch (error) {\n throw new IncortaAuthError(\n \"incorta_unreachable\",\n `Could not reach the Incorta token endpoint (${tokenEndpoint}): ${(error as Error).message}`,\n );\n }\n\n let body: TokenEndpointResponse;\n try {\n body = (await response.json()) as TokenEndpointResponse;\n } catch {\n throw new IncortaAuthError(\n \"invalid_token_response\",\n `Token endpoint returned a non-JSON response (HTTP ${response.status}).`,\n );\n }\n\n if (!response.ok || !body.access_token) {\n throw new IncortaAuthError(\n body.error ?? \"token_exchange_failed\",\n body.error_description ?? `Token exchange failed (HTTP ${response.status}).`,\n );\n }\n\n const { exp } = decodeJwt(body.access_token);\n return {\n accessToken: body.access_token,\n accessTokenExpiresAt: exp ? exp * 1000 : Date.now() + 60 * 60 * 1000,\n idToken: body.id_token,\n refreshToken: body.refresh_token,\n };\n}\n\nexport function exchangeCode(\n config: ResolvedConfig,\n code: string,\n redirectUri: string,\n /** The PKCE verifier sealed at `/login`; Incorta checks it against the challenge the authorize request carried. */\n codeVerifier?: string,\n): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n ...(codeVerifier ? { code_verifier: codeVerifier } : {}),\n });\n}\n\nexport function refreshTokens(config: ResolvedConfig, refreshToken: string): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n });\n}\n\nasync function verifyJwt(config: ResolvedConfig, token: string): Promise<JWTPayload> {\n const { jwks, issuer } = await getEndpoints(config);\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer,\n audience: config.clientId,\n algorithms: [\"RS256\"],\n });\n return payload;\n } catch (error) {\n throw new IncortaAuthError(\n \"invalid_token\",\n `Token verification failed: ${(error as Error).message}`,\n );\n }\n}\n\nexport async function verifyAccessToken(\n config: ResolvedConfig,\n accessToken: string,\n): Promise<JWTPayload> {\n return verifyJwt(config, accessToken);\n}\n\n/**\n * Verifies the ID token (signature, issuer, audience, nonce) and maps its\n * claims to the SDK's user shape.\n */\nexport async function verifyIdToken(\n config: ResolvedConfig,\n idToken: string,\n expectedNonce: string,\n): Promise<IncortaUser> {\n const payload = await verifyJwt(config, idToken);\n\n if (payload.nonce !== expectedNonce) {\n throw new IncortaAuthError(\"nonce_mismatch\", \"ID token nonce does not match the login request.\");\n }\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new IncortaAuthError(\"invalid_token\", \"ID token is missing the sub claim.\");\n }\n\n const roles = Array.isArray(payload.roles)\n ? payload.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n\n return {\n sub: payload.sub,\n name: typeof payload.name === \"string\" ? payload.name : undefined,\n email: typeof payload.email === \"string\" ? payload.email : undefined,\n roles,\n tenant: typeof payload.tenant === \"string\" ? payload.tenant : config.tenant,\n };\n}\n\n/**\n * Fallback identity when the client was registered without the `openid` scope\n * (no ID token in the response): identify the user from the verified access\n * token instead. Name/email/roles are unavailable in that case.\n */\nexport async function userFromAccessToken(\n config: ResolvedConfig,\n accessToken: string,\n): Promise<IncortaUser> {\n const payload = await verifyAccessToken(config, accessToken);\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new IncortaAuthError(\"invalid_token\", \"Access token is missing the sub claim.\");\n }\n return {\n sub: payload.sub,\n roles: [],\n tenant: typeof payload.tenant === \"string\" ? payload.tenant : config.tenant,\n };\n}\n","interface ManagementErrorBody {\n error?: string;\n error_description?: string;\n}\n\nfunction registrationEndpoint(incortaUrl: string, tenant: string, clientId?: string): string {\n const base = `${incortaUrl.replace(/\\/+$/, \"\")}/oauth/${tenant}/register`;\n return clientId ? `${base}/${clientId}` : base;\n}\n\nasync function readJson<T extends ManagementErrorBody>(\n response: Response,\n what: string,\n): Promise<T> {\n let body: T;\n try {\n body = (await response.json()) as T;\n } catch {\n throw new Error(\n `[incorta-auth] ${what} returned a non-JSON response (HTTP ${response.status}). ` +\n \"Is the OAuth authorization server enabled on this cluster?\",\n );\n }\n if (!response.ok) {\n throw new Error(\n `[incorta-auth] ${what} failed (HTTP ${response.status}): ` +\n `${body.error ?? \"unknown_error\"} — ${body.error_description ?? \"no description\"}`,\n );\n }\n return body;\n}\n\nexport interface RegisterClientOptions {\n /** Incorta base URL, e.g. `http://localhost:8080/incorta`. */\n incortaUrl: string;\n /** Tenant to register the client in. */\n tenant: string;\n /** Display name of the client. */\n name: string;\n /** Allowed OAuth callback URLs, e.g. `http://localhost:4000/auth/callback`. */\n redirectUris: string[];\n /** @default \"openid profile email\" */\n scope?: string;\n /** @default [\"authorization_code\", \"refresh_token\"] */\n grantTypes?: string[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface RegisteredClient {\n clientId: string;\n /**\n * Only for a CONFIDENTIAL client. Incorta issues one solely to clients\n * holding the `client_credentials` grant (PAT-authenticated registration);\n * an app registered here is a PUBLIC client and gets none — token requests\n * then carry `client_id` alone, and PKCE protects the code flow.\n */\n clientSecret?: string;\n /** What Incorta decided: `none` for a public client, `client_secret_basic` for a confidential one. */\n tokenEndpointAuthMethod?: string;\n /** Bearer token for managing this registration (GET/PUT/DELETE it later). */\n registrationAccessToken?: string;\n /** URL to manage this registration. */\n registrationClientUri?: string;\n}\n\n/**\n * Registers an OAuth client in Incorta via RFC 7591 dynamic client\n * registration (`POST {incortaUrl}/oauth/{tenant}/register`). Incorta allows\n * unauthenticated registration for authorization_code clients and registers\n * them as PUBLIC clients: the response carries the generated client_id (and a\n * client_secret only on servers that still issue one) — store it once, e.g.\n * in the app's `.env`.\n */\nexport async function registerClient(\n options: RegisterClientOptions,\n): Promise<RegisteredClient> {\n const {\n incortaUrl,\n tenant,\n name,\n redirectUris,\n scope = \"openid profile email\",\n grantTypes = [\"authorization_code\", \"refresh_token\"],\n fetchImpl = fetch,\n } = options;\n\n if (!redirectUris.length) {\n throw new Error(\"[incorta-auth] registerClient requires at least one redirect URI.\");\n }\n\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", accept: \"application/json\" },\n body: JSON.stringify({\n client_name: name,\n redirect_uris: redirectUris,\n grant_types: grantTypes,\n scope,\n token_endpoint_auth_method: \"client_secret_basic\",\n }),\n });\n\n interface RegistrationResponse extends ManagementErrorBody {\n client_id?: string;\n client_secret?: string;\n token_endpoint_auth_method?: string;\n registration_access_token?: string;\n registration_client_uri?: string;\n }\n\n const body = await readJson<RegistrationResponse>(response, \"Client registration\");\n if (!body.client_id) {\n throw new Error(`[incorta-auth] Client registration failed: response is missing client_id.`);\n }\n\n return {\n clientId: body.client_id,\n clientSecret: body.client_secret || undefined,\n tokenEndpointAuthMethod: body.token_endpoint_auth_method,\n registrationAccessToken: body.registration_access_token,\n registrationClientUri: body.registration_client_uri,\n };\n}\n\nexport interface ManageClientOptions {\n /** Incorta base URL, e.g. `http://localhost:8080/incorta`. */\n incortaUrl: string;\n tenant: string;\n clientId: string;\n /** The registration access token returned when the client was registered. */\n registrationAccessToken: string;\n fetchImpl?: typeof fetch;\n}\n\n/** Current metadata of a registered client. */\nexport interface ClientInfo {\n clientId: string;\n name?: string;\n redirectUris: string[];\n grantTypes: string[];\n scope?: string;\n}\n\ninterface ClientMetadataResponse extends ManagementErrorBody {\n client_id?: string;\n client_name?: string;\n redirect_uris?: string[];\n grant_types?: string[];\n scope?: string;\n}\n\nfunction toClientInfo(body: ClientMetadataResponse, fallbackClientId: string): ClientInfo {\n return {\n clientId: body.client_id ?? fallbackClientId,\n name: body.client_name,\n redirectUris: body.redirect_uris ?? [],\n grantTypes: body.grant_types ?? [],\n scope: body.scope,\n };\n}\n\n/**\n * Reads a registered client's current metadata (RFC 7592-style management,\n * `GET {incortaUrl}/oauth/{tenant}/register/{clientId}` with the registration\n * access token).\n */\nexport async function getClient(options: ManageClientOptions): Promise<ClientInfo> {\n const { incortaUrl, tenant, clientId, registrationAccessToken, fetchImpl = fetch } = options;\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n });\n const body = await readJson<ClientMetadataResponse>(response, \"Reading the client\");\n return toClientInfo(body, clientId);\n}\n\nexport interface UpdateRedirectUrisOptions extends ManageClientOptions {\n redirectUris: string[];\n /**\n * `\"add\"` (default) merges the given URIs into the client's current set;\n * `\"replace\"` makes the given list the exact new set (also how URIs are\n * removed).\n */\n mode?: \"add\" | \"replace\";\n}\n\n/**\n * Updates the allowed OAuth callback URLs of a registered client. Incorta's\n * management endpoint applies `redirect_uris` from the PUT body (other\n * metadata is unchanged server-side).\n */\nexport async function updateClientRedirectUris(\n options: UpdateRedirectUrisOptions,\n): Promise<ClientInfo> {\n const {\n incortaUrl,\n tenant,\n clientId,\n registrationAccessToken,\n redirectUris,\n mode = \"add\",\n fetchImpl = fetch,\n } = options;\n\n if (!redirectUris.length) {\n // An empty replace would leave the client unable to complete the\n // authorization-code flow at all — delete the registration instead.\n throw new Error(\"[incorta-auth] updateClientRedirectUris requires at least one redirect URI.\");\n }\n\n const current = await getClient(options);\n const nextUris =\n mode === \"add\" ? [...new Set([...current.redirectUris, ...redirectUris])] : redirectUris;\n\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n method: \"PUT\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n // RFC 7592 update: client_id + full metadata. Incorta applies\n // redirect_uris only, but nimbus-side parsing wants the full shape.\n body: JSON.stringify({\n client_id: clientId,\n redirect_uris: nextUris,\n grant_types: current.grantTypes,\n scope: current.scope,\n client_name: current.name,\n }),\n });\n\n if (!response.ok) {\n await readJson<ManagementErrorBody>(response, \"Updating the client\"); // throws with details\n }\n // The PUT response body is empty on success — re-read for the fresh state.\n return getClient(options);\n}\n\n/**\n * Deletes a registered client (RFC 7592-style management,\n * `DELETE {incortaUrl}/oauth/{tenant}/register/{clientId}` with the\n * registration access token). Existing sessions in apps using the client keep\n * working until they expire; new logins and token refreshes stop immediately.\n */\nexport async function deleteClient(options: ManageClientOptions): Promise<void> {\n const { incortaUrl, tenant, clientId, registrationAccessToken, fetchImpl = fetch } = options;\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n method: \"DELETE\",\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n });\n if (!response.ok) {\n await readJson<ManagementErrorBody>(response, \"Deleting the client\"); // throws with details\n }\n}\n"],"mappings":"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","names":["import_node_crypto","import_node_crypto","import_jose","import_jose","code"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/config.ts","../src/core/handler.ts","../src/core/app-access.ts","../src/core/cookies.ts","../src/core/discovery.ts","../src/core/pkce.ts","../src/core/session.ts","../src/core/tokens.ts","../src/core/register.ts"],"sourcesContent":["import type { AppAccess } from \"./core/app-access.js\";\nimport { resolveConfig } from \"./core/config.js\";\nimport type { IncortaAuthConfig, ResolvedConfig } from \"./core/config.js\";\nimport { createHandler } from \"./core/handler.js\";\nimport type { AuthGate, AuthHandler, AuthSession } from \"./core/handler.js\";\n\nexport {\n AppAccessTokenRejectedError,\n AppAccessUnavailableError,\n EDIT_BIT,\n SHARE_BIT,\n VIEW_BIT,\n} from \"./core/app-access.js\";\nexport type { AppAccess, AppAccessPolicy } from \"./core/app-access.js\";\nexport type {\n IncortaAuthConfig,\n IncortaUser,\n SessionData,\n SessionInfo,\n} from \"./core/config.js\";\nexport type { AuthGate, AuthHandler, AuthSession } from \"./core/handler.js\";\nexport { IncortaAuthError } from \"./core/tokens.js\";\nexport {\n deleteClient,\n getClient,\n registerClient,\n updateClientRedirectUris,\n} from \"./core/register.js\";\nexport type {\n ClientInfo,\n ManageClientOptions,\n RegisterClientOptions,\n RegisteredClient,\n UpdateRedirectUrisOptions,\n} from \"./core/register.js\";\n\nexport interface IncortaAuth {\n /**\n * Web-standard request handler serving the auth routes under\n * `config.basePath` (default `/auth`): `/login`, `/callback`, `/session`,\n * `/logout` (+ `/token` when `exposeAccessToken` is on).\n *\n * Wire it to anything that speaks `Request`/`Response` — a TanStack Start\n * server route, Bun.serve, or via `@incorta/auth/node` / `@incorta/auth/express`\n * for Node servers.\n */\n handler: AuthHandler;\n /**\n * Reads the session from a web-standard `Request` (server-side gating, API\n * routes, server functions). Returns null when the user is not signed in.\n */\n getSession: (request: Request) => Promise<AuthSession | null>;\n /**\n * The whole-app gate: serve the returned `Response` when non-null (auth\n * routes, login redirects, access-denied/unavailable pages), continue to\n * your app on `null`. Enforces `config.appAccess` — `\"public\"` lets every\n * request through, `\"authenticated\"` requires a session, `\"catalog\"`\n * additionally requires at least VIEW access to this app in the Incorta\n * catalog (fails CLOSED with a 503 page when Incorta cannot answer).\n */\n gate: AuthGate;\n /**\n * The Incorta catalog's verdict about `session`'s user and THIS app —\n * registration, permission bits, and the gate's `allowed` outcome. Served\n * from a 30s per-user cache (the same one `gate` uses in `\"catalog\"`\n * mode). Throws `AppAccessUnavailableError` when Incorta cannot answer and\n * `AppAccessTokenRejectedError` when it rejects the session token. Useful\n * for permission-aware UX (e.g. hiding editor-only affordances) in any\n * `appAccess` mode.\n */\n getAppAccess: (session: AuthSession) => Promise<AppAccess>;\n /** The resolved configuration (secrets included — do not log). */\n config: ResolvedConfig;\n}\n\n/**\n * Creates an IncortaAuth instance: OAuth 2.0 authorization-code flow against\n * Incorta's built-in authorization server. Unauthenticated users are redirected\n * to the Incorta login page and returned to your app once signed in.\n */\nexport function createIncortaAuth(config: IncortaAuthConfig = {}): IncortaAuth {\n const resolved = resolveConfig(config);\n const { handler, getSession, gate, getAppAccess } = createHandler(resolved);\n return { handler, getSession, gate, getAppAccess, config: resolved };\n}\n","import type { JWTVerifyGetKey } from \"jose\";\nimport type { AppAccessPolicy } from \"./app-access.js\";\n\n/** The Incorta user identity carried by the session (claims from the ID token). */\nexport interface IncortaUser {\n /** Incorta login name (`sub` claim). */\n sub: string;\n name?: string;\n email?: string;\n /** Incorta role names, as minted into the ID token. */\n roles: string[];\n /** Tenant the user authenticated against. */\n tenant: string;\n}\n\n/** What the sealed session cookie holds. */\nexport interface SessionData {\n user: IncortaUser;\n /** Incorta-issued access token (RS256 JWT, aud = client_id). */\n accessToken: string;\n /** Epoch millis when the access token expires. */\n accessTokenExpiresAt: number;\n /** Rotating refresh token, present when the client has the refresh_token grant. */\n refreshToken?: string;\n /** Epoch millis when this session was first created (login time). */\n createdAt: number;\n}\n\nexport interface SessionInfo {\n user: IncortaUser;\n /** Epoch millis when the session cookie expires (sliding). */\n expiresAt: number;\n /** Epoch millis when the access token expires. */\n accessTokenExpiresAt: number;\n}\n\nexport interface IncortaAuthConfig {\n /**\n * Base URL of the Incorta web application, e.g. `https://acme.cloud.incorta.com/incorta`\n * or `http://localhost:8080/incorta`. Defaults to `INCORTA_URL`.\n */\n incortaUrl?: string;\n /**\n * Split-horizon override: base URL this SERVER should use to reach Incorta\n * when the public `incortaUrl` is not routable from where the app runs\n * (containers, cluster-internal DNS) — e.g.\n * `http://host.docker.internal:8080/incorta` from a local container. Only\n * server-to-server calls (discovery, token exchange, JWKS) use it; browser\n * redirects and token `iss` validation stay on the public `incortaUrl`.\n * Defaults to `INCORTA_INTERNAL_URL`; unset means no rewrite.\n */\n internalIncortaUrl?: string;\n /** Incorta tenant name, e.g. `demo`. Defaults to `INCORTA_TENANT`. */\n tenant?: string;\n /** OAuth client id (see `incorta-auth register`). Defaults to `INCORTA_CLIENT_ID`. */\n clientId?: string;\n /**\n * OAuth client secret — only for a CONFIDENTIAL client. Incorta's dynamic\n * registration issues app clients as PUBLIC clients (no secret; see\n * `registerClient`), and the SDK then authenticates token requests with\n * `client_id` alone and relies on PKCE. Defaults to `INCORTA_CLIENT_SECRET`;\n * unset or empty means public.\n */\n clientSecret?: string;\n /**\n * Secret used to encrypt the session cookie (any long random string, >= 32 chars).\n * Defaults to `INCORTA_AUTH_SECRET`.\n */\n secret?: string;\n /**\n * Path prefix the auth routes are mounted under. The SDK serves\n * `{basePath}/login`, `{basePath}/callback`, `{basePath}/session`,\n * `{basePath}/logout` (and optionally `{basePath}/token`).\n * @default \"/auth\"\n */\n basePath?: string;\n /**\n * External origin of THIS app, e.g. `http://localhost:4000`. Used to build the\n * OAuth callback URL and to validate `redirect_to` targets. When omitted it is\n * derived per request from `x-forwarded-proto`/`x-forwarded-host`/`host`.\n * Defaults to `INCORTA_APP_URL`.\n */\n appUrl?: string;\n /** OAuth scopes to request. @default \"openid profile email\" */\n scope?: string;\n session?: {\n /** @default \"incorta_auth_session\" */\n cookieName?: string;\n /** Session lifetime in seconds (sliding — renewed on activity). @default 43200 (12h) */\n maxAge?: number;\n };\n cookies?: {\n /**\n * `Secure` attribute for cookies. `\"auto\"` sets it when the request is HTTPS.\n * @default \"auto\"\n */\n secure?: boolean | \"auto\";\n /**\n * `SameSite` for the session/state cookies. The `\"lax\"` default is right\n * for top-level apps. Set `\"none\"` when the app runs inside a CROSS-SITE\n * iframe (e.g. an embedded preview) — Lax cookies are never sent there,\n * which loops the login flow forever. `\"none\"` forces the `Secure`\n * attribute (browsers require the pair; Chrome still accepts it on\n * trustworthy plain-http origins like `*.localhost`). Defaults to\n * `INCORTA_COOKIE_SAMESITE`.\n * @default \"lax\"\n */\n sameSite?: \"lax\" | \"none\";\n /**\n * CHIPS: adds the `Partitioned` attribute, giving each embedding\n * top-level site its own cookie jar — what modern Chrome requires for\n * cookies in third-party iframes. Use together with `sameSite: \"none\"`.\n * Defaults to `INCORTA_COOKIE_PARTITIONED` (`1`/`true`).\n * @default false\n */\n partitioned?: boolean;\n };\n /**\n * Expose `GET {basePath}/token` returning the raw Incorta access token to the\n * browser (for calling Incorta public APIs directly from the frontend). Off by\n * default — prefer proxying through your backend.\n * @default false\n */\n exposeAccessToken?: boolean;\n /**\n * Who may use the app — enforced by the whole-app gate (`auth.gate`, and\n * the framework middlewares built on it):\n *\n * - `\"public\"`: anyone with the URL, signed in or not (login stays\n * available, sessions become optional);\n * - `\"authenticated\"` (default): any signed-in Incorta user;\n * - `\"catalog\"`: signed-in AND at least VIEW access to this app in the\n * Incorta catalog, verified against `/apps-catalog/check` with a 30s\n * per-user cache. Unregistered apps stay open to authenticated users\n * (legacy-open); an unanswerable check fails CLOSED (503).\n *\n * Normally stamped by the Incorta Apps platform via `INCORTA_APP_ACCESS`.\n * An unrecognized env value falls back to `\"authenticated\"` (never to\n * `\"public\"` — a typo must not open the app) with a console warning.\n */\n appAccess?: AppAccessPolicy;\n /**\n * How to sign in when the app runs inside a cross-site iframe (the Incorta\n * builder preview). `\"redirect\"` (default) bounces the frame to Incorta —\n * correct top-level, but when the Incorta cluster has iframe inclusion OFF\n * (its login is `X-Frame-Options: SAMEORIGIN` and its session cookie is\n * `SameSite=Lax`) the login can't render/complete in the frame. `\"new-tab\"`\n * serves an interstitial that, inside a frame, offers a \"Sign in with\n * Incorta\" button opening the app top-level in a new tab (where login\n * works); at the top level it still redirects straight to Incorta.\n * Env fallback: `INCORTA_EMBEDDED_SIGN_IN`.\n * @default \"redirect\"\n */\n embeddedSignIn?: \"redirect\" | \"new-tab\";\n advanced?: {\n /** Override the OIDC discovery document URL (rarely needed). */\n discoveryUrl?: string;\n /** Override JWKS resolution (used by tests to verify against a local key). */\n jwks?: JWTVerifyGetKey;\n /** Override fetch (used by tests). */\n fetch?: typeof fetch;\n };\n}\n\nexport interface ResolvedConfig {\n incortaUrl: string;\n /** Server-side fetch base when set (split horizon) — see the config doc. */\n internalIncortaUrl?: string;\n tenant: string;\n clientId: string;\n /** Absent for a public client: token requests then carry `client_id` instead of Basic auth. */\n clientSecret?: string;\n secret: string;\n basePath: string;\n appUrl?: string;\n scope: string;\n cookieName: string;\n stateCookieName: string;\n sessionMaxAge: number;\n secureCookies: boolean | \"auto\";\n cookieSameSite: \"lax\" | \"none\";\n cookiePartitioned: boolean;\n exposeAccessToken: boolean;\n appAccess: AppAccessPolicy;\n embeddedSignIn: \"redirect\" | \"new-tab\";\n /** `{incortaUrl}/oauth/{tenant}` — the OIDC issuer for this tenant. */\n issuer: string;\n discoveryUrl: string;\n jwksOverride?: JWTVerifyGetKey;\n fetch: typeof fetch;\n}\n\nfunction resolveAppAccess(\n input: AppAccessPolicy | undefined,\n envValue: string | undefined,\n): AppAccessPolicy {\n if (input) return input;\n const raw = envValue?.trim().toLowerCase();\n if (!raw) return \"authenticated\";\n if (raw === \"public\" || raw === \"authenticated\" || raw === \"catalog\") return raw;\n // Fail SAFE, not open: garbage in the platform-stamped env keeps the\n // login-only posture rather than silently exposing or bricking the app.\n console.warn(\n `[incorta-auth] Unrecognized INCORTA_APP_ACCESS value \"${envValue}\" — using \"authenticated\".`,\n );\n return \"authenticated\";\n}\n\nfunction required(value: string | undefined, name: string, envVar: string): string {\n if (!value) {\n throw new Error(\n `[incorta-auth] Missing required config \"${name}\" (or env var ${envVar}).`,\n );\n }\n return value;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nexport function resolveConfig(input: IncortaAuthConfig = {}): ResolvedConfig {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env ?? {};\n\n const incortaUrl = stripTrailingSlash(\n required(input.incortaUrl ?? env.INCORTA_URL, \"incortaUrl\", \"INCORTA_URL\"),\n );\n const internalIncortaUrl = input.internalIncortaUrl ?? env.INCORTA_INTERNAL_URL;\n const tenant = required(input.tenant ?? env.INCORTA_TENANT, \"tenant\", \"INCORTA_TENANT\");\n const clientId = required(\n input.clientId ?? env.INCORTA_CLIENT_ID,\n \"clientId\",\n \"INCORTA_CLIENT_ID\",\n );\n // Optional: a public client (Incorta's default for registered apps) has none.\n const clientSecret = (input.clientSecret ?? env.INCORTA_CLIENT_SECRET)?.trim() || undefined;\n const secret = required(\n input.secret ?? env.INCORTA_AUTH_SECRET,\n \"secret\",\n \"INCORTA_AUTH_SECRET\",\n );\n if (secret.length < 32) {\n throw new Error(\n \"[incorta-auth] `secret` must be at least 32 characters. Generate one with: openssl rand -base64 32\",\n );\n }\n\n const basePath = input.basePath ?? \"/auth\";\n if (!basePath.startsWith(\"/\") || basePath.endsWith(\"/\")) {\n throw new Error(\n `[incorta-auth] basePath must start with \"/\" and not end with \"/\" (got \"${basePath}\").`,\n );\n }\n\n const appUrl = input.appUrl ?? env.INCORTA_APP_URL;\n const cookieName = input.session?.cookieName ?? \"incorta_auth_session\";\n const issuer = `${incortaUrl}/oauth/${tenant}`;\n\n const envSameSite = env.INCORTA_COOKIE_SAMESITE?.toLowerCase();\n const cookieSameSite =\n input.cookies?.sameSite ??\n (envSameSite === \"none\" || envSameSite === \"lax\" ? envSameSite : \"lax\");\n const envPartitioned = env.INCORTA_COOKIE_PARTITIONED?.toLowerCase();\n const cookiePartitioned =\n input.cookies?.partitioned ??\n (envPartitioned === \"1\" || envPartitioned === \"true\");\n if (cookiePartitioned && cookieSameSite !== \"none\") {\n throw new Error(\n '[incorta-auth] `cookies.partitioned` requires `cookies.sameSite: \"none\"` — browsers only partition cross-site cookies.',\n );\n }\n\n return {\n incortaUrl,\n internalIncortaUrl: internalIncortaUrl\n ? stripTrailingSlash(internalIncortaUrl)\n : undefined,\n tenant,\n clientId,\n clientSecret,\n secret,\n basePath,\n appUrl: appUrl ? stripTrailingSlash(appUrl) : undefined,\n scope: input.scope ?? \"openid profile email\",\n cookieName,\n stateCookieName: `${cookieName}_state`,\n sessionMaxAge: input.session?.maxAge ?? 12 * 60 * 60,\n secureCookies: input.cookies?.secure ?? \"auto\",\n cookieSameSite,\n cookiePartitioned,\n exposeAccessToken: input.exposeAccessToken ?? false,\n appAccess: resolveAppAccess(input.appAccess, env.INCORTA_APP_ACCESS),\n embeddedSignIn:\n input.embeddedSignIn ??\n (env.INCORTA_EMBEDDED_SIGN_IN?.toLowerCase() === \"new-tab\"\n ? \"new-tab\"\n : \"redirect\"),\n issuer,\n discoveryUrl:\n input.advanced?.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`,\n jwksOverride: input.advanced?.jwks,\n fetch: input.advanced?.fetch ?? fetch,\n };\n}\n","import { randomBytes } from \"node:crypto\";\nimport {\n type AppAccess,\n AppAccessTokenRejectedError,\n AppAccessUnavailableError,\n createAppAccessChecker,\n} from \"./app-access.js\";\nimport type { ResolvedConfig, SessionData, SessionInfo } from \"./config.js\";\nimport { deleteCookie, parseCookies, serializeCookie } from \"./cookies.js\";\nimport { getEndpoints } from \"./discovery.js\";\nimport { CODE_CHALLENGE_METHOD, codeChallengeFor, generateCodeVerifier } from \"./pkce.js\";\nimport {\n sealLoginState,\n sealSession,\n unsealLoginState,\n unsealSession,\n} from \"./session.js\";\nimport {\n IncortaAuthError,\n exchangeCode,\n refreshTokens,\n userFromAccessToken,\n verifyIdToken,\n} from \"./tokens.js\";\n\n/** A read-only view of the current session for server-side app code. */\nexport interface AuthSession extends SessionInfo {\n /** Incorta access token — pass as `Authorization: Bearer` to Incorta APIs. */\n accessToken: string;\n /**\n * The catalog verdict for this user and app, when a gate already resolved\n * it on this request (`appAccess: \"catalog\"` policy). Absent otherwise —\n * fetch on demand with `auth.getAppAccess(session)`.\n */\n appAccess?: AppAccess;\n}\n\nexport type AuthHandler = (request: Request) => Promise<Response>;\n\n/**\n * Whole-app gate: a `Response` to short-circuit with (auth routes, login\n * redirects, deny/unavailable pages) or `null` for \"let the request through\n * to the app\".\n */\nexport type AuthGate = (request: Request) => Promise<Response | null>;\n\n/** Refresh the access token when it has less than this long left to live. */\nconst ACCESS_TOKEN_REFRESH_LEEWAY_MS = 60 * 1000;\n\nfunction requestOrigin(config: ResolvedConfig, request: Request): string {\n if (config.appUrl) return config.appUrl;\n const url = new URL(request.url);\n const proto =\n request.headers.get(\"x-forwarded-proto\")?.split(\",\")[0]?.trim() ??\n url.protocol.replace(\":\", \"\");\n const host =\n request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim() ??\n request.headers.get(\"host\") ??\n url.host;\n return `${proto}://${host}`;\n}\n\nfunction isSecureRequest(config: ResolvedConfig, request: Request): boolean {\n if (config.secureCookies !== \"auto\") return config.secureCookies;\n return requestOrigin(config, request).startsWith(\"https://\");\n}\n\n/**\n * Only same-origin destinations are allowed after login/logout — anything else\n * would let a crafted link bounce users through this app to an attacker page.\n */\nfunction validateReturnTo(raw: string | null, origin: string): string {\n if (!raw) return \"/\";\n if (raw.startsWith(\"/\") && !raw.startsWith(\"//\")) return raw;\n try {\n const url = new URL(raw);\n if (url.origin === origin) return `${url.pathname}${url.search}${url.hash}` || \"/\";\n } catch {\n // Not a URL — fall through.\n }\n return \"/\";\n}\n\nfunction json(status: number, body: unknown, cookies: string[] = []): Response {\n const headers = new Headers({\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(JSON.stringify(body), { status, headers });\n}\n\nfunction redirect(location: string, cookies: string[] = []): Response {\n const headers = new Headers({ location, \"cache-control\": \"no-store\" });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(null, { status: 302, headers });\n}\n\nfunction withErrorParam(target: string, code: string): string {\n const separator = target.includes(\"?\") ? \"&\" : \"?\";\n return `${target}${separator}incorta_auth_error=${encodeURIComponent(code)}`;\n}\n\nfunction randomToken(): string {\n return randomBytes(16).toString(\"base64url\");\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/** Browser navigations get pages/redirects; data requests get JSON. */\nfunction wantsHtml(request: Request): boolean {\n return (\n request.method.toUpperCase() === \"GET\" &&\n (request.headers.get(\"accept\") ?? \"\").includes(\"text/html\")\n );\n}\n\nfunction requestPath(url: URL): string {\n return `${url.pathname}${url.search}`;\n}\n\nexport function createHandler(config: ResolvedConfig): {\n handler: AuthHandler;\n getSession: (request: Request) => Promise<AuthSession | null>;\n gate: AuthGate;\n getAppAccess: (session: AuthSession) => Promise<AppAccess>;\n} {\n const appAccessChecker = createAppAccessChecker(config);\n\n const sessionCookieOptions = (secure: boolean) => ({\n httpOnly: true,\n // SameSite=None requires Secure — force the pair even on plain http\n // (Chrome accepts Secure cookies from trustworthy origins like\n // `*.localhost`; real cross-site embedding needs https anyway).\n secure: config.cookieSameSite === \"none\" ? true : secure,\n sameSite: config.cookieSameSite,\n partitioned: config.cookiePartitioned,\n path: \"/\",\n });\n\n async function login(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const secure = isSecureRequest(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n\n // Already signed in — skip the OAuth round trip entirely.\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const existing = await unsealSession(config, cookies[config.cookieName]);\n if (existing) return redirect(returnTo);\n\n const state = randomToken();\n const nonce = randomToken();\n // PKCE binds the code to this server (pkce.ts) — the verifier stays in the\n // sealed cookie, only its challenge goes to Incorta.\n const codeVerifier = generateCodeVerifier();\n const stateCookie = serializeCookie(\n config.stateCookieName,\n await sealLoginState(config, { state, nonce, returnTo, codeVerifier }),\n { ...sessionCookieOptions(secure), maxAge: 600 },\n );\n\n const { authorizationEndpoint } = await getEndpoints(config);\n const authorizeUrl = new URL(authorizationEndpoint);\n authorizeUrl.searchParams.set(\"response_type\", \"code\");\n authorizeUrl.searchParams.set(\"client_id\", config.clientId);\n authorizeUrl.searchParams.set(\"redirect_uri\", `${origin}${config.basePath}/callback`);\n authorizeUrl.searchParams.set(\"scope\", config.scope);\n authorizeUrl.searchParams.set(\"state\", state);\n authorizeUrl.searchParams.set(\"nonce\", nonce);\n authorizeUrl.searchParams.set(\"code_challenge\", codeChallengeFor(codeVerifier));\n authorizeUrl.searchParams.set(\"code_challenge_method\", CODE_CHALLENGE_METHOD);\n\n // Embedded apps that can't complete Incorta login in a cross-site iframe\n // get a framing-aware interstitial instead of a doomed in-frame redirect.\n if (config.embeddedSignIn === \"new-tab\") {\n return newTabSignInPage(authorizeUrl.toString(), `${origin}/`, [stateCookie]);\n }\n return redirect(authorizeUrl.toString(), [stateCookie]);\n }\n\n async function callback(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const secure = isSecureRequest(config, request);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const clearState = deleteCookie(config.stateCookieName, sessionCookieOptions(secure));\n\n const loginState = await unsealLoginState(config, cookies[config.stateCookieName]);\n const returnTo = validateReturnTo(loginState?.returnTo ?? null, origin);\n\n const fail = (code: string) => redirect(withErrorParam(returnTo, code), [clearState]);\n\n const oauthError = url.searchParams.get(\"error\");\n if (oauthError) return fail(oauthError);\n\n const code = url.searchParams.get(\"code\");\n const state = url.searchParams.get(\"state\");\n if (!code || !state) return fail(\"missing_code\");\n if (!loginState) return fail(\"state_missing\");\n if (state !== loginState.state) return fail(\"state_mismatch\");\n\n try {\n const tokens = await exchangeCode(\n config,\n code,\n `${origin}${config.basePath}/callback`,\n loginState.codeVerifier,\n );\n const user = tokens.idToken\n ? await verifyIdToken(config, tokens.idToken, loginState.nonce)\n : await userFromAccessToken(config, tokens.accessToken);\n\n const session: SessionData = {\n user,\n accessToken: tokens.accessToken,\n accessTokenExpiresAt: tokens.accessTokenExpiresAt,\n refreshToken: tokens.refreshToken,\n createdAt: Date.now(),\n };\n const sessionCookie = serializeCookie(\n config.cookieName,\n await sealSession(config, session),\n { ...sessionCookieOptions(secure), maxAge: config.sessionMaxAge },\n );\n return redirect(returnTo, [sessionCookie, clearState]);\n } catch (error) {\n const code = error instanceof IncortaAuthError ? error.code : \"callback_failed\";\n return fail(code);\n }\n }\n\n /**\n * Loads the session from the request cookie; refreshes the Incorta access\n * token (rotating the refresh token) when it is about to expire, and renews\n * the sliding session cookie past its half-life. Returns the cookies to set.\n */\n async function loadSession(request: Request): Promise<{\n session: SessionData | null;\n sessionExpiresAt: number;\n setCookies: string[];\n }> {\n const secure = isSecureRequest(config, request);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unsealed = await unsealSession(config, cookies[config.cookieName]);\n if (!unsealed) return { session: null, sessionExpiresAt: 0, setCookies: [] };\n\n let { session } = unsealed;\n let mustReseal = false;\n const now = Date.now();\n\n if (session.accessTokenExpiresAt - now < ACCESS_TOKEN_REFRESH_LEEWAY_MS) {\n if (session.refreshToken) {\n try {\n const tokens = await refreshTokens(config, session.refreshToken);\n session = {\n ...session,\n accessToken: tokens.accessToken,\n accessTokenExpiresAt: tokens.accessTokenExpiresAt,\n refreshToken: tokens.refreshToken ?? session.refreshToken,\n };\n mustReseal = true;\n } catch {\n // Refresh failed (expired/rotated elsewhere). Keep the session while\n // the current access token is still usable; otherwise sign out.\n if (session.accessTokenExpiresAt <= now) {\n return {\n session: null,\n sessionExpiresAt: 0,\n setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))],\n };\n }\n }\n } else if (session.accessTokenExpiresAt <= now) {\n return {\n session: null,\n sessionExpiresAt: 0,\n setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))],\n };\n }\n }\n\n const halfLifeMs = (config.sessionMaxAge * 1000) / 2;\n if (now - unsealed.issuedAt > halfLifeMs) mustReseal = true;\n\n if (!mustReseal) {\n return { session, sessionExpiresAt: unsealed.expiresAt, setCookies: [] };\n }\n const resealed = serializeCookie(config.cookieName, await sealSession(config, session), {\n ...sessionCookieOptions(secure),\n maxAge: config.sessionMaxAge,\n });\n return {\n session,\n sessionExpiresAt: now + config.sessionMaxAge * 1000,\n setCookies: [resealed],\n };\n }\n\n async function sessionRoute(request: Request): Promise<Response> {\n const { session, sessionExpiresAt, setCookies } = await loadSession(request);\n if (!session) return json(200, { user: null }, setCookies);\n return json(\n 200,\n {\n user: session.user,\n session: {\n expiresAt: sessionExpiresAt,\n accessTokenExpiresAt: session.accessTokenExpiresAt,\n },\n },\n setCookies,\n );\n }\n\n async function tokenRoute(request: Request): Promise<Response> {\n if (!config.exposeAccessToken) return json(404, { error: \"not_found\" });\n const { session, setCookies } = await loadSession(request);\n if (!session) return json(401, { error: \"unauthenticated\" }, setCookies);\n return json(\n 200,\n { accessToken: session.accessToken, expiresAt: session.accessTokenExpiresAt },\n setCookies,\n );\n }\n\n /**\n * `GET {basePath}/refresh?redirect_to=…` — refresh the access token (and\n * reseal the rotated cookie) via a browser round trip, then bounce back.\n * The catalog gate sends navigations here when the session's access token\n * has already expired: the gate itself returns `null` to continue, so it\n * has no response to carry a rotated refresh-token cookie on — a redirect\n * does. A session that cannot be refreshed is sent to login instead\n * (loadSession already cleared its cookie).\n */\n async function refreshRoute(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n const { session, setCookies } = await loadSession(request);\n if (!session) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(returnTo)}`,\n setCookies,\n );\n }\n return redirect(returnTo, setCookies);\n }\n\n async function logout(request: Request, url: URL): Promise<Response> {\n const secure = isSecureRequest(config, request);\n const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));\n if (request.method === \"GET\") {\n // No explicit redirect_to → the signed-out page, NOT \"/\": in an\n // SSO-gated app, landing on a gated page silently re-runs the login\n // flow and (while the Incorta session lives) signs the user straight\n // back in — sign-out must land somewhere UNGATED to visibly stick.\n const raw = url.searchParams.get(\"redirect_to\");\n if (!raw) return redirect(`${config.basePath}/signed-out`, [clearSession]);\n const origin = requestOrigin(config, request);\n return redirect(validateReturnTo(raw, origin), [clearSession]);\n }\n return json(200, { ok: true }, [clearSession]);\n }\n\n /**\n * Ungated landing page after sign-out (it lives under basePath, so app\n * gates pass it through). Without it there is nowhere for a fully-gated\n * app to show \"you are signed out\" — every app page would bounce through\n * the IdP and re-login. Self-contained HTML, no assets.\n */\n function signedOutPage(): Response {\n const signInHref = `${config.basePath}/login?redirect_to=%2F`;\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Signed out</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n a { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; font-size: .9rem;\n text-decoration: none; }\n </style>\n </head>\n <body>\n <main>\n <h1>You’ve signed out of this app</h1>\n <p>\n Your Incorta account may still be signed in elsewhere — signing back\n in here is one click.\n </p>\n <a href=\"${signInHref}\">Sign in with Incorta</a>\n </main>\n </body>\n</html>`,\n {\n status: 200,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n },\n );\n }\n\n /**\n * `embeddedSignIn: \"new-tab\"` interstitial. The inline script decides\n * client-side: at the top level it redirects straight to Incorta; inside a\n * cross-site iframe (where Incorta login can't render/complete when the\n * cluster has iframe inclusion off) it reveals a button that opens the app\n * in a new top-level tab. Carries the same state cookie the plain redirect\n * would set — used by the top-level path; harmless in the frame, where the\n * new tab writes its own first-party state cookie.\n */\n function newTabSignInPage(\n authorizeUrl: string,\n appRoot: string,\n cookies: string[],\n ): Response {\n const body = `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Sign in with Incorta</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.2rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n a.btn { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; text-decoration: none;\n font-weight: 600; }\n </style>\n </head>\n <body>\n <main id=\"frame\" hidden>\n <h1>Sign in with Incorta</h1>\n <p>\n This app signs you in with your Incorta account. The embedded preview\n can’t complete sign-in here — continue in a new tab.\n </p>\n <a class=\"btn\" href=\"${appRoot}\" target=\"_blank\" rel=\"noopener noreferrer\">Sign in with Incorta ↗</a>\n </main>\n <script>\n (function () {\n var authorize = ${JSON.stringify(authorizeUrl)};\n if (window.top === window.self) {\n window.location.replace(authorize);\n } else {\n document.getElementById(\"frame\").hidden = false;\n }\n })();\n </script>\n <noscript><a href=\"${appRoot}\">Continue to sign in</a></noscript>\n </body>\n</html>`;\n const headers = new Headers({\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(body, { status: 200, headers });\n }\n\n /**\n * `appAccess: \"catalog\"` — the user is signed in but the catalog says no:\n * an honest 403 with who they are and a way out (the wrong-account case is\n * the most common real cause). Non-HTML requests get the JSON twin.\n */\n function accessDeniedResponse(request: Request, session: AuthSession): Response {\n if (!wantsHtml(request)) {\n return json(403, { error: \"app_access_denied\" });\n }\n const identity = escapeHtml(session.user.name?.trim() || session.user.sub);\n const detail =\n session.user.name?.trim() && session.user.name.trim() !== session.user.sub\n ? `${identity} (${escapeHtml(session.user.sub)})`\n : identity;\n const switchHref = `${config.basePath}/logout`;\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>No access</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n strong { color: #18181b; font-weight: 600; }\n a { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; font-size: .9rem;\n text-decoration: none; }\n </style>\n </head>\n <body>\n <main>\n <h1>You don’t have access to this application</h1>\n <p>\n Ask the app’s owner to share it with you in Incorta.\n You are signed in as <strong>${detail}</strong>.\n </p>\n <a href=\"${switchHref}\">Sign in with a different account</a>\n </main>\n </body>\n</html>`,\n {\n status: 403,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n },\n );\n }\n\n /**\n * The catalog could not answer (Incorta unreachable/failing) — deny, but\n * make clear this is an outage, not a sharing decision. Distinct from the\n * 403 page so users never conclude they were unshared. Auto-retries: the\n * verdict cache holds no failures, so the next load re-asks.\n */\n function accessUnavailableResponse(request: Request): Response {\n if (!wantsHtml(request)) {\n return json(503, { error: \"app_access_unavailable\" });\n }\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta http-equiv=\"refresh\" content=\"15\" />\n <title>Temporarily unavailable</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0; }\n </style>\n </head>\n <body>\n <main>\n <h1>This app is temporarily unavailable</h1>\n <p>\n Your access could not be verified because Incorta did not respond.\n This page retries automatically — you can also refresh in a\n moment.\n </p>\n </main>\n </body>\n</html>`,\n {\n status: 503,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n \"retry-after\": \"30\",\n },\n },\n );\n }\n\n async function handler(request: Request): Promise<Response> {\n const url = new URL(request.url);\n if (url.pathname !== config.basePath && !url.pathname.startsWith(`${config.basePath}/`)) {\n return json(404, { error: \"not_found\" });\n }\n const route = url.pathname.slice(config.basePath.length) || \"/\";\n const method = request.method.toUpperCase();\n\n try {\n if (route === \"/login\" && method === \"GET\") return await login(request, url);\n if (route === \"/callback\" && method === \"GET\") return await callback(request, url);\n if (route === \"/session\" && method === \"GET\") return await sessionRoute(request);\n if (route === \"/token\" && method === \"GET\") return await tokenRoute(request);\n if (route === \"/refresh\" && method === \"GET\") return await refreshRoute(request, url);\n if (route === \"/logout\" && (method === \"POST\" || method === \"GET\")) {\n return await logout(request, url);\n }\n if (route === \"/signed-out\" && method === \"GET\") return signedOutPage();\n return json(404, { error: \"not_found\" });\n } catch (error) {\n const code = error instanceof IncortaAuthError ? error.code : \"internal_error\";\n const message =\n error instanceof Error ? error.message : \"Unexpected incorta-auth handler error\";\n console.error(`[incorta-auth] ${route} failed:`, message);\n return json(500, { error: code });\n }\n }\n\n async function getSession(request: Request): Promise<AuthSession | null> {\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unsealed = await unsealSession(config, cookies[config.cookieName]);\n if (!unsealed) return null;\n const { session, expiresAt } = unsealed;\n if (session.accessTokenExpiresAt <= Date.now() && !session.refreshToken) return null;\n return {\n user: session.user,\n accessToken: session.accessToken,\n accessTokenExpiresAt: session.accessTokenExpiresAt,\n expiresAt,\n };\n }\n\n /** The catalog verdict for this session's user, via the 30s per-user cache. */\n function getAppAccess(session: AuthSession): Promise<AppAccess> {\n return appAccessChecker.check(session.accessToken, session.user.sub);\n }\n\n /**\n * The whole-app gate, enforcing `config.appAccess` in front of every\n * request (see `IncortaAuthConfig.appAccess`). Auth routes are always\n * served — login stays reachable even for public apps. `null` means\n * \"continue to the app\"; in `\"public\"` mode that is every non-auth route,\n * signed in or not.\n */\n async function gate(request: Request): Promise<Response | null> {\n const url = new URL(request.url);\n if (url.pathname === config.basePath || url.pathname.startsWith(`${config.basePath}/`)) {\n return handler(request);\n }\n if (config.appAccess === \"public\") return null;\n\n const session = await getSession(request);\n if (!session) {\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`,\n );\n }\n return json(401, { error: \"unauthenticated\" });\n }\n if (config.appAccess !== \"catalog\") return null;\n\n // The check replays the session's access token to Incorta — an already\n // expired token would 401 there, so refresh it first via the browser\n // round trip (which is what persists the rotated cookie).\n if (session.accessTokenExpiresAt <= Date.now()) {\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/refresh?redirect_to=${encodeURIComponent(requestPath(url))}`,\n );\n }\n return json(401, { error: \"token_expired\" });\n }\n\n try {\n const verdict = await getAppAccess(session);\n if (verdict.allowed) return null;\n return accessDeniedResponse(request, session);\n } catch (error) {\n if (error instanceof AppAccessTokenRejectedError) {\n // Incorta no longer honors this session's token — sign out and let\n // the login flow mint a fresh one (SSO makes this a silent bounce).\n const secure = isSecureRequest(config, request);\n const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`,\n [clearSession],\n );\n }\n return json(401, { error: \"unauthenticated\" }, [clearSession]);\n }\n if (!(error instanceof AppAccessUnavailableError)) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(\"[incorta-auth] app access check failed unexpectedly:\", message);\n }\n // Verdicts unavailable — NEVER fail open.\n return accessUnavailableResponse(request);\n }\n }\n\n return { handler, getSession, gate, getAppAccess };\n}\n","import type { ResolvedConfig } from \"./config.js\";\n\n/**\n * Platform-stamped access policy for the app (`INCORTA_APP_ACCESS`):\n *\n * - `\"public\"` — anyone with the URL; the whole-app gate is inert\n * (login routes stay mounted, sessions are optional);\n * - `\"authenticated\"`— any signed-in Incorta user (the pre-policy behavior,\n * and the default when the platform stamped nothing);\n * - `\"catalog\"` — signed-in AND at least VIEW access to this app in the\n * Incorta catalog (`/apps-catalog/check`).\n */\nexport type AppAccessPolicy = \"public\" | \"authenticated\" | \"catalog\";\n\n/** Incorta permission bits (com.incorta.api.Action). */\nexport const VIEW_BIT = 1;\nexport const SHARE_BIT = 2;\nexport const EDIT_BIT = 4;\n\n/** The catalog's verdict about the session user's access to THIS app. */\nexport interface AppAccess {\n /**\n * The app is registered in the Incorta catalog. `false` means it predates\n * the catalog integration — the gate treats it as legacy-open (any\n * authenticated tenant user may enter).\n */\n registered: boolean;\n /** Effective Incorta permission bitmask (VIEW=1, SHARE=2, EDIT=4). */\n access: number;\n canView: boolean;\n canEdit: boolean;\n /** What the runtime gate enforces: unregistered (legacy-open) or ≥ VIEW. */\n allowed: boolean;\n}\n\n/** Incorta could not answer (unreachable, 5xx, disabled) — gates fail CLOSED. */\nexport class AppAccessUnavailableError extends Error {\n constructor(cause: string) {\n super(`[incorta-auth] App access check unavailable: ${cause}`);\n this.name = \"AppAccessUnavailableError\";\n }\n}\n\n/** Incorta rejected the session's access token (expired/revoked) — re-login. */\nexport class AppAccessTokenRejectedError extends Error {\n constructor() {\n super(\"[incorta-auth] Incorta rejected the session token during the app access check\");\n this.name = \"AppAccessTokenRejectedError\";\n }\n}\n\n/** Verdicts are cached (allow AND deny) this long per user. */\nexport const APP_ACCESS_CACHE_TTL_MS = 30_000;\nconst CHECK_TIMEOUT_MS = 5_000;\n/** Lazy prune threshold — entries are (user × 30s), so this is generous. */\nconst CACHE_MAX_ENTRIES = 5_000;\n\ninterface CacheEntry {\n verdict: AppAccess;\n expiresAt: number;\n}\n\nexport interface AppAccessChecker {\n /**\n * The catalog verdict for the user behind `accessToken`, served from the\n * per-user cache when fresh. Throws {@link AppAccessUnavailableError} when\n * Incorta cannot answer and {@link AppAccessTokenRejectedError} when it\n * rejects the token itself.\n */\n check(accessToken: string, userKey: string): Promise<AppAccess>;\n /** Test hook: drops all cached verdicts. */\n clearCache(): void;\n}\n\nfunction toVerdict(registered: boolean, access: number): AppAccess {\n const canView = (access & VIEW_BIT) === VIEW_BIT;\n return {\n registered,\n access,\n canView,\n canEdit: (access & EDIT_BIT) === EDIT_BIT,\n allowed: !registered || canView,\n };\n}\n\n/**\n * Client for `GET {incortaUrl}/apps-catalog/check` — the backend resolves\n * which app the token belongs to from its OAuth client (`software_id`), so\n * the request carries nothing but the user's own Bearer token. Server-side\n * call, so the split-horizon `internalIncortaUrl` wins when set.\n *\n * Verdicts (allow and deny alike) are cached per user for\n * {@link APP_ACCESS_CACHE_TTL_MS}; failures are NOT cached — the next\n * request retries, and callers fail closed in the meantime. Concurrent\n * checks for the same user share one in-flight request.\n */\nexport function createAppAccessChecker(config: ResolvedConfig): AppAccessChecker {\n const base = config.internalIncortaUrl ?? config.incortaUrl;\n const checkUrl = `${base}/apps-catalog/check`;\n const cache = new Map<string, CacheEntry>();\n const inFlight = new Map<string, Promise<AppAccess>>();\n\n function prune(now: number): void {\n if (cache.size < CACHE_MAX_ENTRIES) return;\n for (const [key, entry] of cache) {\n if (entry.expiresAt <= now) cache.delete(key);\n }\n }\n\n async function fetchVerdict(accessToken: string): Promise<AppAccess> {\n let response: Response;\n try {\n response = await config.fetch(checkUrl, {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${accessToken}`,\n },\n signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),\n });\n } catch (error) {\n throw new AppAccessUnavailableError((error as Error).message);\n }\n\n if (response.status === 401) throw new AppAccessTokenRejectedError();\n if (!response.ok) {\n // 404 covers both \"Incorta Apps disabled for the tenant\" and an older\n // Incorta without the /check command — verdicts are unavailable either\n // way, and gates never fail open.\n throw new AppAccessUnavailableError(`HTTP ${response.status}`);\n }\n\n let body: { registered?: unknown; access?: unknown };\n try {\n body = (await response.json()) as { registered?: unknown; access?: unknown };\n } catch {\n throw new AppAccessUnavailableError(\"invalid JSON response\");\n }\n if (typeof body.registered !== \"boolean\" || typeof body.access !== \"number\") {\n throw new AppAccessUnavailableError(\"malformed check response\");\n }\n return toVerdict(body.registered, body.access);\n }\n\n async function check(accessToken: string, userKey: string): Promise<AppAccess> {\n const now = Date.now();\n const cached = cache.get(userKey);\n if (cached && cached.expiresAt > now) return cached.verdict;\n\n const pending = inFlight.get(userKey);\n if (pending) return pending;\n\n const promise = fetchVerdict(accessToken)\n .then((verdict) => {\n prune(now);\n cache.set(userKey, { verdict, expiresAt: Date.now() + APP_ACCESS_CACHE_TTL_MS });\n return verdict;\n })\n .finally(() => {\n inFlight.delete(userKey);\n });\n inFlight.set(userKey, promise);\n return promise;\n }\n\n return {\n check,\n clearCache() {\n cache.clear();\n inFlight.clear();\n },\n };\n}\n","export interface CookieOptions {\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: \"lax\" | \"strict\" | \"none\";\n /** CHIPS `Partitioned` attribute — per-top-site cookie jar for cross-site\n * iframe embedding. Browsers require Secure alongside it. */\n partitioned?: boolean;\n path?: string;\n /** Seconds. 0 deletes the cookie. */\n maxAge?: number;\n}\n\nexport function parseCookies(header: string | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const name = part.slice(0, eq).trim();\n if (!name) continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"')) value = value.slice(1, -1);\n try {\n out[name] = decodeURIComponent(value);\n } catch {\n out[name] = value;\n }\n }\n return out;\n}\n\nexport function serializeCookie(\n name: string,\n value: string,\n options: CookieOptions = {},\n): string {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n parts.push(`Path=${options.path ?? \"/\"}`);\n if (options.maxAge !== undefined) {\n parts.push(`Max-Age=${Math.floor(options.maxAge)}`);\n // Expires for compatibility with clients that ignore Max-Age.\n parts.push(`Expires=${new Date(Date.now() + options.maxAge * 1000).toUTCString()}`);\n }\n if (options.httpOnly ?? true) parts.push(\"HttpOnly\");\n if (options.secure) parts.push(\"Secure\");\n const sameSite = options.sameSite ?? \"lax\";\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n // Deleting a partitioned cookie needs the attribute too — callers pass the\n // same options for set and delete, so this covers both.\n if (options.partitioned) parts.push(\"Partitioned\");\n return parts.join(\"; \");\n}\n\nexport function deleteCookie(name: string, options: CookieOptions = {}): string {\n return serializeCookie(name, \"\", { ...options, maxAge: 0 });\n}\n","import { createRemoteJWKSet } from \"jose\";\nimport type { JWTVerifyGetKey } from \"jose\";\nimport type { ResolvedConfig } from \"./config.js\";\n\ninterface DiscoveryDocument {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n jwks_uri: string;\n}\n\nexport interface Endpoints {\n issuer: string;\n authorizationEndpoint: string;\n tokenEndpoint: string;\n jwks: JWTVerifyGetKey;\n}\n\nconst endpointsCache = new Map<string, Promise<Endpoints>>();\n\n/**\n * Split horizon: URLs the SERVER fetches (discovery, token endpoint, JWKS)\n * are moved onto `internalIncortaUrl` when set. Values the BROWSER navigates\n * to (authorization endpoint) and values used for VALIDATION (issuer — must\n * equal the token's `iss` claim) are never rewritten.\n *\n * The match is by PATH under the Incorta context path (`/incorta` of\n * `incortaUrl`), not by a literal prefix: the discovery document is fetched\n * from the internal Incorta itself, and Incorta spells its own URLs with the\n * explicit port (`https://host:443/...`), as `http://` when the request\n * reached it unproxied, or with an in-cluster host name. A prefix match\n * misses all three and sends the call to the public host — which, where the\n * public host allow-lists callers, fails the token exchange and loops the\n * sign-in. URLs outside the context path pass through untouched.\n */\nfunction toInternal(config: ResolvedConfig, url: string): string {\n const internal = config.internalIncortaUrl?.replace(/\\/+$/, \"\");\n if (!internal) return url;\n let target: URL;\n let contextPath: string;\n try {\n target = new URL(url);\n contextPath = new URL(config.incortaUrl).pathname.replace(/\\/+$/, \"\");\n } catch {\n return url;\n }\n const path = target.pathname;\n if (path !== contextPath && !path.startsWith(`${contextPath}/`)) return url;\n return `${internal}${path.slice(contextPath.length)}${target.search}`;\n}\n\n/**\n * Resolves the tenant's OAuth endpoints, preferring the OIDC discovery document\n * and falling back to Incorta's fixed `/oauth/{tenant}/...` layout when the\n * document is unreachable. Cached per issuer (and internal-URL horizon) for\n * the process lifetime.\n */\nexport function getEndpoints(config: ResolvedConfig): Promise<Endpoints> {\n const cacheKey = `${config.issuer}|${config.internalIncortaUrl ?? \"\"}`;\n const cached = endpointsCache.get(cacheKey);\n if (cached) return cached;\n\n const promise = resolve(config).catch((error) => {\n // Do not cache failures — a temporarily unreachable Incorta should not\n // poison the process.\n endpointsCache.delete(cacheKey);\n throw error;\n });\n endpointsCache.set(cacheKey, promise);\n return promise;\n}\n\nasync function resolve(config: ResolvedConfig): Promise<Endpoints> {\n const fallback: DiscoveryDocument = {\n issuer: config.issuer,\n authorization_endpoint: `${config.issuer}/authorize`,\n token_endpoint: `${config.issuer}/token`,\n jwks_uri: `${config.issuer}/.well-known/jwks.json`,\n };\n\n let doc = fallback;\n try {\n const response = await config.fetch(toInternal(config, config.discoveryUrl), {\n headers: { accept: \"application/json\" },\n });\n if (response.ok) {\n const body = (await response.json()) as Partial<DiscoveryDocument>;\n doc = {\n issuer: body.issuer ?? fallback.issuer,\n authorization_endpoint: body.authorization_endpoint ?? fallback.authorization_endpoint,\n token_endpoint: body.token_endpoint ?? fallback.token_endpoint,\n jwks_uri: body.jwks_uri ?? fallback.jwks_uri,\n };\n }\n } catch {\n // Fall through to the fixed layout.\n }\n\n const jwks =\n config.jwksOverride ??\n createRemoteJWKSet(new URL(toInternal(config, doc.jwks_uri)), {\n cacheMaxAge: 10 * 60 * 1000,\n cooldownDuration: 30 * 1000,\n });\n\n return {\n issuer: doc.issuer,\n authorizationEndpoint: doc.authorization_endpoint,\n tokenEndpoint: toInternal(config, doc.token_endpoint),\n jwks,\n };\n}\n\n/** Test hook: clears the per-process endpoint cache. */\nexport function clearEndpointsCache(): void {\n endpointsCache.clear();\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\n/**\n * PKCE (RFC 7636) for the authorization-code flow. Incorta registers app\n * clients as PUBLIC clients (no client secret — see `registerClient`), so\n * the code verifier is what binds the authorization code to this server:\n * an intercepted code is worthless without it. The verifier lives in the\n * sealed login-state cookie between `/login` and `/callback`; only its\n * S256 challenge travels to Incorta.\n */\nexport function generateCodeVerifier(): string {\n // 32 random bytes → 43 base64url chars, inside RFC 7636's 43–128 range.\n return randomBytes(32).toString(\"base64url\");\n}\n\nexport function codeChallengeFor(codeVerifier: string): string {\n return createHash(\"sha256\").update(codeVerifier, \"ascii\").digest(\"base64url\");\n}\n\nexport const CODE_CHALLENGE_METHOD = \"S256\";\n","import { createHash } from \"node:crypto\";\nimport { EncryptJWT, jwtDecrypt } from \"jose\";\nimport type { ResolvedConfig, SessionData } from \"./config.js\";\n\n/**\n * Session and state cookies are sealed as encrypted JWTs (JWE, dir + A256GCM):\n * tamper-proof AND opaque, since the session carries the Incorta access/refresh\n * tokens. The 256-bit key is derived from the configured secret.\n */\nfunction sealKey(config: ResolvedConfig): Uint8Array {\n return new Uint8Array(\n createHash(\"sha256\").update(`incorta-auth:${config.secret}`).digest(),\n );\n}\n\ninterface SealedPayload {\n /** Guards against replaying one cookie kind as the other. */\n purpose: \"session\" | \"state\";\n data: unknown;\n}\n\nasync function seal(\n config: ResolvedConfig,\n purpose: SealedPayload[\"purpose\"],\n data: unknown,\n maxAgeSeconds: number,\n): Promise<string> {\n return new EncryptJWT({ purpose, data })\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(`${maxAgeSeconds}s`)\n .encrypt(sealKey(config));\n}\n\nasync function unseal<T>(\n config: ResolvedConfig,\n purpose: SealedPayload[\"purpose\"],\n cookieValue: string | undefined,\n): Promise<{ data: T; issuedAt: number; expiresAt: number } | null> {\n if (!cookieValue) return null;\n try {\n const { payload } = await jwtDecrypt(cookieValue, sealKey(config));\n if (payload.purpose !== purpose) return null;\n return {\n data: payload.data as T,\n issuedAt: (payload.iat ?? 0) * 1000,\n expiresAt: (payload.exp ?? 0) * 1000,\n };\n } catch {\n // Expired, tampered, or sealed with a different secret — treat as absent.\n return null;\n }\n}\n\nexport function sealSession(config: ResolvedConfig, session: SessionData): Promise<string> {\n return seal(config, \"session\", session, config.sessionMaxAge);\n}\n\nexport async function unsealSession(\n config: ResolvedConfig,\n cookieValue: string | undefined,\n): Promise<{ session: SessionData; issuedAt: number; expiresAt: number } | null> {\n const result = await unseal<SessionData>(config, \"session\", cookieValue);\n if (!result) return null;\n const { data, issuedAt, expiresAt } = result;\n if (!data || typeof data !== \"object\" || !data.user || !data.accessToken) return null;\n return { session: data, issuedAt, expiresAt };\n}\n\n/** Login round-trip state (CSRF protection + post-login destination). */\nexport interface LoginState {\n state: string;\n nonce: string;\n returnTo: string;\n /**\n * PKCE code verifier for this login attempt (absent only in a state cookie\n * sealed by a pre-PKCE build — the callback then exchanges without one).\n */\n codeVerifier?: string;\n}\n\nconst STATE_MAX_AGE_SECONDS = 10 * 60;\n\nexport function sealLoginState(config: ResolvedConfig, state: LoginState): Promise<string> {\n return seal(config, \"state\", state, STATE_MAX_AGE_SECONDS);\n}\n\nexport async function unsealLoginState(\n config: ResolvedConfig,\n cookieValue: string | undefined,\n): Promise<LoginState | null> {\n const result = await unseal<LoginState>(config, \"state\", cookieValue);\n if (!result) return null;\n const { data } = result;\n if (!data || typeof data.state !== \"string\" || typeof data.nonce !== \"string\") return null;\n if (data.codeVerifier !== undefined && typeof data.codeVerifier !== \"string\") return null;\n return data;\n}\n","import { decodeJwt, jwtVerify } from \"jose\";\nimport type { JWTPayload } from \"jose\";\nimport type { IncortaUser, ResolvedConfig } from \"./config.js\";\nimport { getEndpoints } from \"./discovery.js\";\n\nexport interface TokenSet {\n accessToken: string;\n /** Epoch millis derived from the access token's `exp` claim. */\n accessTokenExpiresAt: number;\n idToken?: string;\n refreshToken?: string;\n}\n\ninterface TokenEndpointResponse {\n access_token?: string;\n id_token?: string;\n refresh_token?: string;\n error?: string;\n error_description?: string;\n}\n\nexport class IncortaAuthError extends Error {\n constructor(\n readonly code: string,\n message: string,\n ) {\n super(`[incorta-auth] ${message}`);\n this.name = \"IncortaAuthError\";\n }\n}\n\nfunction basicAuth(clientId: string, clientSecret: string): string {\n const raw = `${clientId}:${clientSecret}`;\n return `Basic ${Buffer.from(raw, \"utf8\").toString(\"base64\")}`;\n}\n\n/**\n * How the token request identifies the client (RFC 6749 §2.3 / §4.1.3): a\n * confidential client authenticates with `client_secret_basic`; a public\n * client — what Incorta registers apps as — has no secret and sends\n * `client_id` in the body instead.\n */\nfunction clientAuthentication(\n config: ResolvedConfig,\n params: Record<string, string>,\n): { headers: Record<string, string>; params: Record<string, string> } {\n if (config.clientSecret) {\n return { headers: { authorization: basicAuth(config.clientId, config.clientSecret) }, params };\n }\n return { headers: {}, params: { client_id: config.clientId, ...params } };\n}\n\nasync function callTokenEndpoint(\n config: ResolvedConfig,\n params: Record<string, string>,\n): Promise<TokenSet> {\n const { tokenEndpoint } = await getEndpoints(config);\n const auth = clientAuthentication(config, params);\n\n let response: Response;\n try {\n response = await config.fetch(tokenEndpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n accept: \"application/json\",\n ...auth.headers,\n },\n body: new URLSearchParams(auth.params).toString(),\n });\n } catch (error) {\n throw new IncortaAuthError(\n \"incorta_unreachable\",\n `Could not reach the Incorta token endpoint (${tokenEndpoint}): ${(error as Error).message}`,\n );\n }\n\n let body: TokenEndpointResponse;\n try {\n body = (await response.json()) as TokenEndpointResponse;\n } catch {\n throw new IncortaAuthError(\n \"invalid_token_response\",\n `Token endpoint returned a non-JSON response (HTTP ${response.status}).`,\n );\n }\n\n if (!response.ok || !body.access_token) {\n throw new IncortaAuthError(\n body.error ?? \"token_exchange_failed\",\n body.error_description ?? `Token exchange failed (HTTP ${response.status}).`,\n );\n }\n\n const { exp } = decodeJwt(body.access_token);\n return {\n accessToken: body.access_token,\n accessTokenExpiresAt: exp ? exp * 1000 : Date.now() + 60 * 60 * 1000,\n idToken: body.id_token,\n refreshToken: body.refresh_token,\n };\n}\n\nexport function exchangeCode(\n config: ResolvedConfig,\n code: string,\n redirectUri: string,\n /** The PKCE verifier sealed at `/login`; Incorta checks it against the challenge the authorize request carried. */\n codeVerifier?: string,\n): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n ...(codeVerifier ? { code_verifier: codeVerifier } : {}),\n });\n}\n\nexport function refreshTokens(config: ResolvedConfig, refreshToken: string): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n });\n}\n\nasync function verifyJwt(config: ResolvedConfig, token: string): Promise<JWTPayload> {\n const { jwks, issuer } = await getEndpoints(config);\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer,\n audience: config.clientId,\n algorithms: [\"RS256\"],\n });\n return payload;\n } catch (error) {\n throw new IncortaAuthError(\n \"invalid_token\",\n `Token verification failed: ${(error as Error).message}`,\n );\n }\n}\n\nexport async function verifyAccessToken(\n config: ResolvedConfig,\n accessToken: string,\n): Promise<JWTPayload> {\n return verifyJwt(config, accessToken);\n}\n\n/**\n * Verifies the ID token (signature, issuer, audience, nonce) and maps its\n * claims to the SDK's user shape.\n */\nexport async function verifyIdToken(\n config: ResolvedConfig,\n idToken: string,\n expectedNonce: string,\n): Promise<IncortaUser> {\n const payload = await verifyJwt(config, idToken);\n\n if (payload.nonce !== expectedNonce) {\n throw new IncortaAuthError(\"nonce_mismatch\", \"ID token nonce does not match the login request.\");\n }\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new IncortaAuthError(\"invalid_token\", \"ID token is missing the sub claim.\");\n }\n\n const roles = Array.isArray(payload.roles)\n ? payload.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n\n return {\n sub: payload.sub,\n name: typeof payload.name === \"string\" ? payload.name : undefined,\n email: typeof payload.email === \"string\" ? payload.email : undefined,\n roles,\n tenant: typeof payload.tenant === \"string\" ? payload.tenant : config.tenant,\n };\n}\n\n/**\n * Fallback identity when the client was registered without the `openid` scope\n * (no ID token in the response): identify the user from the verified access\n * token instead. Name/email/roles are unavailable in that case.\n */\nexport async function userFromAccessToken(\n config: ResolvedConfig,\n accessToken: string,\n): Promise<IncortaUser> {\n const payload = await verifyAccessToken(config, accessToken);\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new IncortaAuthError(\"invalid_token\", \"Access token is missing the sub claim.\");\n }\n return {\n sub: payload.sub,\n roles: [],\n tenant: typeof payload.tenant === \"string\" ? payload.tenant : config.tenant,\n };\n}\n","interface ManagementErrorBody {\n error?: string;\n error_description?: string;\n}\n\nfunction registrationEndpoint(incortaUrl: string, tenant: string, clientId?: string): string {\n const base = `${incortaUrl.replace(/\\/+$/, \"\")}/oauth/${tenant}/register`;\n return clientId ? `${base}/${clientId}` : base;\n}\n\nasync function readJson<T extends ManagementErrorBody>(\n response: Response,\n what: string,\n): Promise<T> {\n let body: T;\n try {\n body = (await response.json()) as T;\n } catch {\n throw new Error(\n `[incorta-auth] ${what} returned a non-JSON response (HTTP ${response.status}). ` +\n \"Is the OAuth authorization server enabled on this cluster?\",\n );\n }\n if (!response.ok) {\n throw new Error(\n `[incorta-auth] ${what} failed (HTTP ${response.status}): ` +\n `${body.error ?? \"unknown_error\"} — ${body.error_description ?? \"no description\"}`,\n );\n }\n return body;\n}\n\nexport interface RegisterClientOptions {\n /** Incorta base URL, e.g. `http://localhost:8080/incorta`. */\n incortaUrl: string;\n /** Tenant to register the client in. */\n tenant: string;\n /** Display name of the client. */\n name: string;\n /** Allowed OAuth callback URLs, e.g. `http://localhost:4000/auth/callback`. */\n redirectUris: string[];\n /** @default \"openid profile email\" */\n scope?: string;\n /** @default [\"authorization_code\", \"refresh_token\"] */\n grantTypes?: string[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface RegisteredClient {\n clientId: string;\n /**\n * Only for a CONFIDENTIAL client. Incorta issues one solely to clients\n * holding the `client_credentials` grant (PAT-authenticated registration);\n * an app registered here is a PUBLIC client and gets none — token requests\n * then carry `client_id` alone, and PKCE protects the code flow.\n */\n clientSecret?: string;\n /** What Incorta decided: `none` for a public client, `client_secret_basic` for a confidential one. */\n tokenEndpointAuthMethod?: string;\n /** Bearer token for managing this registration (GET/PUT/DELETE it later). */\n registrationAccessToken?: string;\n /** URL to manage this registration. */\n registrationClientUri?: string;\n}\n\n/**\n * Registers an OAuth client in Incorta via RFC 7591 dynamic client\n * registration (`POST {incortaUrl}/oauth/{tenant}/register`). Incorta allows\n * unauthenticated registration for authorization_code clients and registers\n * them as PUBLIC clients: the response carries the generated client_id (and a\n * client_secret only on servers that still issue one) — store it once, e.g.\n * in the app's `.env`.\n */\nexport async function registerClient(\n options: RegisterClientOptions,\n): Promise<RegisteredClient> {\n const {\n incortaUrl,\n tenant,\n name,\n redirectUris,\n scope = \"openid profile email\",\n grantTypes = [\"authorization_code\", \"refresh_token\"],\n fetchImpl = fetch,\n } = options;\n\n if (!redirectUris.length) {\n throw new Error(\"[incorta-auth] registerClient requires at least one redirect URI.\");\n }\n\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", accept: \"application/json\" },\n body: JSON.stringify({\n client_name: name,\n redirect_uris: redirectUris,\n grant_types: grantTypes,\n scope,\n token_endpoint_auth_method: \"client_secret_basic\",\n }),\n });\n\n interface RegistrationResponse extends ManagementErrorBody {\n client_id?: string;\n client_secret?: string;\n token_endpoint_auth_method?: string;\n registration_access_token?: string;\n registration_client_uri?: string;\n }\n\n const body = await readJson<RegistrationResponse>(response, \"Client registration\");\n if (!body.client_id) {\n throw new Error(`[incorta-auth] Client registration failed: response is missing client_id.`);\n }\n\n return {\n clientId: body.client_id,\n clientSecret: body.client_secret || undefined,\n tokenEndpointAuthMethod: body.token_endpoint_auth_method,\n registrationAccessToken: body.registration_access_token,\n registrationClientUri: body.registration_client_uri,\n };\n}\n\nexport interface ManageClientOptions {\n /** Incorta base URL, e.g. `http://localhost:8080/incorta`. */\n incortaUrl: string;\n tenant: string;\n clientId: string;\n /** The registration access token returned when the client was registered. */\n registrationAccessToken: string;\n fetchImpl?: typeof fetch;\n}\n\n/** Current metadata of a registered client. */\nexport interface ClientInfo {\n clientId: string;\n name?: string;\n redirectUris: string[];\n grantTypes: string[];\n scope?: string;\n}\n\ninterface ClientMetadataResponse extends ManagementErrorBody {\n client_id?: string;\n client_name?: string;\n redirect_uris?: string[];\n grant_types?: string[];\n scope?: string;\n}\n\nfunction toClientInfo(body: ClientMetadataResponse, fallbackClientId: string): ClientInfo {\n return {\n clientId: body.client_id ?? fallbackClientId,\n name: body.client_name,\n redirectUris: body.redirect_uris ?? [],\n grantTypes: body.grant_types ?? [],\n scope: body.scope,\n };\n}\n\n/**\n * Reads a registered client's current metadata (RFC 7592-style management,\n * `GET {incortaUrl}/oauth/{tenant}/register/{clientId}` with the registration\n * access token).\n */\nexport async function getClient(options: ManageClientOptions): Promise<ClientInfo> {\n const { incortaUrl, tenant, clientId, registrationAccessToken, fetchImpl = fetch } = options;\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n });\n const body = await readJson<ClientMetadataResponse>(response, \"Reading the client\");\n return toClientInfo(body, clientId);\n}\n\nexport interface UpdateRedirectUrisOptions extends ManageClientOptions {\n redirectUris: string[];\n /**\n * `\"add\"` (default) merges the given URIs into the client's current set;\n * `\"replace\"` makes the given list the exact new set (also how URIs are\n * removed).\n */\n mode?: \"add\" | \"replace\";\n}\n\n/**\n * Updates the allowed OAuth callback URLs of a registered client. Incorta's\n * management endpoint applies `redirect_uris` from the PUT body (other\n * metadata is unchanged server-side).\n */\nexport async function updateClientRedirectUris(\n options: UpdateRedirectUrisOptions,\n): Promise<ClientInfo> {\n const {\n incortaUrl,\n tenant,\n clientId,\n registrationAccessToken,\n redirectUris,\n mode = \"add\",\n fetchImpl = fetch,\n } = options;\n\n if (!redirectUris.length) {\n // An empty replace would leave the client unable to complete the\n // authorization-code flow at all — delete the registration instead.\n throw new Error(\"[incorta-auth] updateClientRedirectUris requires at least one redirect URI.\");\n }\n\n const current = await getClient(options);\n const nextUris =\n mode === \"add\" ? [...new Set([...current.redirectUris, ...redirectUris])] : redirectUris;\n\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n method: \"PUT\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n // RFC 7592 update: client_id + full metadata. Incorta applies\n // redirect_uris only, but nimbus-side parsing wants the full shape.\n body: JSON.stringify({\n client_id: clientId,\n redirect_uris: nextUris,\n grant_types: current.grantTypes,\n scope: current.scope,\n client_name: current.name,\n }),\n });\n\n if (!response.ok) {\n await readJson<ManagementErrorBody>(response, \"Updating the client\"); // throws with details\n }\n // The PUT response body is empty on success — re-read for the fresh state.\n return getClient(options);\n}\n\n/**\n * Deletes a registered client (RFC 7592-style management,\n * `DELETE {incortaUrl}/oauth/{tenant}/register/{clientId}` with the\n * registration access token). Existing sessions in apps using the client keep\n * working until they expire; new logins and token refreshes stop immediately.\n */\nexport async function deleteClient(options: ManageClientOptions): Promise<void> {\n const { incortaUrl, tenant, clientId, registrationAccessToken, fetchImpl = fetch } = options;\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n method: \"DELETE\",\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n });\n if (!response.ok) {\n await readJson<ManagementErrorBody>(response, \"Deleting the client\"); // throws with details\n }\n}\n"],"mappings":"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","names":["import_node_crypto","import_node_crypto","import_jose","import_jose","code"]}
|
package/dist/index.js
CHANGED
|
@@ -140,9 +140,19 @@ function deleteCookie(name, options = {}) {
|
|
|
140
140
|
import { createRemoteJWKSet } from "jose";
|
|
141
141
|
var endpointsCache = /* @__PURE__ */ new Map();
|
|
142
142
|
function toInternal(config, url) {
|
|
143
|
-
const internal = config.internalIncortaUrl;
|
|
144
|
-
if (!internal
|
|
145
|
-
|
|
143
|
+
const internal = config.internalIncortaUrl?.replace(/\/+$/, "");
|
|
144
|
+
if (!internal) return url;
|
|
145
|
+
let target;
|
|
146
|
+
let contextPath;
|
|
147
|
+
try {
|
|
148
|
+
target = new URL(url);
|
|
149
|
+
contextPath = new URL(config.incortaUrl).pathname.replace(/\/+$/, "");
|
|
150
|
+
} catch {
|
|
151
|
+
return url;
|
|
152
|
+
}
|
|
153
|
+
const path = target.pathname;
|
|
154
|
+
if (path !== contextPath && !path.startsWith(`${contextPath}/`)) return url;
|
|
155
|
+
return `${internal}${path.slice(contextPath.length)}${target.search}`;
|
|
146
156
|
}
|
|
147
157
|
function getEndpoints(config) {
|
|
148
158
|
const cacheKey = `${config.issuer}|${config.internalIncortaUrl ?? ""}`;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/config.ts","../src/core/handler.ts","../src/core/cookies.ts","../src/core/discovery.ts","../src/core/pkce.ts","../src/core/session.ts","../src/core/tokens.ts","../src/core/register.ts","../src/index.ts"],"sourcesContent":["import type { JWTVerifyGetKey } from \"jose\";\nimport type { AppAccessPolicy } from \"./app-access.js\";\n\n/** The Incorta user identity carried by the session (claims from the ID token). */\nexport interface IncortaUser {\n /** Incorta login name (`sub` claim). */\n sub: string;\n name?: string;\n email?: string;\n /** Incorta role names, as minted into the ID token. */\n roles: string[];\n /** Tenant the user authenticated against. */\n tenant: string;\n}\n\n/** What the sealed session cookie holds. */\nexport interface SessionData {\n user: IncortaUser;\n /** Incorta-issued access token (RS256 JWT, aud = client_id). */\n accessToken: string;\n /** Epoch millis when the access token expires. */\n accessTokenExpiresAt: number;\n /** Rotating refresh token, present when the client has the refresh_token grant. */\n refreshToken?: string;\n /** Epoch millis when this session was first created (login time). */\n createdAt: number;\n}\n\nexport interface SessionInfo {\n user: IncortaUser;\n /** Epoch millis when the session cookie expires (sliding). */\n expiresAt: number;\n /** Epoch millis when the access token expires. */\n accessTokenExpiresAt: number;\n}\n\nexport interface IncortaAuthConfig {\n /**\n * Base URL of the Incorta web application, e.g. `https://acme.cloud.incorta.com/incorta`\n * or `http://localhost:8080/incorta`. Defaults to `INCORTA_URL`.\n */\n incortaUrl?: string;\n /**\n * Split-horizon override: base URL this SERVER should use to reach Incorta\n * when the public `incortaUrl` is not routable from where the app runs\n * (containers, cluster-internal DNS) — e.g.\n * `http://host.docker.internal:8080/incorta` from a local container. Only\n * server-to-server calls (discovery, token exchange, JWKS) use it; browser\n * redirects and token `iss` validation stay on the public `incortaUrl`.\n * Defaults to `INCORTA_INTERNAL_URL`; unset means no rewrite.\n */\n internalIncortaUrl?: string;\n /** Incorta tenant name, e.g. `demo`. Defaults to `INCORTA_TENANT`. */\n tenant?: string;\n /** OAuth client id (see `incorta-auth register`). Defaults to `INCORTA_CLIENT_ID`. */\n clientId?: string;\n /**\n * OAuth client secret — only for a CONFIDENTIAL client. Incorta's dynamic\n * registration issues app clients as PUBLIC clients (no secret; see\n * `registerClient`), and the SDK then authenticates token requests with\n * `client_id` alone and relies on PKCE. Defaults to `INCORTA_CLIENT_SECRET`;\n * unset or empty means public.\n */\n clientSecret?: string;\n /**\n * Secret used to encrypt the session cookie (any long random string, >= 32 chars).\n * Defaults to `INCORTA_AUTH_SECRET`.\n */\n secret?: string;\n /**\n * Path prefix the auth routes are mounted under. The SDK serves\n * `{basePath}/login`, `{basePath}/callback`, `{basePath}/session`,\n * `{basePath}/logout` (and optionally `{basePath}/token`).\n * @default \"/auth\"\n */\n basePath?: string;\n /**\n * External origin of THIS app, e.g. `http://localhost:4000`. Used to build the\n * OAuth callback URL and to validate `redirect_to` targets. When omitted it is\n * derived per request from `x-forwarded-proto`/`x-forwarded-host`/`host`.\n * Defaults to `INCORTA_APP_URL`.\n */\n appUrl?: string;\n /** OAuth scopes to request. @default \"openid profile email\" */\n scope?: string;\n session?: {\n /** @default \"incorta_auth_session\" */\n cookieName?: string;\n /** Session lifetime in seconds (sliding — renewed on activity). @default 43200 (12h) */\n maxAge?: number;\n };\n cookies?: {\n /**\n * `Secure` attribute for cookies. `\"auto\"` sets it when the request is HTTPS.\n * @default \"auto\"\n */\n secure?: boolean | \"auto\";\n /**\n * `SameSite` for the session/state cookies. The `\"lax\"` default is right\n * for top-level apps. Set `\"none\"` when the app runs inside a CROSS-SITE\n * iframe (e.g. an embedded preview) — Lax cookies are never sent there,\n * which loops the login flow forever. `\"none\"` forces the `Secure`\n * attribute (browsers require the pair; Chrome still accepts it on\n * trustworthy plain-http origins like `*.localhost`). Defaults to\n * `INCORTA_COOKIE_SAMESITE`.\n * @default \"lax\"\n */\n sameSite?: \"lax\" | \"none\";\n /**\n * CHIPS: adds the `Partitioned` attribute, giving each embedding\n * top-level site its own cookie jar — what modern Chrome requires for\n * cookies in third-party iframes. Use together with `sameSite: \"none\"`.\n * Defaults to `INCORTA_COOKIE_PARTITIONED` (`1`/`true`).\n * @default false\n */\n partitioned?: boolean;\n };\n /**\n * Expose `GET {basePath}/token` returning the raw Incorta access token to the\n * browser (for calling Incorta public APIs directly from the frontend). Off by\n * default — prefer proxying through your backend.\n * @default false\n */\n exposeAccessToken?: boolean;\n /**\n * Who may use the app — enforced by the whole-app gate (`auth.gate`, and\n * the framework middlewares built on it):\n *\n * - `\"public\"`: anyone with the URL, signed in or not (login stays\n * available, sessions become optional);\n * - `\"authenticated\"` (default): any signed-in Incorta user;\n * - `\"catalog\"`: signed-in AND at least VIEW access to this app in the\n * Incorta catalog, verified against `/apps-catalog/check` with a 30s\n * per-user cache. Unregistered apps stay open to authenticated users\n * (legacy-open); an unanswerable check fails CLOSED (503).\n *\n * Normally stamped by the Incorta Apps platform via `INCORTA_APP_ACCESS`.\n * An unrecognized env value falls back to `\"authenticated\"` (never to\n * `\"public\"` — a typo must not open the app) with a console warning.\n */\n appAccess?: AppAccessPolicy;\n /**\n * How to sign in when the app runs inside a cross-site iframe (the Incorta\n * builder preview). `\"redirect\"` (default) bounces the frame to Incorta —\n * correct top-level, but when the Incorta cluster has iframe inclusion OFF\n * (its login is `X-Frame-Options: SAMEORIGIN` and its session cookie is\n * `SameSite=Lax`) the login can't render/complete in the frame. `\"new-tab\"`\n * serves an interstitial that, inside a frame, offers a \"Sign in with\n * Incorta\" button opening the app top-level in a new tab (where login\n * works); at the top level it still redirects straight to Incorta.\n * Env fallback: `INCORTA_EMBEDDED_SIGN_IN`.\n * @default \"redirect\"\n */\n embeddedSignIn?: \"redirect\" | \"new-tab\";\n advanced?: {\n /** Override the OIDC discovery document URL (rarely needed). */\n discoveryUrl?: string;\n /** Override JWKS resolution (used by tests to verify against a local key). */\n jwks?: JWTVerifyGetKey;\n /** Override fetch (used by tests). */\n fetch?: typeof fetch;\n };\n}\n\nexport interface ResolvedConfig {\n incortaUrl: string;\n /** Server-side fetch base when set (split horizon) — see the config doc. */\n internalIncortaUrl?: string;\n tenant: string;\n clientId: string;\n /** Absent for a public client: token requests then carry `client_id` instead of Basic auth. */\n clientSecret?: string;\n secret: string;\n basePath: string;\n appUrl?: string;\n scope: string;\n cookieName: string;\n stateCookieName: string;\n sessionMaxAge: number;\n secureCookies: boolean | \"auto\";\n cookieSameSite: \"lax\" | \"none\";\n cookiePartitioned: boolean;\n exposeAccessToken: boolean;\n appAccess: AppAccessPolicy;\n embeddedSignIn: \"redirect\" | \"new-tab\";\n /** `{incortaUrl}/oauth/{tenant}` — the OIDC issuer for this tenant. */\n issuer: string;\n discoveryUrl: string;\n jwksOverride?: JWTVerifyGetKey;\n fetch: typeof fetch;\n}\n\nfunction resolveAppAccess(\n input: AppAccessPolicy | undefined,\n envValue: string | undefined,\n): AppAccessPolicy {\n if (input) return input;\n const raw = envValue?.trim().toLowerCase();\n if (!raw) return \"authenticated\";\n if (raw === \"public\" || raw === \"authenticated\" || raw === \"catalog\") return raw;\n // Fail SAFE, not open: garbage in the platform-stamped env keeps the\n // login-only posture rather than silently exposing or bricking the app.\n console.warn(\n `[incorta-auth] Unrecognized INCORTA_APP_ACCESS value \"${envValue}\" — using \"authenticated\".`,\n );\n return \"authenticated\";\n}\n\nfunction required(value: string | undefined, name: string, envVar: string): string {\n if (!value) {\n throw new Error(\n `[incorta-auth] Missing required config \"${name}\" (or env var ${envVar}).`,\n );\n }\n return value;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nexport function resolveConfig(input: IncortaAuthConfig = {}): ResolvedConfig {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env ?? {};\n\n const incortaUrl = stripTrailingSlash(\n required(input.incortaUrl ?? env.INCORTA_URL, \"incortaUrl\", \"INCORTA_URL\"),\n );\n const internalIncortaUrl = input.internalIncortaUrl ?? env.INCORTA_INTERNAL_URL;\n const tenant = required(input.tenant ?? env.INCORTA_TENANT, \"tenant\", \"INCORTA_TENANT\");\n const clientId = required(\n input.clientId ?? env.INCORTA_CLIENT_ID,\n \"clientId\",\n \"INCORTA_CLIENT_ID\",\n );\n // Optional: a public client (Incorta's default for registered apps) has none.\n const clientSecret = (input.clientSecret ?? env.INCORTA_CLIENT_SECRET)?.trim() || undefined;\n const secret = required(\n input.secret ?? env.INCORTA_AUTH_SECRET,\n \"secret\",\n \"INCORTA_AUTH_SECRET\",\n );\n if (secret.length < 32) {\n throw new Error(\n \"[incorta-auth] `secret` must be at least 32 characters. Generate one with: openssl rand -base64 32\",\n );\n }\n\n const basePath = input.basePath ?? \"/auth\";\n if (!basePath.startsWith(\"/\") || basePath.endsWith(\"/\")) {\n throw new Error(\n `[incorta-auth] basePath must start with \"/\" and not end with \"/\" (got \"${basePath}\").`,\n );\n }\n\n const appUrl = input.appUrl ?? env.INCORTA_APP_URL;\n const cookieName = input.session?.cookieName ?? \"incorta_auth_session\";\n const issuer = `${incortaUrl}/oauth/${tenant}`;\n\n const envSameSite = env.INCORTA_COOKIE_SAMESITE?.toLowerCase();\n const cookieSameSite =\n input.cookies?.sameSite ??\n (envSameSite === \"none\" || envSameSite === \"lax\" ? envSameSite : \"lax\");\n const envPartitioned = env.INCORTA_COOKIE_PARTITIONED?.toLowerCase();\n const cookiePartitioned =\n input.cookies?.partitioned ??\n (envPartitioned === \"1\" || envPartitioned === \"true\");\n if (cookiePartitioned && cookieSameSite !== \"none\") {\n throw new Error(\n '[incorta-auth] `cookies.partitioned` requires `cookies.sameSite: \"none\"` — browsers only partition cross-site cookies.',\n );\n }\n\n return {\n incortaUrl,\n internalIncortaUrl: internalIncortaUrl\n ? stripTrailingSlash(internalIncortaUrl)\n : undefined,\n tenant,\n clientId,\n clientSecret,\n secret,\n basePath,\n appUrl: appUrl ? stripTrailingSlash(appUrl) : undefined,\n scope: input.scope ?? \"openid profile email\",\n cookieName,\n stateCookieName: `${cookieName}_state`,\n sessionMaxAge: input.session?.maxAge ?? 12 * 60 * 60,\n secureCookies: input.cookies?.secure ?? \"auto\",\n cookieSameSite,\n cookiePartitioned,\n exposeAccessToken: input.exposeAccessToken ?? false,\n appAccess: resolveAppAccess(input.appAccess, env.INCORTA_APP_ACCESS),\n embeddedSignIn:\n input.embeddedSignIn ??\n (env.INCORTA_EMBEDDED_SIGN_IN?.toLowerCase() === \"new-tab\"\n ? \"new-tab\"\n : \"redirect\"),\n issuer,\n discoveryUrl:\n input.advanced?.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`,\n jwksOverride: input.advanced?.jwks,\n fetch: input.advanced?.fetch ?? fetch,\n };\n}\n","import { randomBytes } from \"node:crypto\";\nimport {\n type AppAccess,\n AppAccessTokenRejectedError,\n AppAccessUnavailableError,\n createAppAccessChecker,\n} from \"./app-access.js\";\nimport type { ResolvedConfig, SessionData, SessionInfo } from \"./config.js\";\nimport { deleteCookie, parseCookies, serializeCookie } from \"./cookies.js\";\nimport { getEndpoints } from \"./discovery.js\";\nimport { CODE_CHALLENGE_METHOD, codeChallengeFor, generateCodeVerifier } from \"./pkce.js\";\nimport {\n sealLoginState,\n sealSession,\n unsealLoginState,\n unsealSession,\n} from \"./session.js\";\nimport {\n IncortaAuthError,\n exchangeCode,\n refreshTokens,\n userFromAccessToken,\n verifyIdToken,\n} from \"./tokens.js\";\n\n/** A read-only view of the current session for server-side app code. */\nexport interface AuthSession extends SessionInfo {\n /** Incorta access token — pass as `Authorization: Bearer` to Incorta APIs. */\n accessToken: string;\n /**\n * The catalog verdict for this user and app, when a gate already resolved\n * it on this request (`appAccess: \"catalog\"` policy). Absent otherwise —\n * fetch on demand with `auth.getAppAccess(session)`.\n */\n appAccess?: AppAccess;\n}\n\nexport type AuthHandler = (request: Request) => Promise<Response>;\n\n/**\n * Whole-app gate: a `Response` to short-circuit with (auth routes, login\n * redirects, deny/unavailable pages) or `null` for \"let the request through\n * to the app\".\n */\nexport type AuthGate = (request: Request) => Promise<Response | null>;\n\n/** Refresh the access token when it has less than this long left to live. */\nconst ACCESS_TOKEN_REFRESH_LEEWAY_MS = 60 * 1000;\n\nfunction requestOrigin(config: ResolvedConfig, request: Request): string {\n if (config.appUrl) return config.appUrl;\n const url = new URL(request.url);\n const proto =\n request.headers.get(\"x-forwarded-proto\")?.split(\",\")[0]?.trim() ??\n url.protocol.replace(\":\", \"\");\n const host =\n request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim() ??\n request.headers.get(\"host\") ??\n url.host;\n return `${proto}://${host}`;\n}\n\nfunction isSecureRequest(config: ResolvedConfig, request: Request): boolean {\n if (config.secureCookies !== \"auto\") return config.secureCookies;\n return requestOrigin(config, request).startsWith(\"https://\");\n}\n\n/**\n * Only same-origin destinations are allowed after login/logout — anything else\n * would let a crafted link bounce users through this app to an attacker page.\n */\nfunction validateReturnTo(raw: string | null, origin: string): string {\n if (!raw) return \"/\";\n if (raw.startsWith(\"/\") && !raw.startsWith(\"//\")) return raw;\n try {\n const url = new URL(raw);\n if (url.origin === origin) return `${url.pathname}${url.search}${url.hash}` || \"/\";\n } catch {\n // Not a URL — fall through.\n }\n return \"/\";\n}\n\nfunction json(status: number, body: unknown, cookies: string[] = []): Response {\n const headers = new Headers({\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(JSON.stringify(body), { status, headers });\n}\n\nfunction redirect(location: string, cookies: string[] = []): Response {\n const headers = new Headers({ location, \"cache-control\": \"no-store\" });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(null, { status: 302, headers });\n}\n\nfunction withErrorParam(target: string, code: string): string {\n const separator = target.includes(\"?\") ? \"&\" : \"?\";\n return `${target}${separator}incorta_auth_error=${encodeURIComponent(code)}`;\n}\n\nfunction randomToken(): string {\n return randomBytes(16).toString(\"base64url\");\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/** Browser navigations get pages/redirects; data requests get JSON. */\nfunction wantsHtml(request: Request): boolean {\n return (\n request.method.toUpperCase() === \"GET\" &&\n (request.headers.get(\"accept\") ?? \"\").includes(\"text/html\")\n );\n}\n\nfunction requestPath(url: URL): string {\n return `${url.pathname}${url.search}`;\n}\n\nexport function createHandler(config: ResolvedConfig): {\n handler: AuthHandler;\n getSession: (request: Request) => Promise<AuthSession | null>;\n gate: AuthGate;\n getAppAccess: (session: AuthSession) => Promise<AppAccess>;\n} {\n const appAccessChecker = createAppAccessChecker(config);\n\n const sessionCookieOptions = (secure: boolean) => ({\n httpOnly: true,\n // SameSite=None requires Secure — force the pair even on plain http\n // (Chrome accepts Secure cookies from trustworthy origins like\n // `*.localhost`; real cross-site embedding needs https anyway).\n secure: config.cookieSameSite === \"none\" ? true : secure,\n sameSite: config.cookieSameSite,\n partitioned: config.cookiePartitioned,\n path: \"/\",\n });\n\n async function login(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const secure = isSecureRequest(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n\n // Already signed in — skip the OAuth round trip entirely.\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const existing = await unsealSession(config, cookies[config.cookieName]);\n if (existing) return redirect(returnTo);\n\n const state = randomToken();\n const nonce = randomToken();\n // PKCE binds the code to this server (pkce.ts) — the verifier stays in the\n // sealed cookie, only its challenge goes to Incorta.\n const codeVerifier = generateCodeVerifier();\n const stateCookie = serializeCookie(\n config.stateCookieName,\n await sealLoginState(config, { state, nonce, returnTo, codeVerifier }),\n { ...sessionCookieOptions(secure), maxAge: 600 },\n );\n\n const { authorizationEndpoint } = await getEndpoints(config);\n const authorizeUrl = new URL(authorizationEndpoint);\n authorizeUrl.searchParams.set(\"response_type\", \"code\");\n authorizeUrl.searchParams.set(\"client_id\", config.clientId);\n authorizeUrl.searchParams.set(\"redirect_uri\", `${origin}${config.basePath}/callback`);\n authorizeUrl.searchParams.set(\"scope\", config.scope);\n authorizeUrl.searchParams.set(\"state\", state);\n authorizeUrl.searchParams.set(\"nonce\", nonce);\n authorizeUrl.searchParams.set(\"code_challenge\", codeChallengeFor(codeVerifier));\n authorizeUrl.searchParams.set(\"code_challenge_method\", CODE_CHALLENGE_METHOD);\n\n // Embedded apps that can't complete Incorta login in a cross-site iframe\n // get a framing-aware interstitial instead of a doomed in-frame redirect.\n if (config.embeddedSignIn === \"new-tab\") {\n return newTabSignInPage(authorizeUrl.toString(), `${origin}/`, [stateCookie]);\n }\n return redirect(authorizeUrl.toString(), [stateCookie]);\n }\n\n async function callback(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const secure = isSecureRequest(config, request);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const clearState = deleteCookie(config.stateCookieName, sessionCookieOptions(secure));\n\n const loginState = await unsealLoginState(config, cookies[config.stateCookieName]);\n const returnTo = validateReturnTo(loginState?.returnTo ?? null, origin);\n\n const fail = (code: string) => redirect(withErrorParam(returnTo, code), [clearState]);\n\n const oauthError = url.searchParams.get(\"error\");\n if (oauthError) return fail(oauthError);\n\n const code = url.searchParams.get(\"code\");\n const state = url.searchParams.get(\"state\");\n if (!code || !state) return fail(\"missing_code\");\n if (!loginState) return fail(\"state_missing\");\n if (state !== loginState.state) return fail(\"state_mismatch\");\n\n try {\n const tokens = await exchangeCode(\n config,\n code,\n `${origin}${config.basePath}/callback`,\n loginState.codeVerifier,\n );\n const user = tokens.idToken\n ? await verifyIdToken(config, tokens.idToken, loginState.nonce)\n : await userFromAccessToken(config, tokens.accessToken);\n\n const session: SessionData = {\n user,\n accessToken: tokens.accessToken,\n accessTokenExpiresAt: tokens.accessTokenExpiresAt,\n refreshToken: tokens.refreshToken,\n createdAt: Date.now(),\n };\n const sessionCookie = serializeCookie(\n config.cookieName,\n await sealSession(config, session),\n { ...sessionCookieOptions(secure), maxAge: config.sessionMaxAge },\n );\n return redirect(returnTo, [sessionCookie, clearState]);\n } catch (error) {\n const code = error instanceof IncortaAuthError ? error.code : \"callback_failed\";\n return fail(code);\n }\n }\n\n /**\n * Loads the session from the request cookie; refreshes the Incorta access\n * token (rotating the refresh token) when it is about to expire, and renews\n * the sliding session cookie past its half-life. Returns the cookies to set.\n */\n async function loadSession(request: Request): Promise<{\n session: SessionData | null;\n sessionExpiresAt: number;\n setCookies: string[];\n }> {\n const secure = isSecureRequest(config, request);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unsealed = await unsealSession(config, cookies[config.cookieName]);\n if (!unsealed) return { session: null, sessionExpiresAt: 0, setCookies: [] };\n\n let { session } = unsealed;\n let mustReseal = false;\n const now = Date.now();\n\n if (session.accessTokenExpiresAt - now < ACCESS_TOKEN_REFRESH_LEEWAY_MS) {\n if (session.refreshToken) {\n try {\n const tokens = await refreshTokens(config, session.refreshToken);\n session = {\n ...session,\n accessToken: tokens.accessToken,\n accessTokenExpiresAt: tokens.accessTokenExpiresAt,\n refreshToken: tokens.refreshToken ?? session.refreshToken,\n };\n mustReseal = true;\n } catch {\n // Refresh failed (expired/rotated elsewhere). Keep the session while\n // the current access token is still usable; otherwise sign out.\n if (session.accessTokenExpiresAt <= now) {\n return {\n session: null,\n sessionExpiresAt: 0,\n setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))],\n };\n }\n }\n } else if (session.accessTokenExpiresAt <= now) {\n return {\n session: null,\n sessionExpiresAt: 0,\n setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))],\n };\n }\n }\n\n const halfLifeMs = (config.sessionMaxAge * 1000) / 2;\n if (now - unsealed.issuedAt > halfLifeMs) mustReseal = true;\n\n if (!mustReseal) {\n return { session, sessionExpiresAt: unsealed.expiresAt, setCookies: [] };\n }\n const resealed = serializeCookie(config.cookieName, await sealSession(config, session), {\n ...sessionCookieOptions(secure),\n maxAge: config.sessionMaxAge,\n });\n return {\n session,\n sessionExpiresAt: now + config.sessionMaxAge * 1000,\n setCookies: [resealed],\n };\n }\n\n async function sessionRoute(request: Request): Promise<Response> {\n const { session, sessionExpiresAt, setCookies } = await loadSession(request);\n if (!session) return json(200, { user: null }, setCookies);\n return json(\n 200,\n {\n user: session.user,\n session: {\n expiresAt: sessionExpiresAt,\n accessTokenExpiresAt: session.accessTokenExpiresAt,\n },\n },\n setCookies,\n );\n }\n\n async function tokenRoute(request: Request): Promise<Response> {\n if (!config.exposeAccessToken) return json(404, { error: \"not_found\" });\n const { session, setCookies } = await loadSession(request);\n if (!session) return json(401, { error: \"unauthenticated\" }, setCookies);\n return json(\n 200,\n { accessToken: session.accessToken, expiresAt: session.accessTokenExpiresAt },\n setCookies,\n );\n }\n\n /**\n * `GET {basePath}/refresh?redirect_to=…` — refresh the access token (and\n * reseal the rotated cookie) via a browser round trip, then bounce back.\n * The catalog gate sends navigations here when the session's access token\n * has already expired: the gate itself returns `null` to continue, so it\n * has no response to carry a rotated refresh-token cookie on — a redirect\n * does. A session that cannot be refreshed is sent to login instead\n * (loadSession already cleared its cookie).\n */\n async function refreshRoute(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n const { session, setCookies } = await loadSession(request);\n if (!session) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(returnTo)}`,\n setCookies,\n );\n }\n return redirect(returnTo, setCookies);\n }\n\n async function logout(request: Request, url: URL): Promise<Response> {\n const secure = isSecureRequest(config, request);\n const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));\n if (request.method === \"GET\") {\n // No explicit redirect_to → the signed-out page, NOT \"/\": in an\n // SSO-gated app, landing on a gated page silently re-runs the login\n // flow and (while the Incorta session lives) signs the user straight\n // back in — sign-out must land somewhere UNGATED to visibly stick.\n const raw = url.searchParams.get(\"redirect_to\");\n if (!raw) return redirect(`${config.basePath}/signed-out`, [clearSession]);\n const origin = requestOrigin(config, request);\n return redirect(validateReturnTo(raw, origin), [clearSession]);\n }\n return json(200, { ok: true }, [clearSession]);\n }\n\n /**\n * Ungated landing page after sign-out (it lives under basePath, so app\n * gates pass it through). Without it there is nowhere for a fully-gated\n * app to show \"you are signed out\" — every app page would bounce through\n * the IdP and re-login. Self-contained HTML, no assets.\n */\n function signedOutPage(): Response {\n const signInHref = `${config.basePath}/login?redirect_to=%2F`;\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Signed out</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n a { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; font-size: .9rem;\n text-decoration: none; }\n </style>\n </head>\n <body>\n <main>\n <h1>You’ve signed out of this app</h1>\n <p>\n Your Incorta account may still be signed in elsewhere — signing back\n in here is one click.\n </p>\n <a href=\"${signInHref}\">Sign in with Incorta</a>\n </main>\n </body>\n</html>`,\n {\n status: 200,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n },\n );\n }\n\n /**\n * `embeddedSignIn: \"new-tab\"` interstitial. The inline script decides\n * client-side: at the top level it redirects straight to Incorta; inside a\n * cross-site iframe (where Incorta login can't render/complete when the\n * cluster has iframe inclusion off) it reveals a button that opens the app\n * in a new top-level tab. Carries the same state cookie the plain redirect\n * would set — used by the top-level path; harmless in the frame, where the\n * new tab writes its own first-party state cookie.\n */\n function newTabSignInPage(\n authorizeUrl: string,\n appRoot: string,\n cookies: string[],\n ): Response {\n const body = `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Sign in with Incorta</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.2rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n a.btn { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; text-decoration: none;\n font-weight: 600; }\n </style>\n </head>\n <body>\n <main id=\"frame\" hidden>\n <h1>Sign in with Incorta</h1>\n <p>\n This app signs you in with your Incorta account. The embedded preview\n can’t complete sign-in here — continue in a new tab.\n </p>\n <a class=\"btn\" href=\"${appRoot}\" target=\"_blank\" rel=\"noopener noreferrer\">Sign in with Incorta ↗</a>\n </main>\n <script>\n (function () {\n var authorize = ${JSON.stringify(authorizeUrl)};\n if (window.top === window.self) {\n window.location.replace(authorize);\n } else {\n document.getElementById(\"frame\").hidden = false;\n }\n })();\n </script>\n <noscript><a href=\"${appRoot}\">Continue to sign in</a></noscript>\n </body>\n</html>`;\n const headers = new Headers({\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(body, { status: 200, headers });\n }\n\n /**\n * `appAccess: \"catalog\"` — the user is signed in but the catalog says no:\n * an honest 403 with who they are and a way out (the wrong-account case is\n * the most common real cause). Non-HTML requests get the JSON twin.\n */\n function accessDeniedResponse(request: Request, session: AuthSession): Response {\n if (!wantsHtml(request)) {\n return json(403, { error: \"app_access_denied\" });\n }\n const identity = escapeHtml(session.user.name?.trim() || session.user.sub);\n const detail =\n session.user.name?.trim() && session.user.name.trim() !== session.user.sub\n ? `${identity} (${escapeHtml(session.user.sub)})`\n : identity;\n const switchHref = `${config.basePath}/logout`;\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>No access</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n strong { color: #18181b; font-weight: 600; }\n a { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; font-size: .9rem;\n text-decoration: none; }\n </style>\n </head>\n <body>\n <main>\n <h1>You don’t have access to this application</h1>\n <p>\n Ask the app’s owner to share it with you in Incorta.\n You are signed in as <strong>${detail}</strong>.\n </p>\n <a href=\"${switchHref}\">Sign in with a different account</a>\n </main>\n </body>\n</html>`,\n {\n status: 403,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n },\n );\n }\n\n /**\n * The catalog could not answer (Incorta unreachable/failing) — deny, but\n * make clear this is an outage, not a sharing decision. Distinct from the\n * 403 page so users never conclude they were unshared. Auto-retries: the\n * verdict cache holds no failures, so the next load re-asks.\n */\n function accessUnavailableResponse(request: Request): Response {\n if (!wantsHtml(request)) {\n return json(503, { error: \"app_access_unavailable\" });\n }\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta http-equiv=\"refresh\" content=\"15\" />\n <title>Temporarily unavailable</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0; }\n </style>\n </head>\n <body>\n <main>\n <h1>This app is temporarily unavailable</h1>\n <p>\n Your access could not be verified because Incorta did not respond.\n This page retries automatically — you can also refresh in a\n moment.\n </p>\n </main>\n </body>\n</html>`,\n {\n status: 503,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n \"retry-after\": \"30\",\n },\n },\n );\n }\n\n async function handler(request: Request): Promise<Response> {\n const url = new URL(request.url);\n if (url.pathname !== config.basePath && !url.pathname.startsWith(`${config.basePath}/`)) {\n return json(404, { error: \"not_found\" });\n }\n const route = url.pathname.slice(config.basePath.length) || \"/\";\n const method = request.method.toUpperCase();\n\n try {\n if (route === \"/login\" && method === \"GET\") return await login(request, url);\n if (route === \"/callback\" && method === \"GET\") return await callback(request, url);\n if (route === \"/session\" && method === \"GET\") return await sessionRoute(request);\n if (route === \"/token\" && method === \"GET\") return await tokenRoute(request);\n if (route === \"/refresh\" && method === \"GET\") return await refreshRoute(request, url);\n if (route === \"/logout\" && (method === \"POST\" || method === \"GET\")) {\n return await logout(request, url);\n }\n if (route === \"/signed-out\" && method === \"GET\") return signedOutPage();\n return json(404, { error: \"not_found\" });\n } catch (error) {\n const code = error instanceof IncortaAuthError ? error.code : \"internal_error\";\n const message =\n error instanceof Error ? error.message : \"Unexpected incorta-auth handler error\";\n console.error(`[incorta-auth] ${route} failed:`, message);\n return json(500, { error: code });\n }\n }\n\n async function getSession(request: Request): Promise<AuthSession | null> {\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unsealed = await unsealSession(config, cookies[config.cookieName]);\n if (!unsealed) return null;\n const { session, expiresAt } = unsealed;\n if (session.accessTokenExpiresAt <= Date.now() && !session.refreshToken) return null;\n return {\n user: session.user,\n accessToken: session.accessToken,\n accessTokenExpiresAt: session.accessTokenExpiresAt,\n expiresAt,\n };\n }\n\n /** The catalog verdict for this session's user, via the 30s per-user cache. */\n function getAppAccess(session: AuthSession): Promise<AppAccess> {\n return appAccessChecker.check(session.accessToken, session.user.sub);\n }\n\n /**\n * The whole-app gate, enforcing `config.appAccess` in front of every\n * request (see `IncortaAuthConfig.appAccess`). Auth routes are always\n * served — login stays reachable even for public apps. `null` means\n * \"continue to the app\"; in `\"public\"` mode that is every non-auth route,\n * signed in or not.\n */\n async function gate(request: Request): Promise<Response | null> {\n const url = new URL(request.url);\n if (url.pathname === config.basePath || url.pathname.startsWith(`${config.basePath}/`)) {\n return handler(request);\n }\n if (config.appAccess === \"public\") return null;\n\n const session = await getSession(request);\n if (!session) {\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`,\n );\n }\n return json(401, { error: \"unauthenticated\" });\n }\n if (config.appAccess !== \"catalog\") return null;\n\n // The check replays the session's access token to Incorta — an already\n // expired token would 401 there, so refresh it first via the browser\n // round trip (which is what persists the rotated cookie).\n if (session.accessTokenExpiresAt <= Date.now()) {\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/refresh?redirect_to=${encodeURIComponent(requestPath(url))}`,\n );\n }\n return json(401, { error: \"token_expired\" });\n }\n\n try {\n const verdict = await getAppAccess(session);\n if (verdict.allowed) return null;\n return accessDeniedResponse(request, session);\n } catch (error) {\n if (error instanceof AppAccessTokenRejectedError) {\n // Incorta no longer honors this session's token — sign out and let\n // the login flow mint a fresh one (SSO makes this a silent bounce).\n const secure = isSecureRequest(config, request);\n const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`,\n [clearSession],\n );\n }\n return json(401, { error: \"unauthenticated\" }, [clearSession]);\n }\n if (!(error instanceof AppAccessUnavailableError)) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(\"[incorta-auth] app access check failed unexpectedly:\", message);\n }\n // Verdicts unavailable — NEVER fail open.\n return accessUnavailableResponse(request);\n }\n }\n\n return { handler, getSession, gate, getAppAccess };\n}\n","export interface CookieOptions {\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: \"lax\" | \"strict\" | \"none\";\n /** CHIPS `Partitioned` attribute — per-top-site cookie jar for cross-site\n * iframe embedding. Browsers require Secure alongside it. */\n partitioned?: boolean;\n path?: string;\n /** Seconds. 0 deletes the cookie. */\n maxAge?: number;\n}\n\nexport function parseCookies(header: string | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const name = part.slice(0, eq).trim();\n if (!name) continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"')) value = value.slice(1, -1);\n try {\n out[name] = decodeURIComponent(value);\n } catch {\n out[name] = value;\n }\n }\n return out;\n}\n\nexport function serializeCookie(\n name: string,\n value: string,\n options: CookieOptions = {},\n): string {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n parts.push(`Path=${options.path ?? \"/\"}`);\n if (options.maxAge !== undefined) {\n parts.push(`Max-Age=${Math.floor(options.maxAge)}`);\n // Expires for compatibility with clients that ignore Max-Age.\n parts.push(`Expires=${new Date(Date.now() + options.maxAge * 1000).toUTCString()}`);\n }\n if (options.httpOnly ?? true) parts.push(\"HttpOnly\");\n if (options.secure) parts.push(\"Secure\");\n const sameSite = options.sameSite ?? \"lax\";\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n // Deleting a partitioned cookie needs the attribute too — callers pass the\n // same options for set and delete, so this covers both.\n if (options.partitioned) parts.push(\"Partitioned\");\n return parts.join(\"; \");\n}\n\nexport function deleteCookie(name: string, options: CookieOptions = {}): string {\n return serializeCookie(name, \"\", { ...options, maxAge: 0 });\n}\n","import { createRemoteJWKSet } from \"jose\";\nimport type { JWTVerifyGetKey } from \"jose\";\nimport type { ResolvedConfig } from \"./config.js\";\n\ninterface DiscoveryDocument {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n jwks_uri: string;\n}\n\nexport interface Endpoints {\n issuer: string;\n authorizationEndpoint: string;\n tokenEndpoint: string;\n jwks: JWTVerifyGetKey;\n}\n\nconst endpointsCache = new Map<string, Promise<Endpoints>>();\n\n/**\n * Split horizon: URLs the SERVER fetches (discovery, token endpoint, JWKS)\n * are rebased from the public `incortaUrl` onto `internalIncortaUrl` when\n * set. Values the BROWSER navigates to (authorization endpoint) and values\n * used for VALIDATION (issuer — must equal the token's `iss` claim, which\n * Incorta mints from its public URL) are never rewritten. URLs outside the\n * public base pass through untouched.\n */\nfunction toInternal(config: ResolvedConfig, url: string): string {\n const internal = config.internalIncortaUrl;\n if (!internal || !url.startsWith(config.incortaUrl)) return url;\n return internal + url.slice(config.incortaUrl.length);\n}\n\n/**\n * Resolves the tenant's OAuth endpoints, preferring the OIDC discovery document\n * and falling back to Incorta's fixed `/oauth/{tenant}/...` layout when the\n * document is unreachable. Cached per issuer (and internal-URL horizon) for\n * the process lifetime.\n */\nexport function getEndpoints(config: ResolvedConfig): Promise<Endpoints> {\n const cacheKey = `${config.issuer}|${config.internalIncortaUrl ?? \"\"}`;\n const cached = endpointsCache.get(cacheKey);\n if (cached) return cached;\n\n const promise = resolve(config).catch((error) => {\n // Do not cache failures — a temporarily unreachable Incorta should not\n // poison the process.\n endpointsCache.delete(cacheKey);\n throw error;\n });\n endpointsCache.set(cacheKey, promise);\n return promise;\n}\n\nasync function resolve(config: ResolvedConfig): Promise<Endpoints> {\n const fallback: DiscoveryDocument = {\n issuer: config.issuer,\n authorization_endpoint: `${config.issuer}/authorize`,\n token_endpoint: `${config.issuer}/token`,\n jwks_uri: `${config.issuer}/.well-known/jwks.json`,\n };\n\n let doc = fallback;\n try {\n const response = await config.fetch(toInternal(config, config.discoveryUrl), {\n headers: { accept: \"application/json\" },\n });\n if (response.ok) {\n const body = (await response.json()) as Partial<DiscoveryDocument>;\n doc = {\n issuer: body.issuer ?? fallback.issuer,\n authorization_endpoint: body.authorization_endpoint ?? fallback.authorization_endpoint,\n token_endpoint: body.token_endpoint ?? fallback.token_endpoint,\n jwks_uri: body.jwks_uri ?? fallback.jwks_uri,\n };\n }\n } catch {\n // Fall through to the fixed layout.\n }\n\n const jwks =\n config.jwksOverride ??\n createRemoteJWKSet(new URL(toInternal(config, doc.jwks_uri)), {\n cacheMaxAge: 10 * 60 * 1000,\n cooldownDuration: 30 * 1000,\n });\n\n return {\n issuer: doc.issuer,\n authorizationEndpoint: doc.authorization_endpoint,\n tokenEndpoint: toInternal(config, doc.token_endpoint),\n jwks,\n };\n}\n\n/** Test hook: clears the per-process endpoint cache. */\nexport function clearEndpointsCache(): void {\n endpointsCache.clear();\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\n/**\n * PKCE (RFC 7636) for the authorization-code flow. Incorta registers app\n * clients as PUBLIC clients (no client secret — see `registerClient`), so\n * the code verifier is what binds the authorization code to this server:\n * an intercepted code is worthless without it. The verifier lives in the\n * sealed login-state cookie between `/login` and `/callback`; only its\n * S256 challenge travels to Incorta.\n */\nexport function generateCodeVerifier(): string {\n // 32 random bytes → 43 base64url chars, inside RFC 7636's 43–128 range.\n return randomBytes(32).toString(\"base64url\");\n}\n\nexport function codeChallengeFor(codeVerifier: string): string {\n return createHash(\"sha256\").update(codeVerifier, \"ascii\").digest(\"base64url\");\n}\n\nexport const CODE_CHALLENGE_METHOD = \"S256\";\n","import { createHash } from \"node:crypto\";\nimport { EncryptJWT, jwtDecrypt } from \"jose\";\nimport type { ResolvedConfig, SessionData } from \"./config.js\";\n\n/**\n * Session and state cookies are sealed as encrypted JWTs (JWE, dir + A256GCM):\n * tamper-proof AND opaque, since the session carries the Incorta access/refresh\n * tokens. The 256-bit key is derived from the configured secret.\n */\nfunction sealKey(config: ResolvedConfig): Uint8Array {\n return new Uint8Array(\n createHash(\"sha256\").update(`incorta-auth:${config.secret}`).digest(),\n );\n}\n\ninterface SealedPayload {\n /** Guards against replaying one cookie kind as the other. */\n purpose: \"session\" | \"state\";\n data: unknown;\n}\n\nasync function seal(\n config: ResolvedConfig,\n purpose: SealedPayload[\"purpose\"],\n data: unknown,\n maxAgeSeconds: number,\n): Promise<string> {\n return new EncryptJWT({ purpose, data })\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(`${maxAgeSeconds}s`)\n .encrypt(sealKey(config));\n}\n\nasync function unseal<T>(\n config: ResolvedConfig,\n purpose: SealedPayload[\"purpose\"],\n cookieValue: string | undefined,\n): Promise<{ data: T; issuedAt: number; expiresAt: number } | null> {\n if (!cookieValue) return null;\n try {\n const { payload } = await jwtDecrypt(cookieValue, sealKey(config));\n if (payload.purpose !== purpose) return null;\n return {\n data: payload.data as T,\n issuedAt: (payload.iat ?? 0) * 1000,\n expiresAt: (payload.exp ?? 0) * 1000,\n };\n } catch {\n // Expired, tampered, or sealed with a different secret — treat as absent.\n return null;\n }\n}\n\nexport function sealSession(config: ResolvedConfig, session: SessionData): Promise<string> {\n return seal(config, \"session\", session, config.sessionMaxAge);\n}\n\nexport async function unsealSession(\n config: ResolvedConfig,\n cookieValue: string | undefined,\n): Promise<{ session: SessionData; issuedAt: number; expiresAt: number } | null> {\n const result = await unseal<SessionData>(config, \"session\", cookieValue);\n if (!result) return null;\n const { data, issuedAt, expiresAt } = result;\n if (!data || typeof data !== \"object\" || !data.user || !data.accessToken) return null;\n return { session: data, issuedAt, expiresAt };\n}\n\n/** Login round-trip state (CSRF protection + post-login destination). */\nexport interface LoginState {\n state: string;\n nonce: string;\n returnTo: string;\n /**\n * PKCE code verifier for this login attempt (absent only in a state cookie\n * sealed by a pre-PKCE build — the callback then exchanges without one).\n */\n codeVerifier?: string;\n}\n\nconst STATE_MAX_AGE_SECONDS = 10 * 60;\n\nexport function sealLoginState(config: ResolvedConfig, state: LoginState): Promise<string> {\n return seal(config, \"state\", state, STATE_MAX_AGE_SECONDS);\n}\n\nexport async function unsealLoginState(\n config: ResolvedConfig,\n cookieValue: string | undefined,\n): Promise<LoginState | null> {\n const result = await unseal<LoginState>(config, \"state\", cookieValue);\n if (!result) return null;\n const { data } = result;\n if (!data || typeof data.state !== \"string\" || typeof data.nonce !== \"string\") return null;\n if (data.codeVerifier !== undefined && typeof data.codeVerifier !== \"string\") return null;\n return data;\n}\n","import { decodeJwt, jwtVerify } from \"jose\";\nimport type { JWTPayload } from \"jose\";\nimport type { IncortaUser, ResolvedConfig } from \"./config.js\";\nimport { getEndpoints } from \"./discovery.js\";\n\nexport interface TokenSet {\n accessToken: string;\n /** Epoch millis derived from the access token's `exp` claim. */\n accessTokenExpiresAt: number;\n idToken?: string;\n refreshToken?: string;\n}\n\ninterface TokenEndpointResponse {\n access_token?: string;\n id_token?: string;\n refresh_token?: string;\n error?: string;\n error_description?: string;\n}\n\nexport class IncortaAuthError extends Error {\n constructor(\n readonly code: string,\n message: string,\n ) {\n super(`[incorta-auth] ${message}`);\n this.name = \"IncortaAuthError\";\n }\n}\n\nfunction basicAuth(clientId: string, clientSecret: string): string {\n const raw = `${clientId}:${clientSecret}`;\n return `Basic ${Buffer.from(raw, \"utf8\").toString(\"base64\")}`;\n}\n\n/**\n * How the token request identifies the client (RFC 6749 §2.3 / §4.1.3): a\n * confidential client authenticates with `client_secret_basic`; a public\n * client — what Incorta registers apps as — has no secret and sends\n * `client_id` in the body instead.\n */\nfunction clientAuthentication(\n config: ResolvedConfig,\n params: Record<string, string>,\n): { headers: Record<string, string>; params: Record<string, string> } {\n if (config.clientSecret) {\n return { headers: { authorization: basicAuth(config.clientId, config.clientSecret) }, params };\n }\n return { headers: {}, params: { client_id: config.clientId, ...params } };\n}\n\nasync function callTokenEndpoint(\n config: ResolvedConfig,\n params: Record<string, string>,\n): Promise<TokenSet> {\n const { tokenEndpoint } = await getEndpoints(config);\n const auth = clientAuthentication(config, params);\n\n let response: Response;\n try {\n response = await config.fetch(tokenEndpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n accept: \"application/json\",\n ...auth.headers,\n },\n body: new URLSearchParams(auth.params).toString(),\n });\n } catch (error) {\n throw new IncortaAuthError(\n \"incorta_unreachable\",\n `Could not reach the Incorta token endpoint (${tokenEndpoint}): ${(error as Error).message}`,\n );\n }\n\n let body: TokenEndpointResponse;\n try {\n body = (await response.json()) as TokenEndpointResponse;\n } catch {\n throw new IncortaAuthError(\n \"invalid_token_response\",\n `Token endpoint returned a non-JSON response (HTTP ${response.status}).`,\n );\n }\n\n if (!response.ok || !body.access_token) {\n throw new IncortaAuthError(\n body.error ?? \"token_exchange_failed\",\n body.error_description ?? `Token exchange failed (HTTP ${response.status}).`,\n );\n }\n\n const { exp } = decodeJwt(body.access_token);\n return {\n accessToken: body.access_token,\n accessTokenExpiresAt: exp ? exp * 1000 : Date.now() + 60 * 60 * 1000,\n idToken: body.id_token,\n refreshToken: body.refresh_token,\n };\n}\n\nexport function exchangeCode(\n config: ResolvedConfig,\n code: string,\n redirectUri: string,\n /** The PKCE verifier sealed at `/login`; Incorta checks it against the challenge the authorize request carried. */\n codeVerifier?: string,\n): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n ...(codeVerifier ? { code_verifier: codeVerifier } : {}),\n });\n}\n\nexport function refreshTokens(config: ResolvedConfig, refreshToken: string): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n });\n}\n\nasync function verifyJwt(config: ResolvedConfig, token: string): Promise<JWTPayload> {\n const { jwks, issuer } = await getEndpoints(config);\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer,\n audience: config.clientId,\n algorithms: [\"RS256\"],\n });\n return payload;\n } catch (error) {\n throw new IncortaAuthError(\n \"invalid_token\",\n `Token verification failed: ${(error as Error).message}`,\n );\n }\n}\n\nexport async function verifyAccessToken(\n config: ResolvedConfig,\n accessToken: string,\n): Promise<JWTPayload> {\n return verifyJwt(config, accessToken);\n}\n\n/**\n * Verifies the ID token (signature, issuer, audience, nonce) and maps its\n * claims to the SDK's user shape.\n */\nexport async function verifyIdToken(\n config: ResolvedConfig,\n idToken: string,\n expectedNonce: string,\n): Promise<IncortaUser> {\n const payload = await verifyJwt(config, idToken);\n\n if (payload.nonce !== expectedNonce) {\n throw new IncortaAuthError(\"nonce_mismatch\", \"ID token nonce does not match the login request.\");\n }\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new IncortaAuthError(\"invalid_token\", \"ID token is missing the sub claim.\");\n }\n\n const roles = Array.isArray(payload.roles)\n ? payload.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n\n return {\n sub: payload.sub,\n name: typeof payload.name === \"string\" ? payload.name : undefined,\n email: typeof payload.email === \"string\" ? payload.email : undefined,\n roles,\n tenant: typeof payload.tenant === \"string\" ? payload.tenant : config.tenant,\n };\n}\n\n/**\n * Fallback identity when the client was registered without the `openid` scope\n * (no ID token in the response): identify the user from the verified access\n * token instead. Name/email/roles are unavailable in that case.\n */\nexport async function userFromAccessToken(\n config: ResolvedConfig,\n accessToken: string,\n): Promise<IncortaUser> {\n const payload = await verifyAccessToken(config, accessToken);\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new IncortaAuthError(\"invalid_token\", \"Access token is missing the sub claim.\");\n }\n return {\n sub: payload.sub,\n roles: [],\n tenant: typeof payload.tenant === \"string\" ? payload.tenant : config.tenant,\n };\n}\n","interface ManagementErrorBody {\n error?: string;\n error_description?: string;\n}\n\nfunction registrationEndpoint(incortaUrl: string, tenant: string, clientId?: string): string {\n const base = `${incortaUrl.replace(/\\/+$/, \"\")}/oauth/${tenant}/register`;\n return clientId ? `${base}/${clientId}` : base;\n}\n\nasync function readJson<T extends ManagementErrorBody>(\n response: Response,\n what: string,\n): Promise<T> {\n let body: T;\n try {\n body = (await response.json()) as T;\n } catch {\n throw new Error(\n `[incorta-auth] ${what} returned a non-JSON response (HTTP ${response.status}). ` +\n \"Is the OAuth authorization server enabled on this cluster?\",\n );\n }\n if (!response.ok) {\n throw new Error(\n `[incorta-auth] ${what} failed (HTTP ${response.status}): ` +\n `${body.error ?? \"unknown_error\"} — ${body.error_description ?? \"no description\"}`,\n );\n }\n return body;\n}\n\nexport interface RegisterClientOptions {\n /** Incorta base URL, e.g. `http://localhost:8080/incorta`. */\n incortaUrl: string;\n /** Tenant to register the client in. */\n tenant: string;\n /** Display name of the client. */\n name: string;\n /** Allowed OAuth callback URLs, e.g. `http://localhost:4000/auth/callback`. */\n redirectUris: string[];\n /** @default \"openid profile email\" */\n scope?: string;\n /** @default [\"authorization_code\", \"refresh_token\"] */\n grantTypes?: string[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface RegisteredClient {\n clientId: string;\n /**\n * Only for a CONFIDENTIAL client. Incorta issues one solely to clients\n * holding the `client_credentials` grant (PAT-authenticated registration);\n * an app registered here is a PUBLIC client and gets none — token requests\n * then carry `client_id` alone, and PKCE protects the code flow.\n */\n clientSecret?: string;\n /** What Incorta decided: `none` for a public client, `client_secret_basic` for a confidential one. */\n tokenEndpointAuthMethod?: string;\n /** Bearer token for managing this registration (GET/PUT/DELETE it later). */\n registrationAccessToken?: string;\n /** URL to manage this registration. */\n registrationClientUri?: string;\n}\n\n/**\n * Registers an OAuth client in Incorta via RFC 7591 dynamic client\n * registration (`POST {incortaUrl}/oauth/{tenant}/register`). Incorta allows\n * unauthenticated registration for authorization_code clients and registers\n * them as PUBLIC clients: the response carries the generated client_id (and a\n * client_secret only on servers that still issue one) — store it once, e.g.\n * in the app's `.env`.\n */\nexport async function registerClient(\n options: RegisterClientOptions,\n): Promise<RegisteredClient> {\n const {\n incortaUrl,\n tenant,\n name,\n redirectUris,\n scope = \"openid profile email\",\n grantTypes = [\"authorization_code\", \"refresh_token\"],\n fetchImpl = fetch,\n } = options;\n\n if (!redirectUris.length) {\n throw new Error(\"[incorta-auth] registerClient requires at least one redirect URI.\");\n }\n\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", accept: \"application/json\" },\n body: JSON.stringify({\n client_name: name,\n redirect_uris: redirectUris,\n grant_types: grantTypes,\n scope,\n token_endpoint_auth_method: \"client_secret_basic\",\n }),\n });\n\n interface RegistrationResponse extends ManagementErrorBody {\n client_id?: string;\n client_secret?: string;\n token_endpoint_auth_method?: string;\n registration_access_token?: string;\n registration_client_uri?: string;\n }\n\n const body = await readJson<RegistrationResponse>(response, \"Client registration\");\n if (!body.client_id) {\n throw new Error(`[incorta-auth] Client registration failed: response is missing client_id.`);\n }\n\n return {\n clientId: body.client_id,\n clientSecret: body.client_secret || undefined,\n tokenEndpointAuthMethod: body.token_endpoint_auth_method,\n registrationAccessToken: body.registration_access_token,\n registrationClientUri: body.registration_client_uri,\n };\n}\n\nexport interface ManageClientOptions {\n /** Incorta base URL, e.g. `http://localhost:8080/incorta`. */\n incortaUrl: string;\n tenant: string;\n clientId: string;\n /** The registration access token returned when the client was registered. */\n registrationAccessToken: string;\n fetchImpl?: typeof fetch;\n}\n\n/** Current metadata of a registered client. */\nexport interface ClientInfo {\n clientId: string;\n name?: string;\n redirectUris: string[];\n grantTypes: string[];\n scope?: string;\n}\n\ninterface ClientMetadataResponse extends ManagementErrorBody {\n client_id?: string;\n client_name?: string;\n redirect_uris?: string[];\n grant_types?: string[];\n scope?: string;\n}\n\nfunction toClientInfo(body: ClientMetadataResponse, fallbackClientId: string): ClientInfo {\n return {\n clientId: body.client_id ?? fallbackClientId,\n name: body.client_name,\n redirectUris: body.redirect_uris ?? [],\n grantTypes: body.grant_types ?? [],\n scope: body.scope,\n };\n}\n\n/**\n * Reads a registered client's current metadata (RFC 7592-style management,\n * `GET {incortaUrl}/oauth/{tenant}/register/{clientId}` with the registration\n * access token).\n */\nexport async function getClient(options: ManageClientOptions): Promise<ClientInfo> {\n const { incortaUrl, tenant, clientId, registrationAccessToken, fetchImpl = fetch } = options;\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n });\n const body = await readJson<ClientMetadataResponse>(response, \"Reading the client\");\n return toClientInfo(body, clientId);\n}\n\nexport interface UpdateRedirectUrisOptions extends ManageClientOptions {\n redirectUris: string[];\n /**\n * `\"add\"` (default) merges the given URIs into the client's current set;\n * `\"replace\"` makes the given list the exact new set (also how URIs are\n * removed).\n */\n mode?: \"add\" | \"replace\";\n}\n\n/**\n * Updates the allowed OAuth callback URLs of a registered client. Incorta's\n * management endpoint applies `redirect_uris` from the PUT body (other\n * metadata is unchanged server-side).\n */\nexport async function updateClientRedirectUris(\n options: UpdateRedirectUrisOptions,\n): Promise<ClientInfo> {\n const {\n incortaUrl,\n tenant,\n clientId,\n registrationAccessToken,\n redirectUris,\n mode = \"add\",\n fetchImpl = fetch,\n } = options;\n\n if (!redirectUris.length) {\n // An empty replace would leave the client unable to complete the\n // authorization-code flow at all — delete the registration instead.\n throw new Error(\"[incorta-auth] updateClientRedirectUris requires at least one redirect URI.\");\n }\n\n const current = await getClient(options);\n const nextUris =\n mode === \"add\" ? [...new Set([...current.redirectUris, ...redirectUris])] : redirectUris;\n\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n method: \"PUT\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n // RFC 7592 update: client_id + full metadata. Incorta applies\n // redirect_uris only, but nimbus-side parsing wants the full shape.\n body: JSON.stringify({\n client_id: clientId,\n redirect_uris: nextUris,\n grant_types: current.grantTypes,\n scope: current.scope,\n client_name: current.name,\n }),\n });\n\n if (!response.ok) {\n await readJson<ManagementErrorBody>(response, \"Updating the client\"); // throws with details\n }\n // The PUT response body is empty on success — re-read for the fresh state.\n return getClient(options);\n}\n\n/**\n * Deletes a registered client (RFC 7592-style management,\n * `DELETE {incortaUrl}/oauth/{tenant}/register/{clientId}` with the\n * registration access token). Existing sessions in apps using the client keep\n * working until they expire; new logins and token refreshes stop immediately.\n */\nexport async function deleteClient(options: ManageClientOptions): Promise<void> {\n const { incortaUrl, tenant, clientId, registrationAccessToken, fetchImpl = fetch } = options;\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n method: \"DELETE\",\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n });\n if (!response.ok) {\n await readJson<ManagementErrorBody>(response, \"Deleting the client\"); // throws with details\n }\n}\n","import type { AppAccess } from \"./core/app-access.js\";\nimport { resolveConfig } from \"./core/config.js\";\nimport type { IncortaAuthConfig, ResolvedConfig } from \"./core/config.js\";\nimport { createHandler } from \"./core/handler.js\";\nimport type { AuthGate, AuthHandler, AuthSession } from \"./core/handler.js\";\n\nexport {\n AppAccessTokenRejectedError,\n AppAccessUnavailableError,\n EDIT_BIT,\n SHARE_BIT,\n VIEW_BIT,\n} from \"./core/app-access.js\";\nexport type { AppAccess, AppAccessPolicy } from \"./core/app-access.js\";\nexport type {\n IncortaAuthConfig,\n IncortaUser,\n SessionData,\n SessionInfo,\n} from \"./core/config.js\";\nexport type { AuthGate, AuthHandler, AuthSession } from \"./core/handler.js\";\nexport { IncortaAuthError } from \"./core/tokens.js\";\nexport {\n deleteClient,\n getClient,\n registerClient,\n updateClientRedirectUris,\n} from \"./core/register.js\";\nexport type {\n ClientInfo,\n ManageClientOptions,\n RegisterClientOptions,\n RegisteredClient,\n UpdateRedirectUrisOptions,\n} from \"./core/register.js\";\n\nexport interface IncortaAuth {\n /**\n * Web-standard request handler serving the auth routes under\n * `config.basePath` (default `/auth`): `/login`, `/callback`, `/session`,\n * `/logout` (+ `/token` when `exposeAccessToken` is on).\n *\n * Wire it to anything that speaks `Request`/`Response` — a TanStack Start\n * server route, Bun.serve, or via `@incorta/auth/node` / `@incorta/auth/express`\n * for Node servers.\n */\n handler: AuthHandler;\n /**\n * Reads the session from a web-standard `Request` (server-side gating, API\n * routes, server functions). Returns null when the user is not signed in.\n */\n getSession: (request: Request) => Promise<AuthSession | null>;\n /**\n * The whole-app gate: serve the returned `Response` when non-null (auth\n * routes, login redirects, access-denied/unavailable pages), continue to\n * your app on `null`. Enforces `config.appAccess` — `\"public\"` lets every\n * request through, `\"authenticated\"` requires a session, `\"catalog\"`\n * additionally requires at least VIEW access to this app in the Incorta\n * catalog (fails CLOSED with a 503 page when Incorta cannot answer).\n */\n gate: AuthGate;\n /**\n * The Incorta catalog's verdict about `session`'s user and THIS app —\n * registration, permission bits, and the gate's `allowed` outcome. Served\n * from a 30s per-user cache (the same one `gate` uses in `\"catalog\"`\n * mode). Throws `AppAccessUnavailableError` when Incorta cannot answer and\n * `AppAccessTokenRejectedError` when it rejects the session token. Useful\n * for permission-aware UX (e.g. hiding editor-only affordances) in any\n * `appAccess` mode.\n */\n getAppAccess: (session: AuthSession) => Promise<AppAccess>;\n /** The resolved configuration (secrets included — do not log). */\n config: ResolvedConfig;\n}\n\n/**\n * Creates an IncortaAuth instance: OAuth 2.0 authorization-code flow against\n * Incorta's built-in authorization server. Unauthenticated users are redirected\n * to the Incorta login page and returned to your app once signed in.\n */\nexport function createIncortaAuth(config: IncortaAuthConfig = {}): IncortaAuth {\n const resolved = resolveConfig(config);\n const { handler, getSession, gate, getAppAccess } = createHandler(resolved);\n return { handler, getSession, gate, getAppAccess, config: resolved };\n}\n"],"mappings":"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","names":["randomBytes","createHash","randomBytes","code"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/config.ts","../src/core/handler.ts","../src/core/cookies.ts","../src/core/discovery.ts","../src/core/pkce.ts","../src/core/session.ts","../src/core/tokens.ts","../src/core/register.ts","../src/index.ts"],"sourcesContent":["import type { JWTVerifyGetKey } from \"jose\";\nimport type { AppAccessPolicy } from \"./app-access.js\";\n\n/** The Incorta user identity carried by the session (claims from the ID token). */\nexport interface IncortaUser {\n /** Incorta login name (`sub` claim). */\n sub: string;\n name?: string;\n email?: string;\n /** Incorta role names, as minted into the ID token. */\n roles: string[];\n /** Tenant the user authenticated against. */\n tenant: string;\n}\n\n/** What the sealed session cookie holds. */\nexport interface SessionData {\n user: IncortaUser;\n /** Incorta-issued access token (RS256 JWT, aud = client_id). */\n accessToken: string;\n /** Epoch millis when the access token expires. */\n accessTokenExpiresAt: number;\n /** Rotating refresh token, present when the client has the refresh_token grant. */\n refreshToken?: string;\n /** Epoch millis when this session was first created (login time). */\n createdAt: number;\n}\n\nexport interface SessionInfo {\n user: IncortaUser;\n /** Epoch millis when the session cookie expires (sliding). */\n expiresAt: number;\n /** Epoch millis when the access token expires. */\n accessTokenExpiresAt: number;\n}\n\nexport interface IncortaAuthConfig {\n /**\n * Base URL of the Incorta web application, e.g. `https://acme.cloud.incorta.com/incorta`\n * or `http://localhost:8080/incorta`. Defaults to `INCORTA_URL`.\n */\n incortaUrl?: string;\n /**\n * Split-horizon override: base URL this SERVER should use to reach Incorta\n * when the public `incortaUrl` is not routable from where the app runs\n * (containers, cluster-internal DNS) — e.g.\n * `http://host.docker.internal:8080/incorta` from a local container. Only\n * server-to-server calls (discovery, token exchange, JWKS) use it; browser\n * redirects and token `iss` validation stay on the public `incortaUrl`.\n * Defaults to `INCORTA_INTERNAL_URL`; unset means no rewrite.\n */\n internalIncortaUrl?: string;\n /** Incorta tenant name, e.g. `demo`. Defaults to `INCORTA_TENANT`. */\n tenant?: string;\n /** OAuth client id (see `incorta-auth register`). Defaults to `INCORTA_CLIENT_ID`. */\n clientId?: string;\n /**\n * OAuth client secret — only for a CONFIDENTIAL client. Incorta's dynamic\n * registration issues app clients as PUBLIC clients (no secret; see\n * `registerClient`), and the SDK then authenticates token requests with\n * `client_id` alone and relies on PKCE. Defaults to `INCORTA_CLIENT_SECRET`;\n * unset or empty means public.\n */\n clientSecret?: string;\n /**\n * Secret used to encrypt the session cookie (any long random string, >= 32 chars).\n * Defaults to `INCORTA_AUTH_SECRET`.\n */\n secret?: string;\n /**\n * Path prefix the auth routes are mounted under. The SDK serves\n * `{basePath}/login`, `{basePath}/callback`, `{basePath}/session`,\n * `{basePath}/logout` (and optionally `{basePath}/token`).\n * @default \"/auth\"\n */\n basePath?: string;\n /**\n * External origin of THIS app, e.g. `http://localhost:4000`. Used to build the\n * OAuth callback URL and to validate `redirect_to` targets. When omitted it is\n * derived per request from `x-forwarded-proto`/`x-forwarded-host`/`host`.\n * Defaults to `INCORTA_APP_URL`.\n */\n appUrl?: string;\n /** OAuth scopes to request. @default \"openid profile email\" */\n scope?: string;\n session?: {\n /** @default \"incorta_auth_session\" */\n cookieName?: string;\n /** Session lifetime in seconds (sliding — renewed on activity). @default 43200 (12h) */\n maxAge?: number;\n };\n cookies?: {\n /**\n * `Secure` attribute for cookies. `\"auto\"` sets it when the request is HTTPS.\n * @default \"auto\"\n */\n secure?: boolean | \"auto\";\n /**\n * `SameSite` for the session/state cookies. The `\"lax\"` default is right\n * for top-level apps. Set `\"none\"` when the app runs inside a CROSS-SITE\n * iframe (e.g. an embedded preview) — Lax cookies are never sent there,\n * which loops the login flow forever. `\"none\"` forces the `Secure`\n * attribute (browsers require the pair; Chrome still accepts it on\n * trustworthy plain-http origins like `*.localhost`). Defaults to\n * `INCORTA_COOKIE_SAMESITE`.\n * @default \"lax\"\n */\n sameSite?: \"lax\" | \"none\";\n /**\n * CHIPS: adds the `Partitioned` attribute, giving each embedding\n * top-level site its own cookie jar — what modern Chrome requires for\n * cookies in third-party iframes. Use together with `sameSite: \"none\"`.\n * Defaults to `INCORTA_COOKIE_PARTITIONED` (`1`/`true`).\n * @default false\n */\n partitioned?: boolean;\n };\n /**\n * Expose `GET {basePath}/token` returning the raw Incorta access token to the\n * browser (for calling Incorta public APIs directly from the frontend). Off by\n * default — prefer proxying through your backend.\n * @default false\n */\n exposeAccessToken?: boolean;\n /**\n * Who may use the app — enforced by the whole-app gate (`auth.gate`, and\n * the framework middlewares built on it):\n *\n * - `\"public\"`: anyone with the URL, signed in or not (login stays\n * available, sessions become optional);\n * - `\"authenticated\"` (default): any signed-in Incorta user;\n * - `\"catalog\"`: signed-in AND at least VIEW access to this app in the\n * Incorta catalog, verified against `/apps-catalog/check` with a 30s\n * per-user cache. Unregistered apps stay open to authenticated users\n * (legacy-open); an unanswerable check fails CLOSED (503).\n *\n * Normally stamped by the Incorta Apps platform via `INCORTA_APP_ACCESS`.\n * An unrecognized env value falls back to `\"authenticated\"` (never to\n * `\"public\"` — a typo must not open the app) with a console warning.\n */\n appAccess?: AppAccessPolicy;\n /**\n * How to sign in when the app runs inside a cross-site iframe (the Incorta\n * builder preview). `\"redirect\"` (default) bounces the frame to Incorta —\n * correct top-level, but when the Incorta cluster has iframe inclusion OFF\n * (its login is `X-Frame-Options: SAMEORIGIN` and its session cookie is\n * `SameSite=Lax`) the login can't render/complete in the frame. `\"new-tab\"`\n * serves an interstitial that, inside a frame, offers a \"Sign in with\n * Incorta\" button opening the app top-level in a new tab (where login\n * works); at the top level it still redirects straight to Incorta.\n * Env fallback: `INCORTA_EMBEDDED_SIGN_IN`.\n * @default \"redirect\"\n */\n embeddedSignIn?: \"redirect\" | \"new-tab\";\n advanced?: {\n /** Override the OIDC discovery document URL (rarely needed). */\n discoveryUrl?: string;\n /** Override JWKS resolution (used by tests to verify against a local key). */\n jwks?: JWTVerifyGetKey;\n /** Override fetch (used by tests). */\n fetch?: typeof fetch;\n };\n}\n\nexport interface ResolvedConfig {\n incortaUrl: string;\n /** Server-side fetch base when set (split horizon) — see the config doc. */\n internalIncortaUrl?: string;\n tenant: string;\n clientId: string;\n /** Absent for a public client: token requests then carry `client_id` instead of Basic auth. */\n clientSecret?: string;\n secret: string;\n basePath: string;\n appUrl?: string;\n scope: string;\n cookieName: string;\n stateCookieName: string;\n sessionMaxAge: number;\n secureCookies: boolean | \"auto\";\n cookieSameSite: \"lax\" | \"none\";\n cookiePartitioned: boolean;\n exposeAccessToken: boolean;\n appAccess: AppAccessPolicy;\n embeddedSignIn: \"redirect\" | \"new-tab\";\n /** `{incortaUrl}/oauth/{tenant}` — the OIDC issuer for this tenant. */\n issuer: string;\n discoveryUrl: string;\n jwksOverride?: JWTVerifyGetKey;\n fetch: typeof fetch;\n}\n\nfunction resolveAppAccess(\n input: AppAccessPolicy | undefined,\n envValue: string | undefined,\n): AppAccessPolicy {\n if (input) return input;\n const raw = envValue?.trim().toLowerCase();\n if (!raw) return \"authenticated\";\n if (raw === \"public\" || raw === \"authenticated\" || raw === \"catalog\") return raw;\n // Fail SAFE, not open: garbage in the platform-stamped env keeps the\n // login-only posture rather than silently exposing or bricking the app.\n console.warn(\n `[incorta-auth] Unrecognized INCORTA_APP_ACCESS value \"${envValue}\" — using \"authenticated\".`,\n );\n return \"authenticated\";\n}\n\nfunction required(value: string | undefined, name: string, envVar: string): string {\n if (!value) {\n throw new Error(\n `[incorta-auth] Missing required config \"${name}\" (or env var ${envVar}).`,\n );\n }\n return value;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nexport function resolveConfig(input: IncortaAuthConfig = {}): ResolvedConfig {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env ?? {};\n\n const incortaUrl = stripTrailingSlash(\n required(input.incortaUrl ?? env.INCORTA_URL, \"incortaUrl\", \"INCORTA_URL\"),\n );\n const internalIncortaUrl = input.internalIncortaUrl ?? env.INCORTA_INTERNAL_URL;\n const tenant = required(input.tenant ?? env.INCORTA_TENANT, \"tenant\", \"INCORTA_TENANT\");\n const clientId = required(\n input.clientId ?? env.INCORTA_CLIENT_ID,\n \"clientId\",\n \"INCORTA_CLIENT_ID\",\n );\n // Optional: a public client (Incorta's default for registered apps) has none.\n const clientSecret = (input.clientSecret ?? env.INCORTA_CLIENT_SECRET)?.trim() || undefined;\n const secret = required(\n input.secret ?? env.INCORTA_AUTH_SECRET,\n \"secret\",\n \"INCORTA_AUTH_SECRET\",\n );\n if (secret.length < 32) {\n throw new Error(\n \"[incorta-auth] `secret` must be at least 32 characters. Generate one with: openssl rand -base64 32\",\n );\n }\n\n const basePath = input.basePath ?? \"/auth\";\n if (!basePath.startsWith(\"/\") || basePath.endsWith(\"/\")) {\n throw new Error(\n `[incorta-auth] basePath must start with \"/\" and not end with \"/\" (got \"${basePath}\").`,\n );\n }\n\n const appUrl = input.appUrl ?? env.INCORTA_APP_URL;\n const cookieName = input.session?.cookieName ?? \"incorta_auth_session\";\n const issuer = `${incortaUrl}/oauth/${tenant}`;\n\n const envSameSite = env.INCORTA_COOKIE_SAMESITE?.toLowerCase();\n const cookieSameSite =\n input.cookies?.sameSite ??\n (envSameSite === \"none\" || envSameSite === \"lax\" ? envSameSite : \"lax\");\n const envPartitioned = env.INCORTA_COOKIE_PARTITIONED?.toLowerCase();\n const cookiePartitioned =\n input.cookies?.partitioned ??\n (envPartitioned === \"1\" || envPartitioned === \"true\");\n if (cookiePartitioned && cookieSameSite !== \"none\") {\n throw new Error(\n '[incorta-auth] `cookies.partitioned` requires `cookies.sameSite: \"none\"` — browsers only partition cross-site cookies.',\n );\n }\n\n return {\n incortaUrl,\n internalIncortaUrl: internalIncortaUrl\n ? stripTrailingSlash(internalIncortaUrl)\n : undefined,\n tenant,\n clientId,\n clientSecret,\n secret,\n basePath,\n appUrl: appUrl ? stripTrailingSlash(appUrl) : undefined,\n scope: input.scope ?? \"openid profile email\",\n cookieName,\n stateCookieName: `${cookieName}_state`,\n sessionMaxAge: input.session?.maxAge ?? 12 * 60 * 60,\n secureCookies: input.cookies?.secure ?? \"auto\",\n cookieSameSite,\n cookiePartitioned,\n exposeAccessToken: input.exposeAccessToken ?? false,\n appAccess: resolveAppAccess(input.appAccess, env.INCORTA_APP_ACCESS),\n embeddedSignIn:\n input.embeddedSignIn ??\n (env.INCORTA_EMBEDDED_SIGN_IN?.toLowerCase() === \"new-tab\"\n ? \"new-tab\"\n : \"redirect\"),\n issuer,\n discoveryUrl:\n input.advanced?.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`,\n jwksOverride: input.advanced?.jwks,\n fetch: input.advanced?.fetch ?? fetch,\n };\n}\n","import { randomBytes } from \"node:crypto\";\nimport {\n type AppAccess,\n AppAccessTokenRejectedError,\n AppAccessUnavailableError,\n createAppAccessChecker,\n} from \"./app-access.js\";\nimport type { ResolvedConfig, SessionData, SessionInfo } from \"./config.js\";\nimport { deleteCookie, parseCookies, serializeCookie } from \"./cookies.js\";\nimport { getEndpoints } from \"./discovery.js\";\nimport { CODE_CHALLENGE_METHOD, codeChallengeFor, generateCodeVerifier } from \"./pkce.js\";\nimport {\n sealLoginState,\n sealSession,\n unsealLoginState,\n unsealSession,\n} from \"./session.js\";\nimport {\n IncortaAuthError,\n exchangeCode,\n refreshTokens,\n userFromAccessToken,\n verifyIdToken,\n} from \"./tokens.js\";\n\n/** A read-only view of the current session for server-side app code. */\nexport interface AuthSession extends SessionInfo {\n /** Incorta access token — pass as `Authorization: Bearer` to Incorta APIs. */\n accessToken: string;\n /**\n * The catalog verdict for this user and app, when a gate already resolved\n * it on this request (`appAccess: \"catalog\"` policy). Absent otherwise —\n * fetch on demand with `auth.getAppAccess(session)`.\n */\n appAccess?: AppAccess;\n}\n\nexport type AuthHandler = (request: Request) => Promise<Response>;\n\n/**\n * Whole-app gate: a `Response` to short-circuit with (auth routes, login\n * redirects, deny/unavailable pages) or `null` for \"let the request through\n * to the app\".\n */\nexport type AuthGate = (request: Request) => Promise<Response | null>;\n\n/** Refresh the access token when it has less than this long left to live. */\nconst ACCESS_TOKEN_REFRESH_LEEWAY_MS = 60 * 1000;\n\nfunction requestOrigin(config: ResolvedConfig, request: Request): string {\n if (config.appUrl) return config.appUrl;\n const url = new URL(request.url);\n const proto =\n request.headers.get(\"x-forwarded-proto\")?.split(\",\")[0]?.trim() ??\n url.protocol.replace(\":\", \"\");\n const host =\n request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim() ??\n request.headers.get(\"host\") ??\n url.host;\n return `${proto}://${host}`;\n}\n\nfunction isSecureRequest(config: ResolvedConfig, request: Request): boolean {\n if (config.secureCookies !== \"auto\") return config.secureCookies;\n return requestOrigin(config, request).startsWith(\"https://\");\n}\n\n/**\n * Only same-origin destinations are allowed after login/logout — anything else\n * would let a crafted link bounce users through this app to an attacker page.\n */\nfunction validateReturnTo(raw: string | null, origin: string): string {\n if (!raw) return \"/\";\n if (raw.startsWith(\"/\") && !raw.startsWith(\"//\")) return raw;\n try {\n const url = new URL(raw);\n if (url.origin === origin) return `${url.pathname}${url.search}${url.hash}` || \"/\";\n } catch {\n // Not a URL — fall through.\n }\n return \"/\";\n}\n\nfunction json(status: number, body: unknown, cookies: string[] = []): Response {\n const headers = new Headers({\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(JSON.stringify(body), { status, headers });\n}\n\nfunction redirect(location: string, cookies: string[] = []): Response {\n const headers = new Headers({ location, \"cache-control\": \"no-store\" });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(null, { status: 302, headers });\n}\n\nfunction withErrorParam(target: string, code: string): string {\n const separator = target.includes(\"?\") ? \"&\" : \"?\";\n return `${target}${separator}incorta_auth_error=${encodeURIComponent(code)}`;\n}\n\nfunction randomToken(): string {\n return randomBytes(16).toString(\"base64url\");\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/** Browser navigations get pages/redirects; data requests get JSON. */\nfunction wantsHtml(request: Request): boolean {\n return (\n request.method.toUpperCase() === \"GET\" &&\n (request.headers.get(\"accept\") ?? \"\").includes(\"text/html\")\n );\n}\n\nfunction requestPath(url: URL): string {\n return `${url.pathname}${url.search}`;\n}\n\nexport function createHandler(config: ResolvedConfig): {\n handler: AuthHandler;\n getSession: (request: Request) => Promise<AuthSession | null>;\n gate: AuthGate;\n getAppAccess: (session: AuthSession) => Promise<AppAccess>;\n} {\n const appAccessChecker = createAppAccessChecker(config);\n\n const sessionCookieOptions = (secure: boolean) => ({\n httpOnly: true,\n // SameSite=None requires Secure — force the pair even on plain http\n // (Chrome accepts Secure cookies from trustworthy origins like\n // `*.localhost`; real cross-site embedding needs https anyway).\n secure: config.cookieSameSite === \"none\" ? true : secure,\n sameSite: config.cookieSameSite,\n partitioned: config.cookiePartitioned,\n path: \"/\",\n });\n\n async function login(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const secure = isSecureRequest(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n\n // Already signed in — skip the OAuth round trip entirely.\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const existing = await unsealSession(config, cookies[config.cookieName]);\n if (existing) return redirect(returnTo);\n\n const state = randomToken();\n const nonce = randomToken();\n // PKCE binds the code to this server (pkce.ts) — the verifier stays in the\n // sealed cookie, only its challenge goes to Incorta.\n const codeVerifier = generateCodeVerifier();\n const stateCookie = serializeCookie(\n config.stateCookieName,\n await sealLoginState(config, { state, nonce, returnTo, codeVerifier }),\n { ...sessionCookieOptions(secure), maxAge: 600 },\n );\n\n const { authorizationEndpoint } = await getEndpoints(config);\n const authorizeUrl = new URL(authorizationEndpoint);\n authorizeUrl.searchParams.set(\"response_type\", \"code\");\n authorizeUrl.searchParams.set(\"client_id\", config.clientId);\n authorizeUrl.searchParams.set(\"redirect_uri\", `${origin}${config.basePath}/callback`);\n authorizeUrl.searchParams.set(\"scope\", config.scope);\n authorizeUrl.searchParams.set(\"state\", state);\n authorizeUrl.searchParams.set(\"nonce\", nonce);\n authorizeUrl.searchParams.set(\"code_challenge\", codeChallengeFor(codeVerifier));\n authorizeUrl.searchParams.set(\"code_challenge_method\", CODE_CHALLENGE_METHOD);\n\n // Embedded apps that can't complete Incorta login in a cross-site iframe\n // get a framing-aware interstitial instead of a doomed in-frame redirect.\n if (config.embeddedSignIn === \"new-tab\") {\n return newTabSignInPage(authorizeUrl.toString(), `${origin}/`, [stateCookie]);\n }\n return redirect(authorizeUrl.toString(), [stateCookie]);\n }\n\n async function callback(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const secure = isSecureRequest(config, request);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const clearState = deleteCookie(config.stateCookieName, sessionCookieOptions(secure));\n\n const loginState = await unsealLoginState(config, cookies[config.stateCookieName]);\n const returnTo = validateReturnTo(loginState?.returnTo ?? null, origin);\n\n const fail = (code: string) => redirect(withErrorParam(returnTo, code), [clearState]);\n\n const oauthError = url.searchParams.get(\"error\");\n if (oauthError) return fail(oauthError);\n\n const code = url.searchParams.get(\"code\");\n const state = url.searchParams.get(\"state\");\n if (!code || !state) return fail(\"missing_code\");\n if (!loginState) return fail(\"state_missing\");\n if (state !== loginState.state) return fail(\"state_mismatch\");\n\n try {\n const tokens = await exchangeCode(\n config,\n code,\n `${origin}${config.basePath}/callback`,\n loginState.codeVerifier,\n );\n const user = tokens.idToken\n ? await verifyIdToken(config, tokens.idToken, loginState.nonce)\n : await userFromAccessToken(config, tokens.accessToken);\n\n const session: SessionData = {\n user,\n accessToken: tokens.accessToken,\n accessTokenExpiresAt: tokens.accessTokenExpiresAt,\n refreshToken: tokens.refreshToken,\n createdAt: Date.now(),\n };\n const sessionCookie = serializeCookie(\n config.cookieName,\n await sealSession(config, session),\n { ...sessionCookieOptions(secure), maxAge: config.sessionMaxAge },\n );\n return redirect(returnTo, [sessionCookie, clearState]);\n } catch (error) {\n const code = error instanceof IncortaAuthError ? error.code : \"callback_failed\";\n return fail(code);\n }\n }\n\n /**\n * Loads the session from the request cookie; refreshes the Incorta access\n * token (rotating the refresh token) when it is about to expire, and renews\n * the sliding session cookie past its half-life. Returns the cookies to set.\n */\n async function loadSession(request: Request): Promise<{\n session: SessionData | null;\n sessionExpiresAt: number;\n setCookies: string[];\n }> {\n const secure = isSecureRequest(config, request);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unsealed = await unsealSession(config, cookies[config.cookieName]);\n if (!unsealed) return { session: null, sessionExpiresAt: 0, setCookies: [] };\n\n let { session } = unsealed;\n let mustReseal = false;\n const now = Date.now();\n\n if (session.accessTokenExpiresAt - now < ACCESS_TOKEN_REFRESH_LEEWAY_MS) {\n if (session.refreshToken) {\n try {\n const tokens = await refreshTokens(config, session.refreshToken);\n session = {\n ...session,\n accessToken: tokens.accessToken,\n accessTokenExpiresAt: tokens.accessTokenExpiresAt,\n refreshToken: tokens.refreshToken ?? session.refreshToken,\n };\n mustReseal = true;\n } catch {\n // Refresh failed (expired/rotated elsewhere). Keep the session while\n // the current access token is still usable; otherwise sign out.\n if (session.accessTokenExpiresAt <= now) {\n return {\n session: null,\n sessionExpiresAt: 0,\n setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))],\n };\n }\n }\n } else if (session.accessTokenExpiresAt <= now) {\n return {\n session: null,\n sessionExpiresAt: 0,\n setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))],\n };\n }\n }\n\n const halfLifeMs = (config.sessionMaxAge * 1000) / 2;\n if (now - unsealed.issuedAt > halfLifeMs) mustReseal = true;\n\n if (!mustReseal) {\n return { session, sessionExpiresAt: unsealed.expiresAt, setCookies: [] };\n }\n const resealed = serializeCookie(config.cookieName, await sealSession(config, session), {\n ...sessionCookieOptions(secure),\n maxAge: config.sessionMaxAge,\n });\n return {\n session,\n sessionExpiresAt: now + config.sessionMaxAge * 1000,\n setCookies: [resealed],\n };\n }\n\n async function sessionRoute(request: Request): Promise<Response> {\n const { session, sessionExpiresAt, setCookies } = await loadSession(request);\n if (!session) return json(200, { user: null }, setCookies);\n return json(\n 200,\n {\n user: session.user,\n session: {\n expiresAt: sessionExpiresAt,\n accessTokenExpiresAt: session.accessTokenExpiresAt,\n },\n },\n setCookies,\n );\n }\n\n async function tokenRoute(request: Request): Promise<Response> {\n if (!config.exposeAccessToken) return json(404, { error: \"not_found\" });\n const { session, setCookies } = await loadSession(request);\n if (!session) return json(401, { error: \"unauthenticated\" }, setCookies);\n return json(\n 200,\n { accessToken: session.accessToken, expiresAt: session.accessTokenExpiresAt },\n setCookies,\n );\n }\n\n /**\n * `GET {basePath}/refresh?redirect_to=…` — refresh the access token (and\n * reseal the rotated cookie) via a browser round trip, then bounce back.\n * The catalog gate sends navigations here when the session's access token\n * has already expired: the gate itself returns `null` to continue, so it\n * has no response to carry a rotated refresh-token cookie on — a redirect\n * does. A session that cannot be refreshed is sent to login instead\n * (loadSession already cleared its cookie).\n */\n async function refreshRoute(request: Request, url: URL): Promise<Response> {\n const origin = requestOrigin(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n const { session, setCookies } = await loadSession(request);\n if (!session) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(returnTo)}`,\n setCookies,\n );\n }\n return redirect(returnTo, setCookies);\n }\n\n async function logout(request: Request, url: URL): Promise<Response> {\n const secure = isSecureRequest(config, request);\n const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));\n if (request.method === \"GET\") {\n // No explicit redirect_to → the signed-out page, NOT \"/\": in an\n // SSO-gated app, landing on a gated page silently re-runs the login\n // flow and (while the Incorta session lives) signs the user straight\n // back in — sign-out must land somewhere UNGATED to visibly stick.\n const raw = url.searchParams.get(\"redirect_to\");\n if (!raw) return redirect(`${config.basePath}/signed-out`, [clearSession]);\n const origin = requestOrigin(config, request);\n return redirect(validateReturnTo(raw, origin), [clearSession]);\n }\n return json(200, { ok: true }, [clearSession]);\n }\n\n /**\n * Ungated landing page after sign-out (it lives under basePath, so app\n * gates pass it through). Without it there is nowhere for a fully-gated\n * app to show \"you are signed out\" — every app page would bounce through\n * the IdP and re-login. Self-contained HTML, no assets.\n */\n function signedOutPage(): Response {\n const signInHref = `${config.basePath}/login?redirect_to=%2F`;\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Signed out</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n a { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; font-size: .9rem;\n text-decoration: none; }\n </style>\n </head>\n <body>\n <main>\n <h1>You’ve signed out of this app</h1>\n <p>\n Your Incorta account may still be signed in elsewhere — signing back\n in here is one click.\n </p>\n <a href=\"${signInHref}\">Sign in with Incorta</a>\n </main>\n </body>\n</html>`,\n {\n status: 200,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n },\n );\n }\n\n /**\n * `embeddedSignIn: \"new-tab\"` interstitial. The inline script decides\n * client-side: at the top level it redirects straight to Incorta; inside a\n * cross-site iframe (where Incorta login can't render/complete when the\n * cluster has iframe inclusion off) it reveals a button that opens the app\n * in a new top-level tab. Carries the same state cookie the plain redirect\n * would set — used by the top-level path; harmless in the frame, where the\n * new tab writes its own first-party state cookie.\n */\n function newTabSignInPage(\n authorizeUrl: string,\n appRoot: string,\n cookies: string[],\n ): Response {\n const body = `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Sign in with Incorta</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.2rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n a.btn { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; text-decoration: none;\n font-weight: 600; }\n </style>\n </head>\n <body>\n <main id=\"frame\" hidden>\n <h1>Sign in with Incorta</h1>\n <p>\n This app signs you in with your Incorta account. The embedded preview\n can’t complete sign-in here — continue in a new tab.\n </p>\n <a class=\"btn\" href=\"${appRoot}\" target=\"_blank\" rel=\"noopener noreferrer\">Sign in with Incorta ↗</a>\n </main>\n <script>\n (function () {\n var authorize = ${JSON.stringify(authorizeUrl)};\n if (window.top === window.self) {\n window.location.replace(authorize);\n } else {\n document.getElementById(\"frame\").hidden = false;\n }\n })();\n </script>\n <noscript><a href=\"${appRoot}\">Continue to sign in</a></noscript>\n </body>\n</html>`;\n const headers = new Headers({\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(body, { status: 200, headers });\n }\n\n /**\n * `appAccess: \"catalog\"` — the user is signed in but the catalog says no:\n * an honest 403 with who they are and a way out (the wrong-account case is\n * the most common real cause). Non-HTML requests get the JSON twin.\n */\n function accessDeniedResponse(request: Request, session: AuthSession): Response {\n if (!wantsHtml(request)) {\n return json(403, { error: \"app_access_denied\" });\n }\n const identity = escapeHtml(session.user.name?.trim() || session.user.sub);\n const detail =\n session.user.name?.trim() && session.user.name.trim() !== session.user.sub\n ? `${identity} (${escapeHtml(session.user.sub)})`\n : identity;\n const switchHref = `${config.basePath}/logout`;\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>No access</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }\n strong { color: #18181b; font-weight: 600; }\n a { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;\n background: #18181b; color: #fafafa; font-size: .9rem;\n text-decoration: none; }\n </style>\n </head>\n <body>\n <main>\n <h1>You don’t have access to this application</h1>\n <p>\n Ask the app’s owner to share it with you in Incorta.\n You are signed in as <strong>${detail}</strong>.\n </p>\n <a href=\"${switchHref}\">Sign in with a different account</a>\n </main>\n </body>\n</html>`,\n {\n status: 403,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n },\n );\n }\n\n /**\n * The catalog could not answer (Incorta unreachable/failing) — deny, but\n * make clear this is an outage, not a sharing decision. Distinct from the\n * 403 page so users never conclude they were unshared. Auto-retries: the\n * verdict cache holds no failures, so the next load re-asks.\n */\n function accessUnavailableResponse(request: Request): Response {\n if (!wantsHtml(request)) {\n return json(503, { error: \"app_access_unavailable\" });\n }\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta http-equiv=\"refresh\" content=\"15\" />\n <title>Temporarily unavailable</title>\n <style>\n body { margin: 0; display: grid; place-items: center; min-height: 100vh;\n background: #fafafa; color: #18181b;\n font-family: ui-sans-serif, system-ui, sans-serif; }\n main { max-width: 26rem; padding: 2rem; text-align: center; }\n h1 { font-size: 1.25rem; margin: 0 0 .5rem; }\n p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0; }\n </style>\n </head>\n <body>\n <main>\n <h1>This app is temporarily unavailable</h1>\n <p>\n Your access could not be verified because Incorta did not respond.\n This page retries automatically — you can also refresh in a\n moment.\n </p>\n </main>\n </body>\n</html>`,\n {\n status: 503,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n \"retry-after\": \"30\",\n },\n },\n );\n }\n\n async function handler(request: Request): Promise<Response> {\n const url = new URL(request.url);\n if (url.pathname !== config.basePath && !url.pathname.startsWith(`${config.basePath}/`)) {\n return json(404, { error: \"not_found\" });\n }\n const route = url.pathname.slice(config.basePath.length) || \"/\";\n const method = request.method.toUpperCase();\n\n try {\n if (route === \"/login\" && method === \"GET\") return await login(request, url);\n if (route === \"/callback\" && method === \"GET\") return await callback(request, url);\n if (route === \"/session\" && method === \"GET\") return await sessionRoute(request);\n if (route === \"/token\" && method === \"GET\") return await tokenRoute(request);\n if (route === \"/refresh\" && method === \"GET\") return await refreshRoute(request, url);\n if (route === \"/logout\" && (method === \"POST\" || method === \"GET\")) {\n return await logout(request, url);\n }\n if (route === \"/signed-out\" && method === \"GET\") return signedOutPage();\n return json(404, { error: \"not_found\" });\n } catch (error) {\n const code = error instanceof IncortaAuthError ? error.code : \"internal_error\";\n const message =\n error instanceof Error ? error.message : \"Unexpected incorta-auth handler error\";\n console.error(`[incorta-auth] ${route} failed:`, message);\n return json(500, { error: code });\n }\n }\n\n async function getSession(request: Request): Promise<AuthSession | null> {\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unsealed = await unsealSession(config, cookies[config.cookieName]);\n if (!unsealed) return null;\n const { session, expiresAt } = unsealed;\n if (session.accessTokenExpiresAt <= Date.now() && !session.refreshToken) return null;\n return {\n user: session.user,\n accessToken: session.accessToken,\n accessTokenExpiresAt: session.accessTokenExpiresAt,\n expiresAt,\n };\n }\n\n /** The catalog verdict for this session's user, via the 30s per-user cache. */\n function getAppAccess(session: AuthSession): Promise<AppAccess> {\n return appAccessChecker.check(session.accessToken, session.user.sub);\n }\n\n /**\n * The whole-app gate, enforcing `config.appAccess` in front of every\n * request (see `IncortaAuthConfig.appAccess`). Auth routes are always\n * served — login stays reachable even for public apps. `null` means\n * \"continue to the app\"; in `\"public\"` mode that is every non-auth route,\n * signed in or not.\n */\n async function gate(request: Request): Promise<Response | null> {\n const url = new URL(request.url);\n if (url.pathname === config.basePath || url.pathname.startsWith(`${config.basePath}/`)) {\n return handler(request);\n }\n if (config.appAccess === \"public\") return null;\n\n const session = await getSession(request);\n if (!session) {\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`,\n );\n }\n return json(401, { error: \"unauthenticated\" });\n }\n if (config.appAccess !== \"catalog\") return null;\n\n // The check replays the session's access token to Incorta — an already\n // expired token would 401 there, so refresh it first via the browser\n // round trip (which is what persists the rotated cookie).\n if (session.accessTokenExpiresAt <= Date.now()) {\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/refresh?redirect_to=${encodeURIComponent(requestPath(url))}`,\n );\n }\n return json(401, { error: \"token_expired\" });\n }\n\n try {\n const verdict = await getAppAccess(session);\n if (verdict.allowed) return null;\n return accessDeniedResponse(request, session);\n } catch (error) {\n if (error instanceof AppAccessTokenRejectedError) {\n // Incorta no longer honors this session's token — sign out and let\n // the login flow mint a fresh one (SSO makes this a silent bounce).\n const secure = isSecureRequest(config, request);\n const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));\n if (wantsHtml(request)) {\n return redirect(\n `${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`,\n [clearSession],\n );\n }\n return json(401, { error: \"unauthenticated\" }, [clearSession]);\n }\n if (!(error instanceof AppAccessUnavailableError)) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(\"[incorta-auth] app access check failed unexpectedly:\", message);\n }\n // Verdicts unavailable — NEVER fail open.\n return accessUnavailableResponse(request);\n }\n }\n\n return { handler, getSession, gate, getAppAccess };\n}\n","export interface CookieOptions {\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: \"lax\" | \"strict\" | \"none\";\n /** CHIPS `Partitioned` attribute — per-top-site cookie jar for cross-site\n * iframe embedding. Browsers require Secure alongside it. */\n partitioned?: boolean;\n path?: string;\n /** Seconds. 0 deletes the cookie. */\n maxAge?: number;\n}\n\nexport function parseCookies(header: string | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const name = part.slice(0, eq).trim();\n if (!name) continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"')) value = value.slice(1, -1);\n try {\n out[name] = decodeURIComponent(value);\n } catch {\n out[name] = value;\n }\n }\n return out;\n}\n\nexport function serializeCookie(\n name: string,\n value: string,\n options: CookieOptions = {},\n): string {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n parts.push(`Path=${options.path ?? \"/\"}`);\n if (options.maxAge !== undefined) {\n parts.push(`Max-Age=${Math.floor(options.maxAge)}`);\n // Expires for compatibility with clients that ignore Max-Age.\n parts.push(`Expires=${new Date(Date.now() + options.maxAge * 1000).toUTCString()}`);\n }\n if (options.httpOnly ?? true) parts.push(\"HttpOnly\");\n if (options.secure) parts.push(\"Secure\");\n const sameSite = options.sameSite ?? \"lax\";\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n // Deleting a partitioned cookie needs the attribute too — callers pass the\n // same options for set and delete, so this covers both.\n if (options.partitioned) parts.push(\"Partitioned\");\n return parts.join(\"; \");\n}\n\nexport function deleteCookie(name: string, options: CookieOptions = {}): string {\n return serializeCookie(name, \"\", { ...options, maxAge: 0 });\n}\n","import { createRemoteJWKSet } from \"jose\";\nimport type { JWTVerifyGetKey } from \"jose\";\nimport type { ResolvedConfig } from \"./config.js\";\n\ninterface DiscoveryDocument {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n jwks_uri: string;\n}\n\nexport interface Endpoints {\n issuer: string;\n authorizationEndpoint: string;\n tokenEndpoint: string;\n jwks: JWTVerifyGetKey;\n}\n\nconst endpointsCache = new Map<string, Promise<Endpoints>>();\n\n/**\n * Split horizon: URLs the SERVER fetches (discovery, token endpoint, JWKS)\n * are moved onto `internalIncortaUrl` when set. Values the BROWSER navigates\n * to (authorization endpoint) and values used for VALIDATION (issuer — must\n * equal the token's `iss` claim) are never rewritten.\n *\n * The match is by PATH under the Incorta context path (`/incorta` of\n * `incortaUrl`), not by a literal prefix: the discovery document is fetched\n * from the internal Incorta itself, and Incorta spells its own URLs with the\n * explicit port (`https://host:443/...`), as `http://` when the request\n * reached it unproxied, or with an in-cluster host name. A prefix match\n * misses all three and sends the call to the public host — which, where the\n * public host allow-lists callers, fails the token exchange and loops the\n * sign-in. URLs outside the context path pass through untouched.\n */\nfunction toInternal(config: ResolvedConfig, url: string): string {\n const internal = config.internalIncortaUrl?.replace(/\\/+$/, \"\");\n if (!internal) return url;\n let target: URL;\n let contextPath: string;\n try {\n target = new URL(url);\n contextPath = new URL(config.incortaUrl).pathname.replace(/\\/+$/, \"\");\n } catch {\n return url;\n }\n const path = target.pathname;\n if (path !== contextPath && !path.startsWith(`${contextPath}/`)) return url;\n return `${internal}${path.slice(contextPath.length)}${target.search}`;\n}\n\n/**\n * Resolves the tenant's OAuth endpoints, preferring the OIDC discovery document\n * and falling back to Incorta's fixed `/oauth/{tenant}/...` layout when the\n * document is unreachable. Cached per issuer (and internal-URL horizon) for\n * the process lifetime.\n */\nexport function getEndpoints(config: ResolvedConfig): Promise<Endpoints> {\n const cacheKey = `${config.issuer}|${config.internalIncortaUrl ?? \"\"}`;\n const cached = endpointsCache.get(cacheKey);\n if (cached) return cached;\n\n const promise = resolve(config).catch((error) => {\n // Do not cache failures — a temporarily unreachable Incorta should not\n // poison the process.\n endpointsCache.delete(cacheKey);\n throw error;\n });\n endpointsCache.set(cacheKey, promise);\n return promise;\n}\n\nasync function resolve(config: ResolvedConfig): Promise<Endpoints> {\n const fallback: DiscoveryDocument = {\n issuer: config.issuer,\n authorization_endpoint: `${config.issuer}/authorize`,\n token_endpoint: `${config.issuer}/token`,\n jwks_uri: `${config.issuer}/.well-known/jwks.json`,\n };\n\n let doc = fallback;\n try {\n const response = await config.fetch(toInternal(config, config.discoveryUrl), {\n headers: { accept: \"application/json\" },\n });\n if (response.ok) {\n const body = (await response.json()) as Partial<DiscoveryDocument>;\n doc = {\n issuer: body.issuer ?? fallback.issuer,\n authorization_endpoint: body.authorization_endpoint ?? fallback.authorization_endpoint,\n token_endpoint: body.token_endpoint ?? fallback.token_endpoint,\n jwks_uri: body.jwks_uri ?? fallback.jwks_uri,\n };\n }\n } catch {\n // Fall through to the fixed layout.\n }\n\n const jwks =\n config.jwksOverride ??\n createRemoteJWKSet(new URL(toInternal(config, doc.jwks_uri)), {\n cacheMaxAge: 10 * 60 * 1000,\n cooldownDuration: 30 * 1000,\n });\n\n return {\n issuer: doc.issuer,\n authorizationEndpoint: doc.authorization_endpoint,\n tokenEndpoint: toInternal(config, doc.token_endpoint),\n jwks,\n };\n}\n\n/** Test hook: clears the per-process endpoint cache. */\nexport function clearEndpointsCache(): void {\n endpointsCache.clear();\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\n/**\n * PKCE (RFC 7636) for the authorization-code flow. Incorta registers app\n * clients as PUBLIC clients (no client secret — see `registerClient`), so\n * the code verifier is what binds the authorization code to this server:\n * an intercepted code is worthless without it. The verifier lives in the\n * sealed login-state cookie between `/login` and `/callback`; only its\n * S256 challenge travels to Incorta.\n */\nexport function generateCodeVerifier(): string {\n // 32 random bytes → 43 base64url chars, inside RFC 7636's 43–128 range.\n return randomBytes(32).toString(\"base64url\");\n}\n\nexport function codeChallengeFor(codeVerifier: string): string {\n return createHash(\"sha256\").update(codeVerifier, \"ascii\").digest(\"base64url\");\n}\n\nexport const CODE_CHALLENGE_METHOD = \"S256\";\n","import { createHash } from \"node:crypto\";\nimport { EncryptJWT, jwtDecrypt } from \"jose\";\nimport type { ResolvedConfig, SessionData } from \"./config.js\";\n\n/**\n * Session and state cookies are sealed as encrypted JWTs (JWE, dir + A256GCM):\n * tamper-proof AND opaque, since the session carries the Incorta access/refresh\n * tokens. The 256-bit key is derived from the configured secret.\n */\nfunction sealKey(config: ResolvedConfig): Uint8Array {\n return new Uint8Array(\n createHash(\"sha256\").update(`incorta-auth:${config.secret}`).digest(),\n );\n}\n\ninterface SealedPayload {\n /** Guards against replaying one cookie kind as the other. */\n purpose: \"session\" | \"state\";\n data: unknown;\n}\n\nasync function seal(\n config: ResolvedConfig,\n purpose: SealedPayload[\"purpose\"],\n data: unknown,\n maxAgeSeconds: number,\n): Promise<string> {\n return new EncryptJWT({ purpose, data })\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(`${maxAgeSeconds}s`)\n .encrypt(sealKey(config));\n}\n\nasync function unseal<T>(\n config: ResolvedConfig,\n purpose: SealedPayload[\"purpose\"],\n cookieValue: string | undefined,\n): Promise<{ data: T; issuedAt: number; expiresAt: number } | null> {\n if (!cookieValue) return null;\n try {\n const { payload } = await jwtDecrypt(cookieValue, sealKey(config));\n if (payload.purpose !== purpose) return null;\n return {\n data: payload.data as T,\n issuedAt: (payload.iat ?? 0) * 1000,\n expiresAt: (payload.exp ?? 0) * 1000,\n };\n } catch {\n // Expired, tampered, or sealed with a different secret — treat as absent.\n return null;\n }\n}\n\nexport function sealSession(config: ResolvedConfig, session: SessionData): Promise<string> {\n return seal(config, \"session\", session, config.sessionMaxAge);\n}\n\nexport async function unsealSession(\n config: ResolvedConfig,\n cookieValue: string | undefined,\n): Promise<{ session: SessionData; issuedAt: number; expiresAt: number } | null> {\n const result = await unseal<SessionData>(config, \"session\", cookieValue);\n if (!result) return null;\n const { data, issuedAt, expiresAt } = result;\n if (!data || typeof data !== \"object\" || !data.user || !data.accessToken) return null;\n return { session: data, issuedAt, expiresAt };\n}\n\n/** Login round-trip state (CSRF protection + post-login destination). */\nexport interface LoginState {\n state: string;\n nonce: string;\n returnTo: string;\n /**\n * PKCE code verifier for this login attempt (absent only in a state cookie\n * sealed by a pre-PKCE build — the callback then exchanges without one).\n */\n codeVerifier?: string;\n}\n\nconst STATE_MAX_AGE_SECONDS = 10 * 60;\n\nexport function sealLoginState(config: ResolvedConfig, state: LoginState): Promise<string> {\n return seal(config, \"state\", state, STATE_MAX_AGE_SECONDS);\n}\n\nexport async function unsealLoginState(\n config: ResolvedConfig,\n cookieValue: string | undefined,\n): Promise<LoginState | null> {\n const result = await unseal<LoginState>(config, \"state\", cookieValue);\n if (!result) return null;\n const { data } = result;\n if (!data || typeof data.state !== \"string\" || typeof data.nonce !== \"string\") return null;\n if (data.codeVerifier !== undefined && typeof data.codeVerifier !== \"string\") return null;\n return data;\n}\n","import { decodeJwt, jwtVerify } from \"jose\";\nimport type { JWTPayload } from \"jose\";\nimport type { IncortaUser, ResolvedConfig } from \"./config.js\";\nimport { getEndpoints } from \"./discovery.js\";\n\nexport interface TokenSet {\n accessToken: string;\n /** Epoch millis derived from the access token's `exp` claim. */\n accessTokenExpiresAt: number;\n idToken?: string;\n refreshToken?: string;\n}\n\ninterface TokenEndpointResponse {\n access_token?: string;\n id_token?: string;\n refresh_token?: string;\n error?: string;\n error_description?: string;\n}\n\nexport class IncortaAuthError extends Error {\n constructor(\n readonly code: string,\n message: string,\n ) {\n super(`[incorta-auth] ${message}`);\n this.name = \"IncortaAuthError\";\n }\n}\n\nfunction basicAuth(clientId: string, clientSecret: string): string {\n const raw = `${clientId}:${clientSecret}`;\n return `Basic ${Buffer.from(raw, \"utf8\").toString(\"base64\")}`;\n}\n\n/**\n * How the token request identifies the client (RFC 6749 §2.3 / §4.1.3): a\n * confidential client authenticates with `client_secret_basic`; a public\n * client — what Incorta registers apps as — has no secret and sends\n * `client_id` in the body instead.\n */\nfunction clientAuthentication(\n config: ResolvedConfig,\n params: Record<string, string>,\n): { headers: Record<string, string>; params: Record<string, string> } {\n if (config.clientSecret) {\n return { headers: { authorization: basicAuth(config.clientId, config.clientSecret) }, params };\n }\n return { headers: {}, params: { client_id: config.clientId, ...params } };\n}\n\nasync function callTokenEndpoint(\n config: ResolvedConfig,\n params: Record<string, string>,\n): Promise<TokenSet> {\n const { tokenEndpoint } = await getEndpoints(config);\n const auth = clientAuthentication(config, params);\n\n let response: Response;\n try {\n response = await config.fetch(tokenEndpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n accept: \"application/json\",\n ...auth.headers,\n },\n body: new URLSearchParams(auth.params).toString(),\n });\n } catch (error) {\n throw new IncortaAuthError(\n \"incorta_unreachable\",\n `Could not reach the Incorta token endpoint (${tokenEndpoint}): ${(error as Error).message}`,\n );\n }\n\n let body: TokenEndpointResponse;\n try {\n body = (await response.json()) as TokenEndpointResponse;\n } catch {\n throw new IncortaAuthError(\n \"invalid_token_response\",\n `Token endpoint returned a non-JSON response (HTTP ${response.status}).`,\n );\n }\n\n if (!response.ok || !body.access_token) {\n throw new IncortaAuthError(\n body.error ?? \"token_exchange_failed\",\n body.error_description ?? `Token exchange failed (HTTP ${response.status}).`,\n );\n }\n\n const { exp } = decodeJwt(body.access_token);\n return {\n accessToken: body.access_token,\n accessTokenExpiresAt: exp ? exp * 1000 : Date.now() + 60 * 60 * 1000,\n idToken: body.id_token,\n refreshToken: body.refresh_token,\n };\n}\n\nexport function exchangeCode(\n config: ResolvedConfig,\n code: string,\n redirectUri: string,\n /** The PKCE verifier sealed at `/login`; Incorta checks it against the challenge the authorize request carried. */\n codeVerifier?: string,\n): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n ...(codeVerifier ? { code_verifier: codeVerifier } : {}),\n });\n}\n\nexport function refreshTokens(config: ResolvedConfig, refreshToken: string): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n });\n}\n\nasync function verifyJwt(config: ResolvedConfig, token: string): Promise<JWTPayload> {\n const { jwks, issuer } = await getEndpoints(config);\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer,\n audience: config.clientId,\n algorithms: [\"RS256\"],\n });\n return payload;\n } catch (error) {\n throw new IncortaAuthError(\n \"invalid_token\",\n `Token verification failed: ${(error as Error).message}`,\n );\n }\n}\n\nexport async function verifyAccessToken(\n config: ResolvedConfig,\n accessToken: string,\n): Promise<JWTPayload> {\n return verifyJwt(config, accessToken);\n}\n\n/**\n * Verifies the ID token (signature, issuer, audience, nonce) and maps its\n * claims to the SDK's user shape.\n */\nexport async function verifyIdToken(\n config: ResolvedConfig,\n idToken: string,\n expectedNonce: string,\n): Promise<IncortaUser> {\n const payload = await verifyJwt(config, idToken);\n\n if (payload.nonce !== expectedNonce) {\n throw new IncortaAuthError(\"nonce_mismatch\", \"ID token nonce does not match the login request.\");\n }\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new IncortaAuthError(\"invalid_token\", \"ID token is missing the sub claim.\");\n }\n\n const roles = Array.isArray(payload.roles)\n ? payload.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n\n return {\n sub: payload.sub,\n name: typeof payload.name === \"string\" ? payload.name : undefined,\n email: typeof payload.email === \"string\" ? payload.email : undefined,\n roles,\n tenant: typeof payload.tenant === \"string\" ? payload.tenant : config.tenant,\n };\n}\n\n/**\n * Fallback identity when the client was registered without the `openid` scope\n * (no ID token in the response): identify the user from the verified access\n * token instead. Name/email/roles are unavailable in that case.\n */\nexport async function userFromAccessToken(\n config: ResolvedConfig,\n accessToken: string,\n): Promise<IncortaUser> {\n const payload = await verifyAccessToken(config, accessToken);\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new IncortaAuthError(\"invalid_token\", \"Access token is missing the sub claim.\");\n }\n return {\n sub: payload.sub,\n roles: [],\n tenant: typeof payload.tenant === \"string\" ? payload.tenant : config.tenant,\n };\n}\n","interface ManagementErrorBody {\n error?: string;\n error_description?: string;\n}\n\nfunction registrationEndpoint(incortaUrl: string, tenant: string, clientId?: string): string {\n const base = `${incortaUrl.replace(/\\/+$/, \"\")}/oauth/${tenant}/register`;\n return clientId ? `${base}/${clientId}` : base;\n}\n\nasync function readJson<T extends ManagementErrorBody>(\n response: Response,\n what: string,\n): Promise<T> {\n let body: T;\n try {\n body = (await response.json()) as T;\n } catch {\n throw new Error(\n `[incorta-auth] ${what} returned a non-JSON response (HTTP ${response.status}). ` +\n \"Is the OAuth authorization server enabled on this cluster?\",\n );\n }\n if (!response.ok) {\n throw new Error(\n `[incorta-auth] ${what} failed (HTTP ${response.status}): ` +\n `${body.error ?? \"unknown_error\"} — ${body.error_description ?? \"no description\"}`,\n );\n }\n return body;\n}\n\nexport interface RegisterClientOptions {\n /** Incorta base URL, e.g. `http://localhost:8080/incorta`. */\n incortaUrl: string;\n /** Tenant to register the client in. */\n tenant: string;\n /** Display name of the client. */\n name: string;\n /** Allowed OAuth callback URLs, e.g. `http://localhost:4000/auth/callback`. */\n redirectUris: string[];\n /** @default \"openid profile email\" */\n scope?: string;\n /** @default [\"authorization_code\", \"refresh_token\"] */\n grantTypes?: string[];\n fetchImpl?: typeof fetch;\n}\n\nexport interface RegisteredClient {\n clientId: string;\n /**\n * Only for a CONFIDENTIAL client. Incorta issues one solely to clients\n * holding the `client_credentials` grant (PAT-authenticated registration);\n * an app registered here is a PUBLIC client and gets none — token requests\n * then carry `client_id` alone, and PKCE protects the code flow.\n */\n clientSecret?: string;\n /** What Incorta decided: `none` for a public client, `client_secret_basic` for a confidential one. */\n tokenEndpointAuthMethod?: string;\n /** Bearer token for managing this registration (GET/PUT/DELETE it later). */\n registrationAccessToken?: string;\n /** URL to manage this registration. */\n registrationClientUri?: string;\n}\n\n/**\n * Registers an OAuth client in Incorta via RFC 7591 dynamic client\n * registration (`POST {incortaUrl}/oauth/{tenant}/register`). Incorta allows\n * unauthenticated registration for authorization_code clients and registers\n * them as PUBLIC clients: the response carries the generated client_id (and a\n * client_secret only on servers that still issue one) — store it once, e.g.\n * in the app's `.env`.\n */\nexport async function registerClient(\n options: RegisterClientOptions,\n): Promise<RegisteredClient> {\n const {\n incortaUrl,\n tenant,\n name,\n redirectUris,\n scope = \"openid profile email\",\n grantTypes = [\"authorization_code\", \"refresh_token\"],\n fetchImpl = fetch,\n } = options;\n\n if (!redirectUris.length) {\n throw new Error(\"[incorta-auth] registerClient requires at least one redirect URI.\");\n }\n\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", accept: \"application/json\" },\n body: JSON.stringify({\n client_name: name,\n redirect_uris: redirectUris,\n grant_types: grantTypes,\n scope,\n token_endpoint_auth_method: \"client_secret_basic\",\n }),\n });\n\n interface RegistrationResponse extends ManagementErrorBody {\n client_id?: string;\n client_secret?: string;\n token_endpoint_auth_method?: string;\n registration_access_token?: string;\n registration_client_uri?: string;\n }\n\n const body = await readJson<RegistrationResponse>(response, \"Client registration\");\n if (!body.client_id) {\n throw new Error(`[incorta-auth] Client registration failed: response is missing client_id.`);\n }\n\n return {\n clientId: body.client_id,\n clientSecret: body.client_secret || undefined,\n tokenEndpointAuthMethod: body.token_endpoint_auth_method,\n registrationAccessToken: body.registration_access_token,\n registrationClientUri: body.registration_client_uri,\n };\n}\n\nexport interface ManageClientOptions {\n /** Incorta base URL, e.g. `http://localhost:8080/incorta`. */\n incortaUrl: string;\n tenant: string;\n clientId: string;\n /** The registration access token returned when the client was registered. */\n registrationAccessToken: string;\n fetchImpl?: typeof fetch;\n}\n\n/** Current metadata of a registered client. */\nexport interface ClientInfo {\n clientId: string;\n name?: string;\n redirectUris: string[];\n grantTypes: string[];\n scope?: string;\n}\n\ninterface ClientMetadataResponse extends ManagementErrorBody {\n client_id?: string;\n client_name?: string;\n redirect_uris?: string[];\n grant_types?: string[];\n scope?: string;\n}\n\nfunction toClientInfo(body: ClientMetadataResponse, fallbackClientId: string): ClientInfo {\n return {\n clientId: body.client_id ?? fallbackClientId,\n name: body.client_name,\n redirectUris: body.redirect_uris ?? [],\n grantTypes: body.grant_types ?? [],\n scope: body.scope,\n };\n}\n\n/**\n * Reads a registered client's current metadata (RFC 7592-style management,\n * `GET {incortaUrl}/oauth/{tenant}/register/{clientId}` with the registration\n * access token).\n */\nexport async function getClient(options: ManageClientOptions): Promise<ClientInfo> {\n const { incortaUrl, tenant, clientId, registrationAccessToken, fetchImpl = fetch } = options;\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n });\n const body = await readJson<ClientMetadataResponse>(response, \"Reading the client\");\n return toClientInfo(body, clientId);\n}\n\nexport interface UpdateRedirectUrisOptions extends ManageClientOptions {\n redirectUris: string[];\n /**\n * `\"add\"` (default) merges the given URIs into the client's current set;\n * `\"replace\"` makes the given list the exact new set (also how URIs are\n * removed).\n */\n mode?: \"add\" | \"replace\";\n}\n\n/**\n * Updates the allowed OAuth callback URLs of a registered client. Incorta's\n * management endpoint applies `redirect_uris` from the PUT body (other\n * metadata is unchanged server-side).\n */\nexport async function updateClientRedirectUris(\n options: UpdateRedirectUrisOptions,\n): Promise<ClientInfo> {\n const {\n incortaUrl,\n tenant,\n clientId,\n registrationAccessToken,\n redirectUris,\n mode = \"add\",\n fetchImpl = fetch,\n } = options;\n\n if (!redirectUris.length) {\n // An empty replace would leave the client unable to complete the\n // authorization-code flow at all — delete the registration instead.\n throw new Error(\"[incorta-auth] updateClientRedirectUris requires at least one redirect URI.\");\n }\n\n const current = await getClient(options);\n const nextUris =\n mode === \"add\" ? [...new Set([...current.redirectUris, ...redirectUris])] : redirectUris;\n\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n method: \"PUT\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n // RFC 7592 update: client_id + full metadata. Incorta applies\n // redirect_uris only, but nimbus-side parsing wants the full shape.\n body: JSON.stringify({\n client_id: clientId,\n redirect_uris: nextUris,\n grant_types: current.grantTypes,\n scope: current.scope,\n client_name: current.name,\n }),\n });\n\n if (!response.ok) {\n await readJson<ManagementErrorBody>(response, \"Updating the client\"); // throws with details\n }\n // The PUT response body is empty on success — re-read for the fresh state.\n return getClient(options);\n}\n\n/**\n * Deletes a registered client (RFC 7592-style management,\n * `DELETE {incortaUrl}/oauth/{tenant}/register/{clientId}` with the\n * registration access token). Existing sessions in apps using the client keep\n * working until they expire; new logins and token refreshes stop immediately.\n */\nexport async function deleteClient(options: ManageClientOptions): Promise<void> {\n const { incortaUrl, tenant, clientId, registrationAccessToken, fetchImpl = fetch } = options;\n const response = await fetchImpl(registrationEndpoint(incortaUrl, tenant, clientId), {\n method: \"DELETE\",\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${registrationAccessToken}`,\n },\n });\n if (!response.ok) {\n await readJson<ManagementErrorBody>(response, \"Deleting the client\"); // throws with details\n }\n}\n","import type { AppAccess } from \"./core/app-access.js\";\nimport { resolveConfig } from \"./core/config.js\";\nimport type { IncortaAuthConfig, ResolvedConfig } from \"./core/config.js\";\nimport { createHandler } from \"./core/handler.js\";\nimport type { AuthGate, AuthHandler, AuthSession } from \"./core/handler.js\";\n\nexport {\n AppAccessTokenRejectedError,\n AppAccessUnavailableError,\n EDIT_BIT,\n SHARE_BIT,\n VIEW_BIT,\n} from \"./core/app-access.js\";\nexport type { AppAccess, AppAccessPolicy } from \"./core/app-access.js\";\nexport type {\n IncortaAuthConfig,\n IncortaUser,\n SessionData,\n SessionInfo,\n} from \"./core/config.js\";\nexport type { AuthGate, AuthHandler, AuthSession } from \"./core/handler.js\";\nexport { IncortaAuthError } from \"./core/tokens.js\";\nexport {\n deleteClient,\n getClient,\n registerClient,\n updateClientRedirectUris,\n} from \"./core/register.js\";\nexport type {\n ClientInfo,\n ManageClientOptions,\n RegisterClientOptions,\n RegisteredClient,\n UpdateRedirectUrisOptions,\n} from \"./core/register.js\";\n\nexport interface IncortaAuth {\n /**\n * Web-standard request handler serving the auth routes under\n * `config.basePath` (default `/auth`): `/login`, `/callback`, `/session`,\n * `/logout` (+ `/token` when `exposeAccessToken` is on).\n *\n * Wire it to anything that speaks `Request`/`Response` — a TanStack Start\n * server route, Bun.serve, or via `@incorta/auth/node` / `@incorta/auth/express`\n * for Node servers.\n */\n handler: AuthHandler;\n /**\n * Reads the session from a web-standard `Request` (server-side gating, API\n * routes, server functions). Returns null when the user is not signed in.\n */\n getSession: (request: Request) => Promise<AuthSession | null>;\n /**\n * The whole-app gate: serve the returned `Response` when non-null (auth\n * routes, login redirects, access-denied/unavailable pages), continue to\n * your app on `null`. Enforces `config.appAccess` — `\"public\"` lets every\n * request through, `\"authenticated\"` requires a session, `\"catalog\"`\n * additionally requires at least VIEW access to this app in the Incorta\n * catalog (fails CLOSED with a 503 page when Incorta cannot answer).\n */\n gate: AuthGate;\n /**\n * The Incorta catalog's verdict about `session`'s user and THIS app —\n * registration, permission bits, and the gate's `allowed` outcome. Served\n * from a 30s per-user cache (the same one `gate` uses in `\"catalog\"`\n * mode). Throws `AppAccessUnavailableError` when Incorta cannot answer and\n * `AppAccessTokenRejectedError` when it rejects the session token. Useful\n * for permission-aware UX (e.g. hiding editor-only affordances) in any\n * `appAccess` mode.\n */\n getAppAccess: (session: AuthSession) => Promise<AppAccess>;\n /** The resolved configuration (secrets included — do not log). */\n config: ResolvedConfig;\n}\n\n/**\n * Creates an IncortaAuth instance: OAuth 2.0 authorization-code flow against\n * Incorta's built-in authorization server. Unauthenticated users are redirected\n * to the Incorta login page and returned to your app once signed in.\n */\nexport function createIncortaAuth(config: IncortaAuthConfig = {}): IncortaAuth {\n const resolved = resolveConfig(config);\n const { handler, getSession, gate, getAppAccess } = createHandler(resolved);\n return { handler, getSession, gate, getAppAccess, config: resolved };\n}\n"],"mappings":"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","names":["randomBytes","createHash","randomBytes","code"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@incorta/auth",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
4
4
|
"description": "Authenticate Incorta users in any Node.js app — OAuth 2.0/OIDC against Incorta's built-in authorization server, with a React provider and Express/TanStack Start integrations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|