{"version":3,"file":"middleware.mjs","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["/**\n * Shared constants and utility functions for the SDK middleware proxy layer.\n *\n * These are consumed by every server SDK to avoid duplicating\n * framework-agnostic logic.\n */\n\nimport type { JwtPayload } from \"./jwt.js\";\n\nimport { joinURL } from \"ufo\";\n\nimport { isJwtShaped, verifyJwt } from \"./jwt.js\";\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\n/**\n * Headers that must never be forwarded to an upstream service.\n * These are connection-level headers that are meaningful only between\n * two directly connected peers and become invalid when proxied.\n */\nexport const HOP_BY_HOP: ReadonlySet<string> = new Set([\n  \"connection\",\n  // content-length is not hop-by-hop per RFC 7230, but it must be stripped: the\n  // proxy re-sends a freshly buffered body, so the fetch implementation computes\n  // the correct content-length itself. Forwarding the client's value is at best\n  // redundant and at worst fatal — undici rejects a manually-set content-length\n  // alongside a body with `UND_ERR_INVALID_ARG: invalid content-length header`,\n  // which surfaced as an opaque \"fetch failed\" in the Qwik Router proxy.\n  \"content-length\",\n  // host is not a hop-by-hop header per RFC 7230, but it must be stripped so\n  // the fetch implementation derives the correct Host from the upstream URL\n  // rather than forwarding the client's Host and causing SNI/vhost mismatches.\n  \"host\",\n  \"keep-alive\",\n  \"proxy-authenticate\",\n  \"proxy-authorization\",\n  \"te\",\n  \"trailer\",\n  \"transfer-encoding\",\n  \"upgrade\",\n]);\n\n/**\n * SDK-internal headers that must never be forwarded to the upstream backend.\n * These headers carry session data between the middleware and server components;\n * forwarding them upstream would expose internal state and allow header injection.\n */\nexport const INTERNAL_HEADERS: ReadonlySet<string> = new Set([\"x-nextgen-auth-token\"]);\n\n// ─── Route matching ──────────────────────────────────────────────────────────\n\n/**\n * Returns `true` when `pathname` matches at least one entry in `routes`.\n *\n * An entry ending with `*` matches any path that starts with the prefix\n * before the `*`. All other entries match exactly, ignoring a trailing slash\n * on either side: `/profile` and `/profile/` name the same route, and several\n * frameworks (Qwik Router by default) serve the slashed form as canonical. An\n * exact compare would let `/profile/` past a `protectedRoutes: [\"/profile\"]`.\n *\n * @param pathname - The URL pathname to test.\n * @param routes   - The list of route patterns to match against.\n * @returns `true` if at least one pattern matches.\n */\nexport function matchesRoutes(pathname: string, routes: readonly string[]): boolean {\n  // A linear scan, not /\\/+$/: that regex backtracks quadratically on a long\n  // run of slashes, and the pathname is attacker-controlled on every request.\n  const withoutTrailingSlash = (path: string): string => {\n    let end = path.length;\n    while (end > 1 && path.charCodeAt(end - 1) === 0x2f) {\n      end--;\n    }\n    return path.slice(0, end);\n  };\n  const path = withoutTrailingSlash(pathname);\n\n  return routes.some((route) => {\n    if (route.endsWith(\"*\")) {\n      return pathname.startsWith(route.slice(0, -1));\n    }\n    return path === withoutTrailingSlash(route);\n  });\n}\n\n// ─── Login redirect ──────────────────────────────────────────────────────────\n\n/**\n * The query parameter that brings a signed-out request's path to the login\n * page, where `<zitadel-login>` returns after sign-in.\n */\nconst RETURN_PATH_PARAM = \"next\";\n\n/**\n * Where a signed-out request for a protected route is sent: `loginPath`, with\n * the requested path and query string in `next`, so `<zitadel-login>` returns\n * to the page as it was asked for, filters and tabs included. The fragment\n * never reaches the server, so it cannot be carried.\n *\n * A `next` the request carries itself is left out, so a redirect never nests\n * an earlier one: should the login page be protected by mistake, each pass of\n * the redirect loop would otherwise wrap the last and grow the URL. The rest\n * of the query keeps the encoding it was sent with.\n *\n * Whatever query `loginPath` has of its own is kept, except a `next`, which\n * this replaces.\n *\n * @param loginPath  - The login page, a path starting with a single `/`.\n * @param requestUrl - The full URL of the signed-out request.\n * @returns The URL to redirect to, on the request's origin.\n */\nexport function loginRedirectUrl(loginPath: string, requestUrl: string | URL): URL {\n  // A copy made from the whole URL, never the caller's `search`: SvelteKit\n  // makes `search` throw on the request event's URL while it prerenders.\n  const requested = new URL(requestUrl);\n  const query = requested.search\n    .slice(1)\n    .split(\"&\")\n    .filter((pair) => !new URLSearchParams(pair).has(RETURN_PATH_PARAM))\n    .join(\"&\");\n  const loginUrl = new URL(loginPath, requested);\n  loginUrl.searchParams.set(\n    RETURN_PATH_PARAM,\n    query === \"\" ? requested.pathname : `${requested.pathname}?${query}`,\n  );\n  return loginUrl;\n}\n\n// ─── Proxy credentials ───────────────────────────────────────────────────────\n\n/**\n * Whether a proxied call may carry the confidential project secret.\n *\n * ADR 036: the browser flow needs the secret only for the handoff exchange\n * (`POST /sessions/exchange`). Stamping it onto every proxied operation would\n * turn the app's public `/__nextgen/*` boundary into an open relay carrying\n * project-level authority for any anonymous caller.\n *\n * @param method   - The proxied request's HTTP method.\n * @param pathname - The path after the proxy prefix, e.g. `/sessions/exchange`.\n * @returns `true` only for the handoff exchange.\n */\nexport function requiresProjectSecret(method: string, pathname: string): boolean {\n  return method.toUpperCase() === \"POST\" && pathname === \"/sessions/exchange\";\n}\n\n// ─── Response header filtering ───────────────────────────────────────────────\n\n/**\n * An upstream `Location`, pointed back at this app.\n *\n * Only the path, query and fragment survive; whatever origin the upstream\n * named is discarded and the result is re-based onto the request's own URL.\n * That keeps both properties the blanket strip was protecting -- an internal\n * hostname can never reach the browser, and the redirect can only ever land on\n * this app -- without needing to know what this app's public origin is.\n *\n * Comparing origins instead would have required that knowledge, and there is\n * no reliable source for it: `X-Forwarded-Host` is attacker-settable on a\n * direct request, and the request's own host is wrong behind a proxy that\n * terminates TLS or rewrites it. Discarding the origin sidesteps the question\n * -- a redirect to another origin becomes the same path on this one rather\n * than being dropped, so a deployment behind such a proxy works instead of\n * landing on a blank page.\n *\n * Nothing legitimate on this path points off-origin anyway: an outbound\n * redirect to a provider travels as `step.redirect_url` in a JSON body and the\n * widget performs it, never as a proxied header.\n *\n * A relative reference resolves against the path it came from (RFC 3986 §5),\n * so `next?flow=x` answered to `/__nextgen/idp/google/callback` means\n * `/__nextgen/idp/google/next?flow=x`. An unparseable value is the one case with\n * nothing to forward.\n */\nfunction ontoThisApp(location: string, selfUrl: string | undefined): string | undefined {\n  if (selfUrl === undefined || selfUrl === \"\") {\n    return undefined;\n  }\n  try {\n    const base = new URL(selfUrl);\n    const target = new URL(location, base);\n    return new URL(`${target.pathname}${target.search}${target.hash}`, base).toString();\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Filters upstream response headers for proxying: strips hop-by-hop headers\n * and `set-cookie` (handled separately via `getSetCookie()`), and reduces\n * `location` to a redirect that can only land on this app.\n *\n * `location` was stripped outright until the identity-provider callback\n * needed it: the provider returns the browser to `/__nextgen/idp/{slug}/callback`,\n * and the engine answers `303` back to the page the sign-in started on. That\n * is a top-level navigation with no JavaScript in the loop, so a dropped\n * `Location` leaves the browser on an empty page. Outbound redirects to a\n * provider do not travel this way -- they arrive as `step.redirect_url` in a\n * JSON body and the widget navigates -- so nothing legitimate points off-origin.\n *\n * @param upstream - The upstream response headers.\n * @param selfUrl - The app's own request URL, which is both the origin a\n *   redirect must land on and the base a relative one resolves against.\n *   Omitting it keeps the old behaviour of dropping every redirect.\n * @returns A new `Headers` object with filtered headers.\n */\nexport function filterResponseHeaders(upstream: Headers, selfUrl?: string): Headers {\n  const filtered = new Headers();\n  upstream.forEach((value, key) => {\n    const name = key.toLowerCase();\n    if (HOP_BY_HOP.has(name) || name === \"set-cookie\") {\n      return;\n    }\n    if (name === \"location\") {\n      const onThisApp = ontoThisApp(value, selfUrl);\n      if (onThisApp !== undefined) {\n        filtered.set(\"location\", onThisApp);\n      }\n      return;\n    }\n    filtered.set(key, value);\n  });\n  return filtered;\n}\n\n// ─── Session cookies ─────────────────────────────────────────────────────────\n\n/** The cookie the server sets a session in and reads it from. */\nexport const SERVER_SESSION_COOKIE = \"__nextgen_session\";\n\n/** Every app's own session cookie starts with this. */\nconst PROJECT_SESSION_COOKIE_PREFIX = `${SERVER_SESSION_COOKIE}_`;\n\n/**\n * The cookie an app keeps its project's session in: the server's name with the\n * project id appended (ADR 073). A browser shares one cookie jar across every\n * port of a host, so under the server's name alone the Console of a local\n * server and every app on `localhost` would hold their sessions in one slot and\n * replace each other's.\n *\n * Every byte of the id outside `[A-Za-z0-9_-]` is percent-encoded, so any\n * configured id makes a valid cookie name. Ids the server mints need none.\n */\nexport function sessionCookieName(projectId: string): string {\n  let name = PROJECT_SESSION_COOKIE_PREFIX;\n  for (const byte of new TextEncoder().encode(projectId)) {\n    const char = String.fromCharCode(byte);\n    name += /[A-Za-z0-9_-]/.test(char)\n      ? char\n      : `%${byte.toString(16).toUpperCase().padStart(2, \"0\")}`;\n  }\n  return name;\n}\n\n/** Whether `name` is a session cookie: the server's, or any app's own. */\nfunction isSessionCookie(name: string): boolean {\n  return name === SERVER_SESSION_COOKIE || name.startsWith(PROJECT_SESSION_COOKIE_PREFIX);\n}\n\n/**\n * A session cookie a response sets. An empty `value` with `maxAge: 0` clears\n * it.\n */\nexport interface SessionCookie {\n  readonly name: string;\n  readonly value: string;\n  /** Seconds the browser keeps it, or `undefined` for the browser session. */\n  readonly maxAge: number | undefined;\n  readonly secure: boolean;\n}\n\n/**\n * The `Set-Cookie` value for `cookie`, with the attributes the server gives its\n * own session cookie: `Path=/`, `HttpOnly`, `SameSite=Lax`.\n */\nexport function serializeSessionCookie(cookie: SessionCookie): string {\n  const attributes = [`${cookie.name}=${cookie.value}`, \"Path=/\"];\n  if (cookie.maxAge !== undefined) {\n    attributes.push(`Max-Age=${cookie.maxAge}`);\n  }\n  attributes.push(\"HttpOnly\", \"SameSite=Lax\");\n  if (cookie.secure) {\n    attributes.push(\"Secure\");\n  }\n  return attributes.join(\"; \");\n}\n\n/**\n * The `Cache-Control` of a response that sets a session cookie, as the server\n * sends on its session answers: a shared cache must not hand the cookie, the\n * session token it carries or its clearing, to the next visitor.\n */\nexport const SESSION_COOKIE_CACHE_CONTROL = \"private, no-store\";\n\n/** The browser's request as the app sees it, and as a proxy in front of it forwarded it. */\nexport interface BrowserRequest {\n  /** The app's own request URL. */\n  readonly requestUrl: string | URL;\n  /** The request's `X-Forwarded-Proto`, or `null`. */\n  readonly forwardedProto?: string | null;\n  /** The request's `X-Forwarded-Host`, or `null`. */\n  readonly forwardedHost?: string | null;\n}\n\n/** The first entry of a forwarded header a chain of proxies appended to, if any. */\nfunction firstForwarded(value: string | null | undefined): string | undefined {\n  const first = value?.split(\",\")[0]?.trim();\n  return first ? first : undefined;\n}\n\n/**\n * Whether a session cookie this app sets needs `Secure`, by the rule the server\n * applies to its own (`cookieSecureFromContext`): always, except over plain\n * http to a loopback host, where Safari would drop a `Secure` cookie. The\n * scheme and host are what a proxy in front of the app forwarded (the first\n * `X-Forwarded-Proto` and `X-Forwarded-Host`), else the app's request URL's,\n * so an app behind a TLS proxy that talks to it over loopback keeps `Secure`.\n */\nfunction secureFor(request: BrowserRequest): boolean {\n  let url: URL;\n  try {\n    url = new URL(request.requestUrl);\n  } catch {\n    return true;\n  }\n  const proto = firstForwarded(request.forwardedProto) ?? url.protocol.slice(0, -1);\n  if (proto.toLowerCase() !== \"http\") {\n    return true;\n  }\n  let host: string;\n  try {\n    host = new URL(`http://${firstForwarded(request.forwardedHost) ?? url.host}`).hostname;\n  } catch {\n    return true;\n  }\n  host = host.toLowerCase();\n  return !(host === \"localhost\" || host === \"[::1]\" || /^127(?:\\.\\d{1,3}){3}$/.test(host));\n}\n\n/** Seconds from now until `epochMs`, at least one, or `undefined` when unknown. */\nfunction secondsUntil(epochMs: number): number | undefined {\n  return Number.isFinite(epochMs)\n    ? Math.max(1, Math.floor((epochMs - Date.now()) / 1000))\n    : undefined;\n}\n\n/** Clears the cookie `name`. */\nfunction clearedCookie(name: string, secure: boolean): SessionCookie {\n  return { name, value: \"\", maxAge: 0, secure };\n}\n\n/**\n * Copies a session of this project found in the server's cookie into the\n * project's own. The server's cookie keeps it: on a server without platform\n * bootstrap the Console signs in to the app's project, and clearing the cookie\n * would sign it out. The proxy retires the server's cookie when the app signs\n * a user in or out ({@link proxySessionCookies}), so it cannot sign its user\n * back in once the app's own cookie is gone.\n */\nfunction copiedToProjectCookie(\n  token: string,\n  projectId: string,\n  maxAge: number | undefined,\n  secure: boolean,\n): SessionCookie {\n  return { name: sessionCookieName(projectId), value: token, maxAge, secure };\n}\n\n/** The pieces of a `Cookie` header, each with the name it carries. */\nfunction cookiePieces(header: string | null): { name: string; piece: string }[] {\n  const pieces: { name: string; piece: string }[] = [];\n  for (const raw of (header ?? \"\").split(\";\")) {\n    const piece = raw.trim();\n    if (piece !== \"\") {\n      const index = piece.indexOf(\"=\");\n      pieces.push({ name: (index === -1 ? piece : piece.slice(0, index)).trim(), piece });\n    }\n  }\n  return pieces;\n}\n\n/** The value of the first cookie named `name`, or `null` when it is absent or empty. */\nfunction cookieValue(pieces: { name: string; piece: string }[], name: string): string | null {\n  const found = pieces.find((candidate) => candidate.name === name);\n  const value = found ? found.piece.slice(found.piece.indexOf(\"=\") + 1).trim() : \"\";\n  return value === \"\" ? null : value;\n}\n\n/** The name a `Set-Cookie` value sets. */\nfunction setCookieName(setCookie: string): string {\n  const index = setCookie.indexOf(\"=\");\n  return index === -1 ? \"\" : setCookie.slice(0, index).trim();\n}\n\n/**\n * What a proxied request carries that decides its session cookies. The\n * request URL and the forwarded headers decide whether a cookie needs `Secure`.\n */\nexport interface ProxiedRequest extends BrowserRequest {\n  readonly method: string;\n  /** The path after the proxy prefix, e.g. `/sessions/me`. */\n  readonly path: string;\n  /** The query string, with its leading `?`, or `\"\"`. */\n  readonly search: string;\n  /** The request's `Cookie` header, or `null`. */\n  readonly cookie: string | null;\n}\n\n/** The server a proxy forwards to, for the calls it makes of its own. */\nexport interface ProxyUpstream {\n  /** Base URL of the auth backend. */\n  readonly url: string;\n  /** How long to wait for an answer. */\n  readonly timeoutMs: number;\n}\n\n/** What a proxied answer sends the browser. */\nexport interface ProxiedAnswer {\n  readonly setCookies: string[];\n  readonly body: ReadableStream<Uint8Array> | string | null;\n}\n\n/** A proxied call's session cookies, on the way up and on the way back. */\nexport interface ProxiedSessionCookies {\n  /** The `Cookie` header to send upstream, or `null` to send none. */\n  readonly upstreamCookie: string | null;\n  /**\n   * The upstream answer's way back to the browser: the `Set-Cookie` values and\n   * the body to send. Send this body, never the upstream response's own, which\n   * this may have read.\n   */\n  toBrowser(upstream: Response): Promise<ProxiedAnswer>;\n}\n\n/**\n * The session cookies of a call the app proxies to the server (ADR 073). The\n * server knows one session cookie, `__nextgen_session`; the app keeps its\n * project's session in {@link sessionCookieName}, so it neither replaces nor\n * reads the session of another app or of the Console on the same host.\n *\n * - On the way up, the app's own session cookie goes under the server's name,\n *   and every other session cookie stays behind. Other cookies pass unchanged.\n * - On the way back, a `Set-Cookie` of the server's session cookie, which sets\n *   or clears it, is renamed to the app's.\n * - A session kept under the server's name goes up to one call only, the\n *   session probe `GET /sessions/me?project_id=<project>`, which the server\n *   refuses for another project's session. When the answer signs a user of\n *   this project in, the session is copied to the app's cookie; the server's\n *   cookie keeps it, since the Console may share the project.\n * - When the app signs a user in (`POST /sessions/exchange`) or out\n *   (`DELETE /sessions/me`), a session still kept under the server's name is\n *   retired, or it would sign its user back in once the app's own cookie is\n *   gone: the server is asked about it in this project, and a session of this\n *   project is cleared (and on sign-out also ended), a dead one cleared, and\n *   one of another project, the Console's on `localhost`, left alone.\n * - Without a project the cookies pass as they are: nothing says which session\n *   is the app's.\n *\n * @param request   - The proxied request.\n * @param projectId - The project this app signs its users in to.\n * @param upstream  - The server the proxy forwards to, which it asks about a\n *   session kept under the server's name.\n */\nexport function proxySessionCookies(\n  request: ProxiedRequest,\n  projectId: string | undefined,\n  upstream: ProxyUpstream,\n): ProxiedSessionCookies {\n  if (!projectId) {\n    return {\n      upstreamCookie: request.cookie,\n      toBrowser: async (upstream) => ({\n        setCookies: upstream.headers.getSetCookie?.() ?? [],\n        body: upstream.body,\n      }),\n    };\n  }\n\n  const ownName = sessionCookieName(projectId);\n  const pieces = cookiePieces(request.cookie);\n  const own = cookieValue(pieces, ownName);\n  // A session kept under the server's name, which goes up to the probe only.\n  const shared = cookieValue(pieces, SERVER_SESSION_COOKIE);\n  const method = request.method.toUpperCase();\n  const isProbe =\n    method === \"GET\" &&\n    request.path === \"/sessions/me\" &&\n    new URLSearchParams(request.search).get(\"project_id\") === projectId;\n  const signsIn = method === \"POST\" && request.path === \"/sessions/exchange\";\n  const signsOut = method === \"DELETE\" && request.path === \"/sessions/me\";\n  const probed = own === null && isProbe ? shared : null;\n  const secure = secureFor(request);\n\n  const sent = pieces.filter(({ name }) => !isSessionCookie(name)).map(({ piece }) => piece);\n  const session = own ?? probed;\n  if (session !== null) {\n    sent.push(`${SERVER_SESSION_COOKIE}=${session}`);\n  }\n\n  return {\n    upstreamCookie: sent.length > 0 ? sent.join(\"; \") : null,\n    async toBrowser(answer) {\n      const upstreamSetCookies = answer.headers.getSetCookie?.() ?? [];\n      const setCookies = upstreamSetCookies.map((setCookie) =>\n        setCookieName(setCookie) === SERVER_SESSION_COOKIE\n          ? `${ownName}${setCookie.slice(setCookie.indexOf(\"=\"))}`\n          : setCookie,\n      );\n      // A sign-in that set the app's session, or any sign-out: the session the\n      // server's cookie still holds goes too, unless it is another project's.\n      const setsSession = upstreamSetCookies.some(\n        (setCookie) => setCookieName(setCookie) === SERVER_SESSION_COOKIE,\n      );\n      if (shared !== null && (signsOut || (signsIn && answer.ok && setsSession))) {\n        if (await retiresSharedSession(shared, projectId, upstream, signsOut)) {\n          setCookies.push(serializeSessionCookie(clearedCookie(SERVER_SESSION_COOKIE, secure)));\n        }\n      }\n      if (probed === null || answer.status !== 200) {\n        return { setCookies, body: answer.body };\n      }\n      const body = await answer.text();\n      const signedIn = sessionAnswer(parsedJson(body), projectId);\n      if (\n        signedIn.userId &&\n        !setCookies.some((setCookie) => setCookieName(setCookie) === ownName)\n      ) {\n        const maxAge = signedIn.expiresAt\n          ? secondsUntil(Date.parse(signedIn.expiresAt))\n          : undefined;\n        setCookies.push(\n          serializeSessionCookie(copiedToProjectCookie(probed, projectId, maxAge, secure)),\n        );\n      }\n      return { setCookies, body };\n    },\n  };\n}\n\n/**\n * Whether the session kept under the server's name goes when the app signs a\n * user in or out: the server is asked about it in this project. A session of\n * this project goes, and on sign-out it is ended too, so that it cannot sign\n * its user back in; one the server no longer holds goes as well. One of another\n * project, the Console's on `localhost`, stays, and so does one the server gave\n * no verdict on, and a JWT, which the middleware judges itself.\n */\nasync function retiresSharedSession(\n  token: string,\n  projectId: string,\n  upstream: ProxyUpstream,\n  signsOut: boolean,\n): Promise<boolean> {\n  if (isJwtShaped(token)) {\n    return false;\n  }\n  const cookie = `${SERVER_SESSION_COOKIE}=${token}`;\n  let res: Response;\n  try {\n    res = await fetch(\n      `${joinURL(upstream.url, \"/sessions/me\")}?project_id=${encodeURIComponent(projectId)}`,\n      {\n        method: \"GET\",\n        headers: { cookie },\n        signal: AbortSignal.timeout(upstream.timeoutMs),\n      },\n    );\n  } catch {\n    return false;\n  }\n  if (res.status === 401) {\n    return true;\n  }\n  if (res.status === 404 || res.status === 403) {\n    return (await errorCode(res)) === SESSION_NOT_FOUND;\n  }\n  if (!res.ok) {\n    return false;\n  }\n  const answer = parsedJson(await res.text().catch(() => \"\")) as\n    | { project_id?: unknown }\n    | undefined;\n  if (answer?.project_id !== projectId) {\n    return false;\n  }\n  if (signsOut) {\n    // A failed revoke still clears the cookie: the browser no longer holds the\n    // session.\n    await revokeSession(cookie, projectId, upstream);\n  }\n  return true;\n}\n\n/** The header the session's CSRF token travels in (ADR 053 §5). */\nconst CSRF_HEADER = \"X-Zitadel-CSRF\";\n\n/**\n * Ends the session `cookie` carries, as `@zitadel/components` ends its own.\n * Sign-out is a write, so it carries the session's CSRF token (ADR 053 §5),\n * read first from `GET /sessions/me/csrf` in this project: a call without an\n * Origin passes the server's cross-site check, but not its token check.\n * Without a token it sends no revoke, which the server would refuse.\n */\nasync function revokeSession(\n  cookie: string,\n  projectId: string,\n  upstream: ProxyUpstream,\n): Promise<void> {\n  try {\n    const res = await fetch(\n      `${joinURL(upstream.url, \"/sessions/me/csrf\")}?project_id=${encodeURIComponent(projectId)}`,\n      {\n        method: \"GET\",\n        headers: { cookie },\n        signal: AbortSignal.timeout(upstream.timeoutMs),\n      },\n    );\n    const answer = res.ok\n      ? (parsedJson(await res.text()) as { csrf_token?: unknown } | undefined)\n      : undefined;\n    if (typeof answer?.csrf_token !== \"string\") {\n      return;\n    }\n    await fetch(joinURL(upstream.url, \"/sessions/me\"), {\n      method: \"DELETE\",\n      headers: { cookie, [CSRF_HEADER]: answer.csrf_token },\n      signal: AbortSignal.timeout(upstream.timeoutMs),\n    });\n  } catch {\n    // No answer: the caller clears the cookie all the same.\n  }\n}\n\n/** `text` as JSON, or `undefined` when it is not JSON. */\nfunction parsedJson(text: string): unknown {\n  try {\n    return JSON.parse(text);\n  } catch {\n    return undefined;\n  }\n}\n\n// ─── Middleware types ──────────────────────────────────────────────────────────\n\n/**\n * The authenticated session for a signed-in user. Identity follows the\n * user-ref vocabulary (ADR 058): render `display`, falling back to\n * `identifier`, then `userId`.\n */\nexport type NextgenSession = {\n  /** The user's unique identifier (`sub` claim). */\n  userId: string;\n  /**\n   * The user schema's designated identifier value (the login identifier,\n   * e.g. an email address or username), or `null` when unresolved. On the\n   * JWT path this carries the `email` claim.\n   */\n  identifier: string | null;\n  /**\n   * The schema property `identifier` came from (e.g. `\"email\"`), or `null`\n   * when unknown — JWT-claim identities carry no property attribution.\n   */\n  identifierProperty: string | null;\n  /** The user's display name rendering, or `null` when the schema designates none. */\n  display: string | null;\n  /** The raw verified JWT. */\n  token: string;\n};\n\n/** Auth state when the user is signed in. */\nexport type AuthState = { isAuthenticated: true; session: NextgenSession };\n\n/** Auth state when the user is not signed in. */\nexport type UnauthState = { isAuthenticated: false; session: null };\n\n/** Union of all possible auth states. */\nexport type AuthResult = AuthState | UnauthState;\n\n/**\n * The client-safe session exposed to app UI (headers, account menus).\n * Identical to {@link NextgenSession} but omits `token` — the raw session\n * token must never reach client-side JavaScript, whether through an SSR\n * payload or a client-side fetch result.\n */\nexport type ClientSession = {\n  /** The user's unique identifier (`sub` claim). */\n  userId: string;\n  /** The designated identifier value (login identifier), or `null`. */\n  identifier: string | null;\n  /** The schema property the identifier came from, or `null`. */\n  identifierProperty: string | null;\n  /** The display name rendering, or `null`. */\n  display: string | null;\n};\n\n/** Client-safe auth state when the user is signed in. */\nexport type ClientAuthState = { isAuthenticated: true; session: ClientSession };\n\n/**\n * Union of all possible client-safe auth states. Returned by the client\n * session reads (`useAuth()` in sdk-nuxt, `getSession()` in sdk-next).\n * Token is intentionally absent — use the server-side helpers when the raw\n * token is needed.\n */\nexport type ClientAuthResult = ClientAuthState | UnauthState;\n\n/**\n * Options passed to the SDK middleware factory (`nextgenMiddleware` in\n * sdk-next, `createNextgenMiddleware` in sdk-nuxt).\n */\nexport type NextgenMiddlewareOptions = {\n  /**\n   * Full URL of the Zitadel auth backend.\n   * @default process.env.ZITADEL_URL ?? \"http://localhost:8080\"\n   */\n  url?: string;\n\n  /**\n   * The project this app signs its users in to. A session counts as signed in\n   * only when it belongs to this project. A session of any other project on the\n   * same server (a second app, or the Console, whose cookie the browser sends\n   * to every port of a shared host) reads as signed out, and its cookie is left\n   * alone.\n   *\n   * The app keeps its session in a cookie of its own,\n   * `__nextgen_session_<projectId>` ({@link sessionCookieName}), so signing in\n   * here neither replaces nor reads the session of another app or of the\n   * Console on the same host (ADR 073).\n   *\n   * An opaque session token is checked by the backend\n   * (`GET /sessions/me?project_id=…`) and against the project its answer names;\n   * a JWT must name the project in its `aud` claim.\n   *\n   * Required: with no project, no session counts as signed in, and the proxy\n   * passes the server's session cookie through unchanged. Each SDK falls back\n   * to the `ZITADEL_PROJECT_ID` environment variable, then to its framework's\n   * public project id variable, which `zitadel setup` writes to `.env.local`.\n   */\n  projectId?: string;\n\n  /**\n   * URL path prefix that is reverse-proxied to the auth backend.\n   *\n   * Sign-in with an external provider currently requires the default: the\n   * provider returns to `/__nextgen/idp/{slug}/callback`, which is proxied only under\n   * the default prefix.\n   * @default \"/__nextgen\"\n   */\n  proxyPath?: string;\n\n  /**\n   * Pathnames that require a valid session. Requests to these routes without\n   * a valid token are redirected to {@link loginPath}.\n   * Entries ending with `*` match any sub-path (e.g. `\"/admin*\"`).\n   *\n   * When the backend cannot say whether a session cookie is live (it answers\n   * `5xx`, does not answer in time, or cannot be reached), the route answers\n   * `503 Service Unavailable` instead: the user may well be signed in, and the\n   * login page would tell them they are not.\n   * @default []\n   */\n  protectedRoutes?: string[];\n\n  /**\n   * Pathnames that are completely skipped by the middleware — no JWT\n   * verification, no protection check, no header tunnelling.\n   * Useful for webhooks, health-check endpoints, or any route where\n   * running auth logic is undesirable.\n   * Entries ending with `*` match any sub-path (e.g. `\"/public*\"`).\n   * @default []\n   */\n  ignoredRoutes?: string[];\n\n  /**\n   * Pathname to redirect unauthenticated users to.\n   * A `next` query parameter carries the requested path and query string,\n   * where `<zitadel-login>` returns after sign-in.\n   * @default \"/login\"\n   */\n  loginPath?: string;\n\n  /**\n   * Reserved for future use. Local PEM public key for offline JWT verification.\n   */\n  jwtKey?: string;\n\n  /**\n   * Restrict accepted JWT `alg` header values. Tokens whose algorithm is not\n   * in this list are rejected before JWKS is fetched.\n   * @default [\"RS256\", \"ES256\"]\n   */\n  allowedAlgorithms?: string[];\n\n  /**\n   * Clock skew tolerance in milliseconds applied to `exp`, `nbf`, and `iat`\n   * claim validation.\n   * @default 5000\n   */\n  clockSkewMs?: number;\n\n  /**\n   * Expected value(s) of the `aud` claim. When omitted, audience is not\n   * validated. Pass a single string or an array when a token may carry\n   * multiple audiences.\n   */\n  audience?: string | string[];\n\n  /**\n   * Accepted values for the JWT header `typ` claim (case-insensitive).\n   * Tokens whose `typ` is not in this list are rejected.\n   * Set to `[]` to disable token-type checking entirely.\n   * @default [\"JWT\", \"at+JWT\"]\n   */\n  allowedTokenTypes?: string[];\n\n  /**\n   * Timeout in milliseconds for JWKS endpoint requests.\n   * Requests that do not complete within this window are aborted and the\n   * token is rejected, treating the request as unauthenticated.\n   * @default 5000\n   */\n  jwksTimeoutMs?: number;\n\n  /**\n   * Timeout in milliseconds for upstream proxy requests.\n   * Requests that do not complete within this window are aborted with a\n   * network error. Defaults to 5 000 ms.\n   * @default 5000\n   */\n  proxyTimeoutMs?: number;\n\n  /**\n   * Timeout in milliseconds for opaque (non-JWT) session token validation\n   * via the backend's `GET /sessions/me` endpoint. A token the backend does\n   * not answer for within this window is not refused: the request counts as\n   * signed out, its session cookie is kept, and a protected route answers\n   * `503` (see {@link protectedRoutes}).\n   * @default 5000\n   */\n  opaqueTokenTimeoutMs?: number;\n};\n\n// ─── Opaque session tokens ───────────────────────────────────────────────────\n\n/**\n * What the backend reports for an opaque (non-JWT) session token.\n *\n * `userId` is `null` when the session names no user of this app's project.\n * That is an anonymous session — `CreateSession` sets the session cookie\n * before any user factor is verified, so an abandoned or in-progress sign-in\n * holds a live session that names no user — or a live session of another\n * project. Such a session is **not** signed in, but it is not dead either, so\n * callers keep its cookie instead of clearing it: an anonymous sign-in may\n * still complete, and the other project's session belongs to the app that\n * signed it in.\n */\nexport interface OpaqueSession {\n  readonly userId: string | null;\n  readonly identifier: string | null;\n  readonly identifierProperty: string | null;\n  readonly display: string | null;\n  /** When the session expires, as the backend reports it, or `null`. */\n  readonly expiresAt: string | null;\n}\n\n/**\n * The error code the backend answers a session read with when the session\n * belongs to another project than the one the read names.\n */\nexport const SESSION_PROJECT_MISMATCH = \"sess.project_mismatch\";\n\n/** A live session that names no user of this app's project. */\nfunction noUser(): OpaqueSession {\n  return {\n    userId: null,\n    identifier: null,\n    identifierProperty: null,\n    display: null,\n    expiresAt: null,\n  };\n}\n\n/**\n * What a `GET /sessions/me` answer says about this app's project. A session of\n * another project names no user here: a backend that predates `project_id`\n * answers for any project, so the project the answer names is checked too.\n */\nfunction sessionAnswer(body: unknown, projectId: string): OpaqueSession {\n  const answer = (typeof body === \"object\" && body !== null ? body : {}) as {\n    project_id?: unknown;\n    user_id?: unknown;\n    expires_at?: unknown;\n    user?: { identifier?: unknown; identifier_property?: unknown; display?: unknown };\n  };\n  if (answer.project_id !== projectId) {\n    return noUser();\n  }\n  const text = (value: unknown): string | null =>\n    typeof value === \"string\" && value !== \"\" ? value : null;\n  return {\n    userId: text(answer.user_id),\n    identifier: text(answer.user?.identifier),\n    identifierProperty: text(answer.user?.identifier_property),\n    display: text(answer.user?.display),\n    expiresAt: text(answer.expires_at),\n  };\n}\n\n/**\n * The backend's verdict on an opaque session token, from\n * {@link checkOpaqueSessionToken}.\n *\n * - `live`: the backend holds a live session for the token. Its `userId` is\n *   `null` when the session names no user of this app's project: an anonymous\n *   one, or one of another project (see {@link OpaqueSession}).\n * - `refused`: the backend judged the token and refused it (`401`), or found\n *   the session it names gone (`404` with the `sess.not_found` code). Only\n *   this verdict makes the cookie dead.\n * - `undecided`: the backend gave no verdict: a `5xx` or any other answer, no\n *   answer in time, a network error, or a body that cannot be read. The\n *   backend answers `500`, never `401`, when it cannot decide (its token or\n *   key store failed), so that an outage does not sign its users out. The\n *   request counts as signed out, but the cookie is kept.\n */\nexport type OpaqueSessionCheck =\n  | { readonly verdict: \"live\"; readonly session: OpaqueSession }\n  | { readonly verdict: \"refused\" }\n  | { readonly verdict: \"undecided\" };\n\n/** The code the backend answers `GET /sessions/me` with when the session is gone. */\nconst SESSION_NOT_FOUND = \"sess.not_found\";\n\n/**\n * Asks the backend (`GET {issuerUrl}/sessions/me?project_id={projectId}`)\n * whether an opaque session token names a live session in this app's project,\n * telling a refusal apart from no verdict.\n *\n * A live session of another project comes back with no user, like an\n * anonymous one: it signs no one in here, and its cookie is not dead. The\n * backend refuses such a session itself (`403 sess.project_mismatch`); the\n * project its answer names is checked as well, because a backend that predates\n * the parameter answers for any project.\n *\n * A `404` is a refusal only with the `sess.not_found` code. Without it, the\n * answer came from something in front of the backend (a proxy, a `url` with\n * the wrong path), which judged nothing.\n *\n * When there is no verdict, it logs why: the request it was asked for then\n * renders signed out, or answers `503` on a protected route, and nothing else\n * would say what went wrong.\n *\n * @param token     - The opaque session token from the app's session cookie.\n * @param issuerUrl - Base URL of the auth backend.\n * @param timeoutMs - How long to wait for the backend before giving up.\n * @param projectId - The project this app signs its users in to. Without one,\n *   no session can be shown to belong here: the backend is not asked, and the\n *   session reads as live with no user, so its cookie is kept.\n * @returns The backend's {@link OpaqueSessionCheck | verdict}.\n */\nexport async function checkOpaqueSessionToken(\n  token: string,\n  issuerUrl: string,\n  timeoutMs: number,\n  projectId: string,\n): Promise<OpaqueSessionCheck> {\n  if (!projectId) {\n    return { verdict: \"live\", session: noUser() };\n  }\n  const key = await otherProjectKey(token, projectId);\n  if (otherProjectSessions.knows(key)) {\n    return { verdict: \"live\", session: noUser() };\n  }\n  const endpoint = joinURL(issuerUrl, \"/sessions/me\");\n  try {\n    const res = await fetch(`${endpoint}?project_id=${encodeURIComponent(projectId)}`, {\n      method: \"GET\",\n      headers: { cookie: `${SERVER_SESSION_COOKIE}=${token}` },\n      signal: AbortSignal.timeout(timeoutMs),\n    });\n    if (res.status === 401) {\n      return { verdict: \"refused\" };\n    }\n    if (res.status === 404 && (await errorCode(res)) === SESSION_NOT_FOUND) {\n      return { verdict: \"refused\" };\n    }\n    if (res.status === 403 && (await errorCode(res)) === SESSION_PROJECT_MISMATCH) {\n      otherProjectSessions.remember(key);\n      return { verdict: \"live\", session: noUser() };\n    }\n    if (!res.ok) {\n      return undecided(endpoint, `HTTP ${res.status}`);\n    }\n    const body: unknown = await res.json();\n    if ((body as { project_id?: unknown } | null)?.project_id !== projectId) {\n      otherProjectSessions.remember(key);\n    }\n    return { verdict: \"live\", session: sessionAnswer(body, projectId) };\n  } catch (err) {\n    return undecided(endpoint, failureCause(err, timeoutMs));\n  }\n}\n\n/**\n * The opaque session tokens the backend placed in another project, so that a\n * request signed out here does not ask again about the session another app,\n * or on `localhost` the Console, keeps under the server's cookie: a session\n * never changes its project. Each is known for a while, and only so many.\n */\nconst otherProjectSessions = (() => {\n  const TTL_MS = 5 * 60_000;\n  const MAX_ENTRIES = 1024;\n  const known = new Map<string, number>();\n  return {\n    knows(key: string): boolean {\n      const until = known.get(key);\n      if (until === undefined) {\n        return false;\n      }\n      if (until > Date.now()) {\n        return true;\n      }\n      known.delete(key);\n      return false;\n    },\n    remember(key: string): void {\n      known.delete(key);\n      known.set(key, Date.now() + TTL_MS);\n      if (known.size > MAX_ENTRIES) {\n        const oldest = known.keys().next().value;\n        if (oldest !== undefined) {\n          known.delete(oldest);\n        }\n      }\n    },\n    clear(): void {\n      known.clear();\n    },\n  };\n})();\n\n/** The memo key of a token in a project: a hash, so that the memo holds no session token. */\nasync function otherProjectKey(token: string, projectId: string): Promise<string> {\n  const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(token));\n  let hex = \"\";\n  for (const byte of new Uint8Array(digest)) {\n    hex += byte.toString(16).padStart(2, \"0\");\n  }\n  return `${projectId}:${hex}`;\n}\n\n/**\n * Forgets which session tokens the backend placed in another project\n * ({@link checkOpaqueSessionToken} remembers them for a while). For tests that\n * answer for the same token differently.\n */\nexport function clearSessionCheckCache(): void {\n  otherProjectSessions.clear();\n}\n\n/**\n * Validates an opaque session token by asking the backend\n * (`GET {issuerUrl}/sessions/me?project_id={projectId}`) whether the session is\n * live in this app's project.\n *\n * `null` folds a refusal and no verdict together, so it says whether the\n * request is signed in, not whether the cookie is dead: clearing the cookie on\n * `null` signs users out whenever the backend fails. To decide that, use\n * {@link checkOpaqueSessionToken}.\n *\n * @param token     - The opaque session token from the app's session cookie.\n * @param issuerUrl - Base URL of the auth backend.\n * @param timeoutMs - How long to wait for the backend before giving up.\n * @param projectId - The project this app signs its users in to.\n * @returns The live session — with a `null` `userId` when it names no user of\n *   this project — or `null` when the backend did not confirm one: it refused\n *   the token, or gave no verdict.\n */\nexport async function validateOpaqueSessionToken(\n  token: string,\n  issuerUrl: string,\n  timeoutMs: number,\n  projectId: string,\n): Promise<OpaqueSession | null> {\n  const check = await checkOpaqueSessionToken(token, issuerUrl, timeoutMs, projectId);\n  return check.verdict === \"live\" ? check.session : null;\n}\n\n/** The `code` of an error envelope, or `undefined` when the body is not one. */\nasync function errorCode(res: Response): Promise<string | undefined> {\n  try {\n    const body = (await res.json()) as { code?: unknown };\n    return typeof body.code === \"string\" ? body.code : undefined;\n  } catch {\n    return undefined;\n  }\n}\n\n/** Why a session check failed, for the log. */\nfunction failureCause(err: unknown, timeoutMs: number): string {\n  if ((err as { name?: unknown } | null)?.name === \"TimeoutError\") {\n    return `no answer within ${timeoutMs} ms`;\n  }\n  // A failed fetch says only \"fetch failed\"; what failed is in its cause.\n  const cause = (err as { cause?: unknown } | null)?.cause;\n  return cause instanceof Error ? `${String(err)}: ${cause.message}` : String(err);\n}\n\n/** Logs why the backend gave no verdict on a session cookie, and returns that. */\nfunction undecided(endpoint: string, cause: string): OpaqueSessionCheck {\n  console.warn(\n    `[nextgen] Could not check the session cookie against ${endpoint} (${cause}). ` +\n      \"That is not a refusal: the request counts as signed out, and the cookie is kept.\",\n  );\n  return { verdict: \"undecided\" };\n}\n\n/**\n * Whether a verified JWT names `projectId` in its `aud` claim, the one place a\n * self-contained token can say which project it was issued for.\n */\nexport function jwtNamesProject(payload: JwtPayload, projectId: string): boolean {\n  const audience = payload.aud === undefined ? [] : [payload.aud].flat();\n  return projectId !== \"\" && audience.includes(projectId);\n}\n\n// ─── Session resolution ──────────────────────────────────────────────────────\n\n/**\n * What a request carries that decides its session. The request URL and the\n * forwarded headers decide whether a cookie needs `Secure`.\n */\nexport interface SessionRequest extends BrowserRequest {\n  /**\n   * The `Authorization: Bearer` token, or `null`. It takes precedence: when it\n   * signs a user of this project in, the cookie is not consulted.\n   */\n  readonly bearer: string | null;\n  /** Reads one of the request's cookies by name. */\n  readonly cookie: (name: string) => string | null | undefined;\n}\n\n/** The settings {@link resolveSession} judges a request's session by. */\nexport interface ResolveSessionOptions {\n  readonly url: string;\n  /** The project this app signs its users in to; see {@link NextgenMiddlewareOptions.projectId}. */\n  readonly projectId: string | undefined;\n  readonly allowedAlgorithms: readonly string[] | undefined;\n  readonly clockSkewMs: number;\n  readonly audience: string | readonly string[] | undefined;\n  readonly allowedTokenTypes: readonly string[];\n  readonly jwksTimeoutMs: number | undefined;\n  readonly opaqueTokenTimeoutMs: number;\n  /**\n   * Whether an opaque session cookie is checked with the backend, a call to\n   * the server on every request. `false` leaves it to a render layer that\n   * checks the session itself, as sdk-next's `auth()` does: the result hands\n   * the token on as {@link ResolvedSession.unchecked}. A JWT is judged either\n   * way, without a call.\n   * @default true\n   */\n  readonly checkOpaqueSession?: boolean;\n}\n\n/** A request's session, as {@link resolveSession} judged it. */\nexport interface ResolvedSession {\n  readonly auth: AuthResult;\n  /**\n   * Whether the backend gave no verdict on the session cookie\n   * ({@link OpaqueSessionCheck}). The request counts as signed out and the\n   * cookie is kept; a protected route answers `503` rather than send a user\n   * who may well be signed in to the login page.\n   */\n  readonly undecided: boolean;\n  /**\n   * The session cookies the response sets, with {@link SESSION_COOKIE_CACHE_CONTROL}\n   * when there are any. A cookie the backend refused is cleared so the browser\n   * stops sending it; a session of this project found in the server's cookie is\n   * copied to the project's own, and the server's cookie keeps it, since the\n   * Console may share the project. No other cookie is\n   * touched: a session that is live but signs no one in here (an anonymous\n   * one, or one of another project) keeps its cookie, and so does one the\n   * backend gave no verdict on, and one behind a Bearer token that signed the\n   * request in, which was never judged.\n   */\n  readonly cookies: readonly SessionCookie[];\n  /**\n   * The opaque session token the backend was not asked about, when\n   * {@link ResolveSessionOptions.checkOpaqueSession} is `false`. The request\n   * counts as signed out here and its cookie is left as it is: whatever reads\n   * the session next checks the token itself.\n   */\n  readonly unchecked?: string;\n}\n\nfunction signedOut(): UnauthState {\n  return { isAuthenticated: false, session: null };\n}\n\nlet warnedMissingProject = false;\n\n/** Says once per process why no session counts: no project is configured. */\nfunction warnMissingProject(): void {\n  if (warnedMissingProject) {\n    return;\n  }\n  warnedMissingProject = true;\n  console.error(\n    \"[nextgen] No project id is configured, so no session counts as signed in. \" +\n      \"Set ZITADEL_PROJECT_ID (or the `projectId` option) to the project this app signs its users in to.\",\n  );\n}\n\n/**\n * The cookie a request's session is read from: the project's own\n * ({@link sessionCookieName}), else the server's `__nextgen_session`, where\n * this app kept its sessions before ADR 073 and where, on `localhost`, the\n * Console keeps its own.\n */\nfunction storedSession(\n  cookie: SessionRequest[\"cookie\"],\n  projectId: string,\n): { readonly name: string; readonly token: string } | null {\n  for (const name of [sessionCookieName(projectId), SERVER_SESSION_COOKIE]) {\n    const token = cookie(name);\n    if (token) {\n      return { name, token };\n    }\n  }\n  return null;\n}\n\n/**\n * What {@link judgeJwt} makes of a JWT: who it signs in, whether a cookie\n * holding it is dead, and how long it lives.\n */\ninterface JwtVerdict {\n  readonly auth: AuthResult;\n  /** Whether a cookie holding the JWT is dead, so the browser can drop it. */\n  readonly dead: boolean;\n  /** Seconds until a JWT that signs its user in expires, if it says. */\n  readonly maxAge: number | undefined;\n}\n\n/**\n * Judges a JWT. It signs its `sub` in when it verifies and its `aud` names\n * this project. Held in a cookie, it is dead when it fails verification or\n * names no user of this project; a verified JWT of another project is live\n * there, so its cookie stays.\n */\nasync function judgeJwt(\n  token: string,\n  projectId: string,\n  options: ResolveSessionOptions,\n): Promise<JwtVerdict> {\n  const payload = isJwtShaped(token)\n    ? await verifyJwt(token, {\n        issuerUrl: options.url,\n        allowedAlgorithms: options.allowedAlgorithms,\n        clockSkewMs: options.clockSkewMs,\n        audience: options.audience,\n        allowedTokenTypes: options.allowedTokenTypes,\n        jwksTimeoutMs: options.jwksTimeoutMs,\n      })\n    : null;\n  if (!payload) {\n    return { auth: signedOut(), dead: true, maxAge: undefined };\n  }\n  const namesProject = jwtNamesProject(payload, projectId);\n  if (!payload.sub || !namesProject) {\n    return { auth: signedOut(), dead: namesProject, maxAge: undefined };\n  }\n  return {\n    auth: {\n      isAuthenticated: true,\n      session: {\n        userId: payload.sub,\n        identifier: payload.email ?? null,\n        identifierProperty: null,\n        display: payload.name ?? null,\n        token,\n      },\n    },\n    dead: false,\n    maxAge: payload.exp === undefined ? undefined : secondsUntil(payload.exp * 1000),\n  };\n}\n\n/**\n * Judges which user, if any, a request is signed in as in this app's project.\n * The one implementation behind every server SDK's middleware, so they cannot\n * drift on who counts as signed in.\n *\n * - A Bearer token is judged first, as a JWT. When it signs a user of this\n *   project in, that is the answer and the cookie is not judged; otherwise the\n *   cookie speaks for itself.\n * - The session cookie is the project's own, else the server's. A session of\n *   this project found in the server's cookie is copied to the project's own.\n * - A JWT-shaped session cookie signs its `sub` in when it verifies and its\n *   `aud` names the project. It never reaches the backend, which does not\n *   re-check JWT claims.\n * - Any other session cookie is checked by the backend\n *   ({@link checkOpaqueSessionToken}) and signs its user in when the session\n *   is live in this project. With `checkOpaqueSession: false` it is not: the\n *   result hands it on unchecked, and touches no cookie.\n * - Only a cookie that held a dead session is cleared: one the backend refused,\n *   or a JWT that fails verification or names no user of this project. When\n *   the backend gives no verdict, the result says so\n *   ({@link ResolvedSession.undecided}) and the cookie stays.\n * - Without a project nothing signs in, and no cookie is touched.\n */\nexport async function resolveSession(\n  request: SessionRequest,\n  options: ResolveSessionOptions,\n): Promise<ResolvedSession> {\n  const { url, projectId, opaqueTokenTimeoutMs } = options;\n  if (!projectId) {\n    warnMissingProject();\n    return { auth: signedOut(), undecided: false, cookies: [] };\n  }\n\n  // A Bearer token and the cookie are separate credentials: one that is not\n  // this app's leaves the cookie to be judged on its own.\n  if (request.bearer !== null) {\n    const bearer = await judgeJwt(request.bearer, projectId, options);\n    if (bearer.auth.isAuthenticated) {\n      return { auth: bearer.auth, undecided: false, cookies: [] };\n    }\n  }\n\n  const stored = storedSession(request.cookie, projectId);\n  if (!stored) {\n    return { auth: signedOut(), undecided: false, cookies: [] };\n  }\n  const secure = secureFor(request);\n  // A session of this project in the server's cookie is copied to the\n  // project's own once it signs a user in; one already there stays as it is.\n  const moved = (maxAge: number | undefined): SessionCookie[] =>\n    stored.name === SERVER_SESSION_COOKIE\n      ? [copiedToProjectCookie(stored.token, projectId, maxAge, secure)]\n      : [];\n  const cleared = (): SessionCookie[] => [clearedCookie(stored.name, secure)];\n\n  if (isJwtShaped(stored.token)) {\n    const judged = await judgeJwt(stored.token, projectId, options);\n    if (judged.auth.isAuthenticated) {\n      return { auth: judged.auth, undecided: false, cookies: moved(judged.maxAge) };\n    }\n    return { auth: signedOut(), undecided: false, cookies: judged.dead ? cleared() : [] };\n  }\n\n  if (options.checkOpaqueSession === false) {\n    return { auth: signedOut(), undecided: false, cookies: [], unchecked: stored.token };\n  }\n\n  const check = await checkOpaqueSessionToken(stored.token, url, opaqueTokenTimeoutMs, projectId);\n  const userId = check.verdict === \"live\" ? check.session.userId : null;\n  if (check.verdict === \"live\" && userId) {\n    const { session } = check;\n    return {\n      auth: {\n        isAuthenticated: true,\n        session: {\n          userId,\n          identifier: session.identifier,\n          identifierProperty: session.identifierProperty,\n          display: session.display,\n          token: stored.token,\n        },\n      },\n      undecided: false,\n      cookies: moved(session.expiresAt ? secondsUntil(Date.parse(session.expiresAt)) : undefined),\n    };\n  }\n  // Only the backend's refusal makes the cookie dead: a live session that\n  // signs no one in here may still complete its sign-in or belongs to another\n  // app, and a check without a verdict refused nothing.\n  return {\n    auth: signedOut(),\n    undecided: check.verdict === \"undecided\",\n    cookies: check.verdict === \"refused\" ? cleared() : [],\n  };\n}\n"],"mappings":"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"}