@incorta/auth 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,6 +48,15 @@ The cookie is an encrypted JWT (JWE `dir`/`A256GCM`, key derived from
48
48
  claims (`sub`, `name`, `email`, `roles`, `tenant`) + the Incorta access/refresh
49
49
  tokens. Nothing is stored server-side.
50
50
 
51
+ **Embedded (cross-site iframe) mode**: when the app runs inside an iframe on a
52
+ DIFFERENT site, `Lax` cookies are never sent — the login flow loops forever.
53
+ Set `cookies: { sameSite: "none", partitioned: true }` (or env
54
+ `INCORTA_COOKIE_SAMESITE=none` + `INCORTA_COOKIE_PARTITIONED=1`): the auth
55
+ cookies become `SameSite=None; Secure; Partitioned` (CHIPS), which modern
56
+ Chrome allows in third-party iframes with a per-embedder cookie jar. `none`
57
+ forces `Secure` (accepted on trustworthy plain-http origins like
58
+ `*.localhost`); top-level usage keeps working with the same setting.
59
+
51
60
  ## CLI
52
61
 
53
62
  ```bash
@@ -85,6 +85,25 @@ interface IncortaAuthConfig {
85
85
  * @default "auto"
86
86
  */
87
87
  secure?: boolean | "auto";
88
+ /**
89
+ * `SameSite` for the session/state cookies. The `"lax"` default is right
90
+ * for top-level apps. Set `"none"` when the app runs inside a CROSS-SITE
91
+ * iframe (e.g. an embedded preview) — Lax cookies are never sent there,
92
+ * which loops the login flow forever. `"none"` forces the `Secure`
93
+ * attribute (browsers require the pair; Chrome still accepts it on
94
+ * trustworthy plain-http origins like `*.localhost`). Defaults to
95
+ * `INCORTA_COOKIE_SAMESITE`.
96
+ * @default "lax"
97
+ */
98
+ sameSite?: "lax" | "none";
99
+ /**
100
+ * CHIPS: adds the `Partitioned` attribute, giving each embedding
101
+ * top-level site its own cookie jar — what modern Chrome requires for
102
+ * cookies in third-party iframes. Use together with `sameSite: "none"`.
103
+ * Defaults to `INCORTA_COOKIE_PARTITIONED` (`1`/`true`).
104
+ * @default false
105
+ */
106
+ partitioned?: boolean;
88
107
  };
89
108
  /**
90
109
  * Expose `GET {basePath}/token` returning the raw Incorta access token to the
@@ -117,6 +136,8 @@ interface ResolvedConfig {
117
136
  stateCookieName: string;
118
137
  sessionMaxAge: number;
119
138
  secureCookies: boolean | "auto";
139
+ cookieSameSite: "lax" | "none";
140
+ cookiePartitioned: boolean;
120
141
  exposeAccessToken: boolean;
121
142
  /** `{incortaUrl}/oauth/{tenant}` — the OIDC issuer for this tenant. */
122
143
  issuer: string;
@@ -85,6 +85,25 @@ interface IncortaAuthConfig {
85
85
  * @default "auto"
86
86
  */
87
87
  secure?: boolean | "auto";
88
+ /**
89
+ * `SameSite` for the session/state cookies. The `"lax"` default is right
90
+ * for top-level apps. Set `"none"` when the app runs inside a CROSS-SITE
91
+ * iframe (e.g. an embedded preview) — Lax cookies are never sent there,
92
+ * which loops the login flow forever. `"none"` forces the `Secure`
93
+ * attribute (browsers require the pair; Chrome still accepts it on
94
+ * trustworthy plain-http origins like `*.localhost`). Defaults to
95
+ * `INCORTA_COOKIE_SAMESITE`.
96
+ * @default "lax"
97
+ */
98
+ sameSite?: "lax" | "none";
99
+ /**
100
+ * CHIPS: adds the `Partitioned` attribute, giving each embedding
101
+ * top-level site its own cookie jar — what modern Chrome requires for
102
+ * cookies in third-party iframes. Use together with `sameSite: "none"`.
103
+ * Defaults to `INCORTA_COOKIE_PARTITIONED` (`1`/`true`).
104
+ * @default false
105
+ */
106
+ partitioned?: boolean;
88
107
  };
89
108
  /**
90
109
  * Expose `GET {basePath}/token` returning the raw Incorta access token to the
@@ -117,6 +136,8 @@ interface ResolvedConfig {
117
136
  stateCookieName: string;
118
137
  sessionMaxAge: number;
119
138
  secureCookies: boolean | "auto";
139
+ cookieSameSite: "lax" | "none";
140
+ cookiePartitioned: boolean;
120
141
  exposeAccessToken: boolean;
121
142
  /** `{incortaUrl}/oauth/{tenant}` — the OIDC issuer for this tenant. */
122
143
  issuer: string;
@@ -1,6 +1,6 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { AuthSession, IncortaAuth } from './index.cjs';
3
- import './config-DAKbk8N5.cjs';
3
+ import './config-CHNLJVau.cjs';
4
4
  import 'jose';
5
5
 
6
6
  /**
package/dist/express.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { AuthSession, IncortaAuth } from './index.js';
3
- import './config-DAKbk8N5.js';
3
+ import './config-CHNLJVau.js';
4
4
  import 'jose';
5
5
 
6
6
  /**
package/dist/index.cjs CHANGED
@@ -77,6 +77,15 @@ function resolveConfig(input = {}) {
77
77
  const appUrl = input.appUrl ?? env.INCORTA_APP_URL;
78
78
  const cookieName = input.session?.cookieName ?? "incorta_auth_session";
79
79
  const issuer = `${incortaUrl}/oauth/${tenant}`;
80
+ const envSameSite = env.INCORTA_COOKIE_SAMESITE?.toLowerCase();
81
+ const cookieSameSite = input.cookies?.sameSite ?? (envSameSite === "none" || envSameSite === "lax" ? envSameSite : "lax");
82
+ const envPartitioned = env.INCORTA_COOKIE_PARTITIONED?.toLowerCase();
83
+ const cookiePartitioned = input.cookies?.partitioned ?? (envPartitioned === "1" || envPartitioned === "true");
84
+ if (cookiePartitioned && cookieSameSite !== "none") {
85
+ throw new Error(
86
+ '[incorta-auth] `cookies.partitioned` requires `cookies.sameSite: "none"` \u2014 browsers only partition cross-site cookies.'
87
+ );
88
+ }
80
89
  return {
81
90
  incortaUrl,
82
91
  internalIncortaUrl: internalIncortaUrl ? stripTrailingSlash(internalIncortaUrl) : void 0,
@@ -91,6 +100,8 @@ function resolveConfig(input = {}) {
91
100
  stateCookieName: `${cookieName}_state`,
92
101
  sessionMaxAge: input.session?.maxAge ?? 12 * 60 * 60,
93
102
  secureCookies: input.cookies?.secure ?? "auto",
103
+ cookieSameSite,
104
+ cookiePartitioned,
94
105
  exposeAccessToken: input.exposeAccessToken ?? false,
95
106
  issuer,
96
107
  discoveryUrl: input.advanced?.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`,
@@ -132,6 +143,7 @@ function serializeCookie(name, value, options = {}) {
132
143
  if (options.secure) parts.push("Secure");
133
144
  const sameSite = options.sameSite ?? "lax";
134
145
  parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);
146
+ if (options.partitioned) parts.push("Partitioned");
135
147
  return parts.join("; ");
136
148
  }
137
149
  function deleteCookie(name, options = {}) {
@@ -402,8 +414,12 @@ function randomToken() {
402
414
  function createHandler(config) {
403
415
  const sessionCookieOptions = (secure) => ({
404
416
  httpOnly: true,
405
- secure,
406
- sameSite: "lax",
417
+ // SameSite=None requires Secure — force the pair even on plain http
418
+ // (Chrome accepts Secure cookies from trustworthy origins like
419
+ // `*.localhost`; real cross-site embedding needs https anyway).
420
+ secure: config.cookieSameSite === "none" ? true : secure,
421
+ sameSite: config.cookieSameSite,
422
+ partitioned: config.cookiePartitioned,
407
423
  path: "/"
408
424
  });
409
425
  async function login(request, url) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/core/config.ts","../src/core/handler.ts","../src/core/cookies.ts","../src/core/discovery.ts","../src/core/session.ts","../src/core/tokens.ts","../src/core/register.ts"],"sourcesContent":["import { resolveConfig } from \"./core/config.js\";\nimport type { IncortaAuthConfig, ResolvedConfig } from \"./core/config.js\";\nimport { createHandler } from \"./core/handler.js\";\nimport type { AuthHandler, AuthSession } from \"./core/handler.js\";\n\nexport type {\n IncortaAuthConfig,\n IncortaUser,\n SessionData,\n SessionInfo,\n} from \"./core/config.js\";\nexport type { 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 /** 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 } = createHandler(resolved);\n return { handler, getSession, config: resolved };\n}\n","import type { JWTVerifyGetKey } from \"jose\";\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 /** OAuth client secret. Defaults to `INCORTA_CLIENT_SECRET`. */\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 /**\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 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 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 exposeAccessToken: boolean;\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 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 const clientSecret = required(\n input.clientSecret ?? env.INCORTA_CLIENT_SECRET,\n \"clientSecret\",\n \"INCORTA_CLIENT_SECRET\",\n );\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 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 exposeAccessToken: input.exposeAccessToken ?? false,\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 type { ResolvedConfig, SessionData, SessionInfo } from \"./config.js\";\nimport { deleteCookie, parseCookies, serializeCookie } from \"./cookies.js\";\nimport { getEndpoints } from \"./discovery.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\nexport type AuthHandler = (request: Request) => Promise<Response>;\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\nexport function createHandler(config: ResolvedConfig): {\n handler: AuthHandler;\n getSession: (request: Request) => Promise<AuthSession | null>;\n} {\n const sessionCookieOptions = (secure: boolean) => ({\n httpOnly: true,\n secure,\n sameSite: \"lax\" as const,\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 const stateCookie = serializeCookie(\n config.stateCookieName,\n await sealLoginState(config, { state, nonce, returnTo }),\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\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(config, code, `${origin}${config.basePath}/callback`);\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 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 const origin = requestOrigin(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n return redirect(returnTo, [clearSession]);\n }\n return json(200, { ok: true }, [clearSession]);\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 === \"/logout\" && (method === \"POST\" || method === \"GET\")) {\n return await logout(request, url);\n }\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 return { handler, getSession };\n}\n","export interface CookieOptions {\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: \"lax\" | \"strict\" | \"none\";\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 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 } 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\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 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(config: ResolvedConfig): string {\n const raw = `${config.clientId}:${config.clientSecret}`;\n return `Basic ${Buffer.from(raw, \"utf8\").toString(\"base64\")}`;\n}\n\nasync function callTokenEndpoint(\n config: ResolvedConfig,\n params: Record<string, string>,\n): Promise<TokenSet> {\n const { tokenEndpoint } = await getEndpoints(config);\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 authorization: basicAuth(config),\n },\n body: new URLSearchParams(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): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\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 clientSecret: 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; the response\n * carries the generated client_id/client_secret — store them once, e.g. in the\n * 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 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 || !body.client_secret) {\n throw new Error(\n `[incorta-auth] Client registration failed: response is missing client_id/client_secret.`,\n );\n }\n\n return {\n clientId: body.client_id,\n clientSecret: body.client_secret,\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_jose","import_jose","code"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/core/config.ts","../src/core/handler.ts","../src/core/cookies.ts","../src/core/discovery.ts","../src/core/session.ts","../src/core/tokens.ts","../src/core/register.ts"],"sourcesContent":["import { resolveConfig } from \"./core/config.js\";\nimport type { IncortaAuthConfig, ResolvedConfig } from \"./core/config.js\";\nimport { createHandler } from \"./core/handler.js\";\nimport type { AuthHandler, AuthSession } from \"./core/handler.js\";\n\nexport type {\n IncortaAuthConfig,\n IncortaUser,\n SessionData,\n SessionInfo,\n} from \"./core/config.js\";\nexport type { 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 /** 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 } = createHandler(resolved);\n return { handler, getSession, config: resolved };\n}\n","import type { JWTVerifyGetKey } from \"jose\";\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 /** OAuth client secret. Defaults to `INCORTA_CLIENT_SECRET`. */\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 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 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 /** `{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 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 const clientSecret = required(\n input.clientSecret ?? env.INCORTA_CLIENT_SECRET,\n \"clientSecret\",\n \"INCORTA_CLIENT_SECRET\",\n );\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 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 type { ResolvedConfig, SessionData, SessionInfo } from \"./config.js\";\nimport { deleteCookie, parseCookies, serializeCookie } from \"./cookies.js\";\nimport { getEndpoints } from \"./discovery.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\nexport type AuthHandler = (request: Request) => Promise<Response>;\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\nexport function createHandler(config: ResolvedConfig): {\n handler: AuthHandler;\n getSession: (request: Request) => Promise<AuthSession | null>;\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 const stateCookie = serializeCookie(\n config.stateCookieName,\n await sealLoginState(config, { state, nonce, returnTo }),\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\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(config, code, `${origin}${config.basePath}/callback`);\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 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 const origin = requestOrigin(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n return redirect(returnTo, [clearSession]);\n }\n return json(200, { ok: true }, [clearSession]);\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 === \"/logout\" && (method === \"POST\" || method === \"GET\")) {\n return await logout(request, url);\n }\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 return { handler, getSession };\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 } 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\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 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(config: ResolvedConfig): string {\n const raw = `${config.clientId}:${config.clientSecret}`;\n return `Basic ${Buffer.from(raw, \"utf8\").toString(\"base64\")}`;\n}\n\nasync function callTokenEndpoint(\n config: ResolvedConfig,\n params: Record<string, string>,\n): Promise<TokenSet> {\n const { tokenEndpoint } = await getEndpoints(config);\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 authorization: basicAuth(config),\n },\n body: new URLSearchParams(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): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\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 clientSecret: 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; the response\n * carries the generated client_id/client_secret — store them once, e.g. in the\n * 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 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 || !body.client_secret) {\n throw new Error(\n `[incorta-auth] Client registration failed: response is missing client_id/client_secret.`,\n );\n }\n\n return {\n clientId: body.client_id,\n clientSecret: body.client_secret,\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_jose","import_jose","code"]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SessionInfo, R as ResolvedConfig, I as IncortaAuthConfig } from './config-DAKbk8N5.cjs';
2
- export { a as IncortaUser, b as SessionData } from './config-DAKbk8N5.cjs';
1
+ import { S as SessionInfo, R as ResolvedConfig, I as IncortaAuthConfig } from './config-CHNLJVau.cjs';
2
+ export { a as IncortaUser, b as SessionData } from './config-CHNLJVau.cjs';
3
3
  import 'jose';
4
4
 
5
5
  /** A read-only view of the current session for server-side app code. */
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SessionInfo, R as ResolvedConfig, I as IncortaAuthConfig } from './config-DAKbk8N5.js';
2
- export { a as IncortaUser, b as SessionData } from './config-DAKbk8N5.js';
1
+ import { S as SessionInfo, R as ResolvedConfig, I as IncortaAuthConfig } from './config-CHNLJVau.js';
2
+ export { a as IncortaUser, b as SessionData } from './config-CHNLJVau.js';
3
3
  import 'jose';
4
4
 
5
5
  /** A read-only view of the current session for server-side app code. */
package/dist/index.js CHANGED
@@ -46,6 +46,15 @@ function resolveConfig(input = {}) {
46
46
  const appUrl = input.appUrl ?? env.INCORTA_APP_URL;
47
47
  const cookieName = input.session?.cookieName ?? "incorta_auth_session";
48
48
  const issuer = `${incortaUrl}/oauth/${tenant}`;
49
+ const envSameSite = env.INCORTA_COOKIE_SAMESITE?.toLowerCase();
50
+ const cookieSameSite = input.cookies?.sameSite ?? (envSameSite === "none" || envSameSite === "lax" ? envSameSite : "lax");
51
+ const envPartitioned = env.INCORTA_COOKIE_PARTITIONED?.toLowerCase();
52
+ const cookiePartitioned = input.cookies?.partitioned ?? (envPartitioned === "1" || envPartitioned === "true");
53
+ if (cookiePartitioned && cookieSameSite !== "none") {
54
+ throw new Error(
55
+ '[incorta-auth] `cookies.partitioned` requires `cookies.sameSite: "none"` \u2014 browsers only partition cross-site cookies.'
56
+ );
57
+ }
49
58
  return {
50
59
  incortaUrl,
51
60
  internalIncortaUrl: internalIncortaUrl ? stripTrailingSlash(internalIncortaUrl) : void 0,
@@ -60,6 +69,8 @@ function resolveConfig(input = {}) {
60
69
  stateCookieName: `${cookieName}_state`,
61
70
  sessionMaxAge: input.session?.maxAge ?? 12 * 60 * 60,
62
71
  secureCookies: input.cookies?.secure ?? "auto",
72
+ cookieSameSite,
73
+ cookiePartitioned,
63
74
  exposeAccessToken: input.exposeAccessToken ?? false,
64
75
  issuer,
65
76
  discoveryUrl: input.advanced?.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`,
@@ -101,6 +112,7 @@ function serializeCookie(name, value, options = {}) {
101
112
  if (options.secure) parts.push("Secure");
102
113
  const sameSite = options.sameSite ?? "lax";
103
114
  parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);
115
+ if (options.partitioned) parts.push("Partitioned");
104
116
  return parts.join("; ");
105
117
  }
106
118
  function deleteCookie(name, options = {}) {
@@ -371,8 +383,12 @@ function randomToken() {
371
383
  function createHandler(config) {
372
384
  const sessionCookieOptions = (secure) => ({
373
385
  httpOnly: true,
374
- secure,
375
- sameSite: "lax",
386
+ // SameSite=None requires Secure — force the pair even on plain http
387
+ // (Chrome accepts Secure cookies from trustworthy origins like
388
+ // `*.localhost`; real cross-site embedding needs https anyway).
389
+ secure: config.cookieSameSite === "none" ? true : secure,
390
+ sameSite: config.cookieSameSite,
391
+ partitioned: config.cookiePartitioned,
376
392
  path: "/"
377
393
  });
378
394
  async function login(request, url) {
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/session.ts","../src/core/tokens.ts","../src/core/register.ts","../src/index.ts"],"sourcesContent":["import type { JWTVerifyGetKey } from \"jose\";\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 /** OAuth client secret. Defaults to `INCORTA_CLIENT_SECRET`. */\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 /**\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 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 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 exposeAccessToken: boolean;\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 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 const clientSecret = required(\n input.clientSecret ?? env.INCORTA_CLIENT_SECRET,\n \"clientSecret\",\n \"INCORTA_CLIENT_SECRET\",\n );\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 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 exposeAccessToken: input.exposeAccessToken ?? false,\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 type { ResolvedConfig, SessionData, SessionInfo } from \"./config.js\";\nimport { deleteCookie, parseCookies, serializeCookie } from \"./cookies.js\";\nimport { getEndpoints } from \"./discovery.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\nexport type AuthHandler = (request: Request) => Promise<Response>;\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\nexport function createHandler(config: ResolvedConfig): {\n handler: AuthHandler;\n getSession: (request: Request) => Promise<AuthSession | null>;\n} {\n const sessionCookieOptions = (secure: boolean) => ({\n httpOnly: true,\n secure,\n sameSite: \"lax\" as const,\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 const stateCookie = serializeCookie(\n config.stateCookieName,\n await sealLoginState(config, { state, nonce, returnTo }),\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\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(config, code, `${origin}${config.basePath}/callback`);\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 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 const origin = requestOrigin(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n return redirect(returnTo, [clearSession]);\n }\n return json(200, { ok: true }, [clearSession]);\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 === \"/logout\" && (method === \"POST\" || method === \"GET\")) {\n return await logout(request, url);\n }\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 return { handler, getSession };\n}\n","export interface CookieOptions {\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: \"lax\" | \"strict\" | \"none\";\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 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 } 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\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 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(config: ResolvedConfig): string {\n const raw = `${config.clientId}:${config.clientSecret}`;\n return `Basic ${Buffer.from(raw, \"utf8\").toString(\"base64\")}`;\n}\n\nasync function callTokenEndpoint(\n config: ResolvedConfig,\n params: Record<string, string>,\n): Promise<TokenSet> {\n const { tokenEndpoint } = await getEndpoints(config);\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 authorization: basicAuth(config),\n },\n body: new URLSearchParams(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): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\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 clientSecret: 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; the response\n * carries the generated client_id/client_secret — store them once, e.g. in the\n * 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 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 || !body.client_secret) {\n throw new Error(\n `[incorta-auth] Client registration failed: response is missing client_id/client_secret.`,\n );\n }\n\n return {\n clientId: body.client_id,\n clientSecret: body.client_secret,\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 { resolveConfig } from \"./core/config.js\";\nimport type { IncortaAuthConfig, ResolvedConfig } from \"./core/config.js\";\nimport { createHandler } from \"./core/handler.js\";\nimport type { AuthHandler, AuthSession } from \"./core/handler.js\";\n\nexport type {\n IncortaAuthConfig,\n IncortaUser,\n SessionData,\n SessionInfo,\n} from \"./core/config.js\";\nexport type { 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 /** 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 } = createHandler(resolved);\n return { handler, getSession, config: resolved };\n}\n"],"mappings":"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","names":["code"]}
1
+ {"version":3,"sources":["../src/core/config.ts","../src/core/handler.ts","../src/core/cookies.ts","../src/core/discovery.ts","../src/core/session.ts","../src/core/tokens.ts","../src/core/register.ts","../src/index.ts"],"sourcesContent":["import type { JWTVerifyGetKey } from \"jose\";\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 /** OAuth client secret. Defaults to `INCORTA_CLIENT_SECRET`. */\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 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 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 /** `{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 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 const clientSecret = required(\n input.clientSecret ?? env.INCORTA_CLIENT_SECRET,\n \"clientSecret\",\n \"INCORTA_CLIENT_SECRET\",\n );\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 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 type { ResolvedConfig, SessionData, SessionInfo } from \"./config.js\";\nimport { deleteCookie, parseCookies, serializeCookie } from \"./cookies.js\";\nimport { getEndpoints } from \"./discovery.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\nexport type AuthHandler = (request: Request) => Promise<Response>;\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\nexport function createHandler(config: ResolvedConfig): {\n handler: AuthHandler;\n getSession: (request: Request) => Promise<AuthSession | null>;\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 const stateCookie = serializeCookie(\n config.stateCookieName,\n await sealLoginState(config, { state, nonce, returnTo }),\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\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(config, code, `${origin}${config.basePath}/callback`);\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 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 const origin = requestOrigin(config, request);\n const returnTo = validateReturnTo(url.searchParams.get(\"redirect_to\"), origin);\n return redirect(returnTo, [clearSession]);\n }\n return json(200, { ok: true }, [clearSession]);\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 === \"/logout\" && (method === \"POST\" || method === \"GET\")) {\n return await logout(request, url);\n }\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 return { handler, getSession };\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 } 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\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 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(config: ResolvedConfig): string {\n const raw = `${config.clientId}:${config.clientSecret}`;\n return `Basic ${Buffer.from(raw, \"utf8\").toString(\"base64\")}`;\n}\n\nasync function callTokenEndpoint(\n config: ResolvedConfig,\n params: Record<string, string>,\n): Promise<TokenSet> {\n const { tokenEndpoint } = await getEndpoints(config);\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 authorization: basicAuth(config),\n },\n body: new URLSearchParams(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): Promise<TokenSet> {\n return callTokenEndpoint(config, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\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 clientSecret: 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; the response\n * carries the generated client_id/client_secret — store them once, e.g. in the\n * 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 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 || !body.client_secret) {\n throw new Error(\n `[incorta-auth] Client registration failed: response is missing client_id/client_secret.`,\n );\n }\n\n return {\n clientId: body.client_id,\n clientSecret: body.client_secret,\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 { resolveConfig } from \"./core/config.js\";\nimport type { IncortaAuthConfig, ResolvedConfig } from \"./core/config.js\";\nimport { createHandler } from \"./core/handler.js\";\nimport type { AuthHandler, AuthSession } from \"./core/handler.js\";\n\nexport type {\n IncortaAuthConfig,\n IncortaUser,\n SessionData,\n SessionInfo,\n} from \"./core/config.js\";\nexport type { 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 /** 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 } = createHandler(resolved);\n return { handler, getSession, config: resolved };\n}\n"],"mappings":"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","names":["code"]}
package/dist/node.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IncortaAuth, AuthSession } from './index.cjs';
2
2
  import { IncomingMessage, ServerResponse } from 'node:http';
3
- import './config-DAKbk8N5.cjs';
3
+ import './config-CHNLJVau.cjs';
4
4
  import 'jose';
5
5
 
6
6
  /**
package/dist/node.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IncortaAuth, AuthSession } from './index.js';
2
2
  import { IncomingMessage, ServerResponse } from 'node:http';
3
- import './config-DAKbk8N5.js';
3
+ import './config-CHNLJVau.js';
4
4
  import 'jose';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { a as IncortaUser } from '../config-DAKbk8N5.cjs';
3
+ import { a as IncortaUser } from '../config-CHNLJVau.cjs';
4
4
  import 'jose';
5
5
 
6
6
  interface SignInOptions {
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { a as IncortaUser } from '../config-DAKbk8N5.js';
3
+ import { a as IncortaUser } from '../config-CHNLJVau.js';
4
4
  import 'jose';
5
5
 
6
6
  interface SignInOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@incorta/auth",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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": {