{"version":3,"sources":["../src/paths.ts","../src/auth.ts","../src/server.ts","../src/express.ts"],"names":["createRemoteJWKSet","createLocalJWKSet","jwtVerify"],"mappings":"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","file":"express.cjs","sourcesContent":["/**\n * The single source of truth for Hanzo IAM OIDC endpoint paths.\n *\n * Hanzo IAM is a Casdoor-derived OIDC provider served per-brand from a\n * configurable origin (`serverUrl`): hanzo → https://iam.hanzo.ai,\n * lux → https://lux.id, zoo → https://zoo.id, bootnode → https://id.bootno.de.\n *\n * These are the ONLY paths. There is no legacy `/oauth/*` and no\n * `/api/login/*`. Every module in this SDK references `OIDC_PATHS` —\n * no path string is written anywhere else.\n *\n * CRITICAL GOTCHA: IAM serves a 200 `text/html` SPA catch-all for ANY\n * unregistered path. A client that hits a wrong path therefore gets a\n * 200 HTML body, not a 404 — silent breakage. So clients MUST hit these\n * exact paths, and a discovery round-trip must never be allowed to\n * resolve to a different path. The hard-coded fallbacks here are these\n * same values, so a failed discovery degrades to correct paths.\n */\n\n/** OIDC endpoint paths, relative to the brand `serverUrl`. */\nexport const OIDC_PATHS = {\n  /** OIDC discovery document. */\n  discovery: \"/.well-known/openid-configuration\",\n  /** Authorization endpoint (RFC 6749 §3.1). */\n  authorize: \"/v1/iam/oauth/authorize\",\n  /** Token endpoint (RFC 6749 §3.2). */\n  token: \"/v1/iam/oauth/token\",\n  /** UserInfo endpoint (OIDC Core §5.3). */\n  userinfo: \"/v1/iam/oauth/userinfo\",\n  /** JWKS endpoint (RFC 7517). */\n  jwks: \"/v1/iam/.well-known/jwks\",\n  /** RP-initiated logout endpoint (OIDC RP-Initiated Logout). */\n  logout: \"/v1/iam/oauth/logout\",\n} as const;\n\nexport type OidcPathKey = keyof typeof OIDC_PATHS;\n\n/**\n * Hanzo-IAM application paths that are NOT part of the OIDC spec — the\n * auth-method discovery endpoint and the onboarding state machine the\n * embedded views drive. Mounted under the same `/v1/iam` prefix.\n */\nexport const IAM_PATHS = {\n  /** Live list of enabled auth methods for the embedded login views. */\n  authMethods: \"/v1/iam/auth/methods\",\n  /** Onboarding state-machine base (steps append `/identity`, etc.). */\n  onboarding: \"/v1/iam/onboarding\",\n  /**\n   * Casdoor credential-login endpoint the embedded `<Login>` views POST to.\n   * `type=code` (a client `redirectUri` is present) mints an authorization\n   * code returned in `data`; `type=login` establishes the session cookie.\n   * This is what the deployed IAM actually authenticates against — the OIDC\n   * token endpoint's password/OTP grants are NOT enabled per-client.\n   */\n  login: \"/v1/iam/login\",\n  /** Send an email/SMS verification code (passwordless login). */\n  sendCode: \"/v1/iam/send-verification-code\",\n  /** Account registration. */\n  signup: \"/v1/iam/signup\",\n} as const;\n\nexport type IamPathKey = keyof typeof IAM_PATHS;\n\n/**\n * The canonical `serverUrl` origin for each Hanzo IAM brand. White-label\n * is host-based: one IAM deployment serves every brand and selects the\n * tenant by the origin it is reached on. This is the SINGLE place the\n * brand→origin mapping lives — adapters take a `brand` and resolve here\n * rather than each app hard-coding a hostname.\n */\nexport const BRAND_SERVER_URLS = {\n  hanzo: \"https://iam.hanzo.ai\",\n  lux: \"https://lux.id\",\n  zoo: \"https://zoo.id\",\n  bootnode: \"https://id.bootno.de\",\n  pars: \"https://pars.id\",\n} as const;\n\n/** A known Hanzo IAM brand key. */\nexport type IamBrand = keyof typeof BRAND_SERVER_URLS;\n\n/**\n * Resolve a brand to its canonical IAM `serverUrl`.\n *\n * @example\n * serverUrlForBrand(\"lux\") // → \"https://lux.id\"\n */\nexport function serverUrlForBrand(brand: IamBrand): string {\n  return BRAND_SERVER_URLS[brand];\n}\n\n/** Strip trailing slashes from a server origin so paths concat cleanly. */\nexport function trimServerUrl(serverUrl: string): string {\n  return serverUrl.replace(/\\/+$/, \"\");\n}\n\n/**\n * Build an absolute IAM endpoint URL from a server origin and a path key.\n *\n * @example\n * iamUrl(\"https://iam.hanzo.ai\", \"token\") // → \"https://iam.hanzo.ai/v1/iam/oauth/token\"\n */\nexport function iamUrl(serverUrl: string, key: OidcPathKey): string {\n  return `${trimServerUrl(serverUrl)}${OIDC_PATHS[key]}`;\n}\n","/**\n * JWT validation using jose library + OIDC JWKS discovery.\n *\n * Validates access/ID tokens issued by Hanzo IAM. The JWKS URI is taken\n * from the live OIDC discovery document (which points at the canonical\n * `/v1/iam/.well-known/jwks` endpoint); the issuer is verified.\n *\n * Audience is verified against `config.clientId` by default. A token\n * whose `aud` does not match is REJECTED — unless the caller explicitly\n * opts out with `allowMissingAudience: true` (for IAM deployments that\n * issue access tokens without an `aud` claim). There is no silent\n * fall-through.\n */\n\nimport {\n  createLocalJWKSet,\n  createRemoteJWKSet,\n  jwtVerify,\n  type JSONWebKeySet,\n  type JWTPayload,\n} from \"jose\";\nimport type { Config, AuthResult, JwtClaims } from \"./types.js\";\nimport { OIDC_PATHS, trimServerUrl } from \"./paths.js\";\n\n// ---------------------------------------------------------------------------\n// JWKS key set cache (per issuer)\n// ---------------------------------------------------------------------------\n\nconst jwksSets = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\nfunction getJwksKeySet(jwksUri: string): ReturnType<typeof createRemoteJWKSet> {\n  let keySet = jwksSets.get(jwksUri);\n  if (!keySet) {\n    keySet = createRemoteJWKSet(new URL(jwksUri));\n    jwksSets.set(jwksUri, keySet);\n  }\n  return keySet;\n}\n\n// Fallback local key sets, deduped by kid. A spec-compliant JWKS has one key\n// per `kid` (RFC 7517 §4.5), but some issuers publish the SAME key twice under\n// one kid (e.g. an identical cert collected from a global + an org-scoped\n// owner). jose's remote set then can't pick one and throws\n// `JWKSMultipleMatchingKeys`, which would hard-fail verification of EVERY token\n// signed with that kid. We dedupe by kid and verify against a local set so a\n// malformed-but-harmless JWKS never breaks login. Cached per jwksUri.\nconst localJwksSets = new Map<string, ReturnType<typeof createLocalJWKSet>>();\n\nasync function getDedupedLocalKeySet(\n  jwksUri: string,\n): Promise<ReturnType<typeof createLocalJWKSet>> {\n  const cached = localJwksSets.get(jwksUri);\n  if (cached) return cached;\n\n  const res = await fetch(jwksUri, { headers: { Accept: \"application/json\" } });\n  if (!res.ok) throw new Error(`JWKS fetch failed: ${res.status}`);\n  const jwks = (await res.json()) as JSONWebKeySet;\n\n  const seen = new Set<string>();\n  const keys = (jwks.keys ?? []).filter((k) => {\n    const id = `${k.kid ?? \"\"}:${k.alg ?? \"\"}`;\n    if (seen.has(id)) return false;\n    seen.add(id);\n    return true;\n  });\n\n  const keySet = createLocalJWKSet({ keys });\n  localJwksSets.set(jwksUri, keySet);\n  return keySet;\n}\n\n/** Clear cached JWKS key sets (useful for testing or key rotation). */\nexport function clearJwksCache(): void {\n  jwksSets.clear();\n  localJwksSets.clear();\n}\n\n// ---------------------------------------------------------------------------\n// OIDC discovery cache (lightweight, no full client needed)\n// ---------------------------------------------------------------------------\n\ntype CachedDiscovery = { jwksUri: string; issuer: string; fetchedAt: number };\nconst discoveryCache = new Map<string, CachedDiscovery>();\nconst DISCOVERY_TTL_MS = 5 * 60 * 1000;\n\nasync function resolveJwksUri(serverUrl: string): Promise<{ jwksUri: string; issuer: string }> {\n  const baseUrl = trimServerUrl(serverUrl);\n  const cached = discoveryCache.get(baseUrl);\n  if (cached && Date.now() - cached.fetchedAt < DISCOVERY_TTL_MS) {\n    return { jwksUri: cached.jwksUri, issuer: cached.issuer };\n  }\n\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), 8_000);\n  try {\n    const res = await fetch(`${baseUrl}${OIDC_PATHS.discovery}`, {\n      signal: controller.signal,\n      headers: { Accept: \"application/json\" },\n    });\n    if (!res.ok) {\n      throw new Error(`OIDC discovery failed: ${res.status}`);\n    }\n    const body = (await res.json()) as { jwks_uri?: string; issuer?: string };\n    const jwksUri = body.jwks_uri;\n    const issuer = body.issuer ?? baseUrl;\n    if (!jwksUri) {\n      throw new Error(\"OIDC discovery response missing jwks_uri\");\n    }\n    discoveryCache.set(baseUrl, { jwksUri, issuer, fetchedAt: Date.now() });\n    return { jwksUri, issuer };\n  } finally {\n    clearTimeout(timer);\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Token validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a JWT access token against IAM's JWKS.\n *\n * Uses OIDC discovery to find the JWKS URI, then verifies the token\n * signature, issuer, audience, and expiry using the `jose` library.\n *\n * The audience MUST equal `config.clientId`. A mismatch fails with\n * `iam_audience_invalid` unless `config.allowMissingAudience === true`,\n * in which case the audience check is skipped entirely (for IAM\n * deployments that issue access tokens without an `aud` claim). There is\n * no automatic, silent retry.\n */\nexport async function validateToken(\n  token: string,\n  config: Config,\n): Promise<AuthResult> {\n  if (!token || typeof token !== \"string\") {\n    return { ok: false, reason: \"iam_token_missing\" };\n  }\n\n  let jwksUri: string;\n  let issuer: string;\n  if (config.jwksUri && config.issuer) {\n    // Explicit overrides — bypass OIDC discovery entirely. This is the\n    // in-cluster / split-horizon path: the pod can't reach the public IAM\n    // origin (LB hairpin / WAF 403) and discovery would advertise an\n    // unreachable public jwks_uri + a mismatched issuer, so the caller pins\n    // a reachable in-cluster JWKS endpoint and the real public issuer.\n    jwksUri = config.jwksUri;\n    issuer = config.issuer;\n  } else {\n    try {\n      const discovery = await resolveJwksUri(config.serverUrl);\n      jwksUri = discovery.jwksUri;\n      issuer = discovery.issuer;\n    } catch {\n      return { ok: false, reason: \"iam_discovery_failed\" };\n    }\n  }\n\n  const keySet = getJwksKeySet(jwksUri);\n  const verifyOptions = {\n    issuer,\n    // Skip aud verification only when explicitly allowed; otherwise\n    // pin to the client id. No silent fall-through on mismatch.\n    ...(config.allowMissingAudience ? {} : { audience: config.clientId }),\n    clockTolerance: 30, // 30s clock skew\n  };\n\n  let payload: JWTPayload;\n  try {\n    let result;\n    try {\n      result = await jwtVerify(token, keySet, verifyOptions);\n    } catch (err) {\n      // A JWKS that lists the token's kid more than once makes jose's remote\n      // set ambiguous (`JWKSMultipleMatchingKeys`). Retry ONCE against a local\n      // set deduped by kid — the only key for that kid then verifies cleanly.\n      // Any other error propagates to the mapping below unchanged.\n      if ((err as { code?: string }).code === \"ERR_JWKS_MULTIPLE_MATCHING_KEYS\") {\n        const localSet = await getDedupedLocalKeySet(jwksUri);\n        result = await jwtVerify(token, localSet, verifyOptions);\n      } else {\n        throw err;\n      }\n    }\n    payload = result.payload;\n  } catch (err) {\n    // Branch on jose's typed error codes, not message strings.\n    // jose v6: ERR_JWT_EXPIRED for `exp`; ERR_JWT_CLAIM_VALIDATION_FAILED\n    // with `claim === \"aud\"` for an audience mismatch.\n    const code = (err as { code?: string }).code;\n    const claim = (err as { claim?: string }).claim;\n    if (code === \"ERR_JWT_EXPIRED\") {\n      return { ok: false, reason: \"iam_token_expired\" };\n    }\n    if (code === \"ERR_JWT_CLAIM_VALIDATION_FAILED\" && claim === \"aud\") {\n      return { ok: false, reason: \"iam_audience_invalid\" };\n    }\n    return { ok: false, reason: \"iam_signature_invalid\" };\n  }\n\n  const claims = payload as unknown as JwtClaims;\n\n  // Hanzo IAM tokens may use owner/name instead of sub claim\n  const sub =\n    claims.sub ||\n    (typeof claims.owner === \"string\" && typeof claims.name === \"string\"\n      ? `${claims.owner}/${claims.name}`\n      : undefined);\n\n  if (!sub) {\n    return { ok: false, reason: \"iam_subject_missing\" };\n  }\n\n  // IAM sub format is \"org/username\" - extract owner\n  const parts = sub.split(\"/\");\n  const owner = parts.length > 1 ? parts[0] : config.orgName ?? \"unknown\";\n\n  return {\n    ok: true,\n    userId: sub,\n    email: typeof claims.email === \"string\" ? claims.email : undefined,\n    name:\n      typeof claims.name === \"string\"\n        ? claims.name\n        : typeof claims.preferred_username === \"string\"\n          ? claims.preferred_username\n          : undefined,\n    avatar: typeof claims.picture === \"string\" ? claims.picture : undefined,\n    owner,\n    claims,\n  };\n}\n","/**\n * Framework-agnostic server-side session helper for Hanzo IAM.\n *\n * One way to answer \"who is calling?\" on the server: read the bearer\n * token (`Authorization: Bearer …`) or the session cookie, verify it\n * against IAM's JWKS via {@link validateToken}, and return the identity.\n *\n * Works with any request that exposes headers — the Web Fetch `Request`\n * (Next.js App Router route handlers / middleware, Hono, Remix) and the\n * Node `IncomingMessage` (Next.js Pages API routes, Express, raw http).\n *\n * @example Next.js App Router route handler\n * ```ts\n * import { getServerSession } from \"@hanzo/iam/server\";\n *\n * const iam = { serverUrl: process.env.IAM_ENDPOINT!, clientId: process.env.IAM_CLIENT_ID! };\n *\n * export async function GET(req: Request) {\n *   const session = await getServerSession(req, iam);\n *   if (!session) return new Response(\"unauthorized\", { status: 401 });\n *   // session.userId, session.owner, session.email, session.claims\n *   return Response.json({ user: session.userId, org: session.owner });\n * }\n * ```\n *\n * @example Guarded route handler\n * ```ts\n * import { withSession } from \"@hanzo/iam/server\";\n *\n * export const GET = withSession(iam, (req, session) =>\n *   Response.json({ org: session.owner }),\n * );\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { Config, JwtClaims } from \"./types.js\";\nimport { validateToken } from \"./auth.js\";\n\n/** Canonical cookie name carrying the IAM access token for SSR. */\nexport const SESSION_COOKIE = \"hanzo_iam_access_token\";\n\n/** The authenticated identity resolved from a verified IAM token. */\nexport interface ServerSession {\n  /** Subject — IAM user id, format \"org/username\". */\n  userId: string;\n  /** Owning organization (the `owner` claim or the `sub` org prefix). */\n  owner: string;\n  /** User email, when present in the token. */\n  email?: string;\n  /** All verified JWT claims. */\n  claims: JwtClaims;\n}\n\n/** Options for session resolution. */\nexport interface ServerSessionOptions {\n  /** Cookie name to read the token from. Default {@link SESSION_COOKIE}. */\n  cookieName?: string;\n}\n\n/**\n * Minimal request shape: either a Web `Request` (with `headers.get`) or a\n * Node `IncomingMessage` (with a `headers` record). Both are accepted.\n */\nexport interface HeaderCarrier {\n  headers:\n    | { get(name: string): string | null }\n    | Record<string, string | string[] | undefined>;\n}\n\nfunction readHeader(req: HeaderCarrier, name: string): string | undefined {\n  const headers = req.headers;\n  if (typeof (headers as { get?: unknown }).get === \"function\") {\n    return (headers as { get(n: string): string | null }).get(name) ?? undefined;\n  }\n  const value = (headers as Record<string, string | string[] | undefined>)[\n    name.toLowerCase()\n  ];\n  if (Array.isArray(value)) return value[0];\n  return value;\n}\n\nfunction parseCookie(header: string | undefined, name: string): string | undefined {\n  if (!header) return undefined;\n  for (const part of header.split(\";\")) {\n    const eq = part.indexOf(\"=\");\n    if (eq === -1) continue;\n    if (part.slice(0, eq).trim() === name) {\n      return decodeURIComponent(part.slice(eq + 1).trim());\n    }\n  }\n  return undefined;\n}\n\n/**\n * Extract the IAM access token from a request: the `Authorization:\n * Bearer …` header takes precedence, falling back to the session cookie.\n * Returns undefined when neither is present.\n */\nexport function getBearerToken(\n  req: HeaderCarrier,\n  options?: ServerSessionOptions,\n): string | undefined {\n  const auth = readHeader(req, \"authorization\");\n  if (auth) {\n    const match = /^Bearer\\s+(.+)$/i.exec(auth.trim());\n    if (match) return match[1];\n  }\n  return parseCookie(readHeader(req, \"cookie\"), options?.cookieName ?? SESSION_COOKIE);\n}\n\n/**\n * Resolve the server session for a request. Reads the bearer token or\n * session cookie, verifies it against IAM, and returns the identity —\n * or `null` when no valid token is present.\n */\nexport async function getServerSession(\n  req: HeaderCarrier,\n  config: Config,\n  options?: ServerSessionOptions,\n): Promise<ServerSession | null> {\n  const token = getBearerToken(req, options);\n  if (!token) return null;\n\n  const result = await validateToken(token, config);\n  if (!result.ok) return null;\n\n  return {\n    userId: result.userId,\n    owner: result.owner,\n    email: result.email,\n    claims: result.claims,\n  };\n}\n\n/**\n * Wrap a route handler so it only runs with a valid session. Unauthorized\n * requests get a `401` (Web `Response`) before the handler is called.\n * The handler receives the original request plus the resolved session.\n *\n * Framework-agnostic over the Web Fetch `Request`/`Response` model used\n * by Next.js App Router, Hono, and Remix.\n */\nexport function withSession<Req extends HeaderCarrier, Res>(\n  config: Config,\n  handler: (req: Req, session: ServerSession) => Res | Promise<Res>,\n  options?: ServerSessionOptions,\n): (req: Req) => Promise<Res | Response> {\n  return async (req: Req): Promise<Res | Response> => {\n    const session = await getServerSession(req, config, options);\n    if (!session) {\n      return new Response(\"unauthorized\", { status: 401 });\n    }\n    return handler(req, session);\n  };\n}\n","/**\n * Express / Connect middleware for Hanzo IAM.\n *\n * Two functions, both pointed at the canonical IAM endpoints via the\n * framework-agnostic `@hanzo/iam/server` verifier:\n *\n *  - {@link requireAuth} — middleware that 401s requests without a valid\n *    IAM token and otherwise attaches the session to `req.iamSession`.\n *  - {@link getSession} — resolve the session for a request without\n *    blocking (returns `null` when absent), for optional-auth routes.\n *\n * Express keeps its NATIVE routing; this is just a guard. Works with any\n * Connect-style framework whose `req.headers` is a Node headers record\n * (Express, Connect, raw `http`). For Hono use `@hanzo/iam/hono`.\n *\n * @example\n * ```ts\n * import express from \"express\";\n * import { requireAuth, getIamSession } from \"@hanzo/iam/express\";\n *\n * const iam = { serverUrl: process.env.IAM_SERVER_URL!, clientId: process.env.IAM_CLIENT_ID! };\n * const app = express();\n *\n * app.get(\"/me\", requireAuth(iam), (req, res) => {\n *   const session = getIamSession(req);          // typed, always present here\n *   res.json({ user: session.userId, org: session.owner });\n * });\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { Config } from \"./types.js\";\nimport {\n  getServerSession,\n  type ServerSession,\n  type ServerSessionOptions,\n  type HeaderCarrier,\n} from \"./server.js\";\n\n/** The property `requireAuth` attaches the resolved session to on `req`. */\nexport const IAM_SESSION_PROP = \"iamSession\" as const;\n\n/** Minimal Express-style request: a Node headers record + our attached session. */\nexport interface IamRequest extends HeaderCarrier {\n  [IAM_SESSION_PROP]?: ServerSession;\n}\n\n/** Minimal Express-style response (only what the guard touches). */\nexport interface IamResponse {\n  status(code: number): IamResponse;\n  json(body: unknown): unknown;\n}\n\n/** Express `next` callback. */\nexport type NextFn = (err?: unknown) => void;\n\n/** Express-style middleware signature. */\nexport type IamMiddleware = (\n  req: IamRequest,\n  res: IamResponse,\n  next: NextFn,\n) => void;\n\n/**\n * Express/Connect middleware that requires a valid IAM session.\n *\n * Reads `Authorization: Bearer …` (then the session cookie), verifies it\n * against IAM's JWKS, attaches the result to `req.iamSession`, and calls\n * `next()`. On no/invalid token it responds `401` and does NOT call\n * `next()`.\n */\nexport function requireAuth(\n  config: Config,\n  options?: ServerSessionOptions,\n): IamMiddleware {\n  return (req, res, next) => {\n    getServerSession(req, config, options)\n      .then((session) => {\n        if (!session) {\n          res.status(401).json({ error: \"unauthorized\" });\n          return;\n        }\n        req[IAM_SESSION_PROP] = session;\n        next();\n      })\n      .catch(next);\n  };\n}\n\n/**\n * Resolve the IAM session for an Express request without blocking the\n * route (returns `null` when there is no valid token). Use for routes that\n * are public but personalize when signed in.\n */\nexport function getSession(\n  req: IamRequest,\n  config: Config,\n  options?: ServerSessionOptions,\n): Promise<ServerSession | null> {\n  return getServerSession(req, config, options);\n}\n\n/**\n * Read the session attached by {@link requireAuth} off a request. Throws if\n * called on a request that did not pass through `requireAuth` (programming\n * error). For optional auth, call {@link getSession} instead.\n */\nexport function getIamSession(req: IamRequest): ServerSession {\n  const session = req[IAM_SESSION_PROP];\n  if (!session) {\n    throw new Error(\n      \"@hanzo/iam/express: no session on request — guard the route with requireAuth() first, or use getSession() for optional auth.\",\n    );\n  }\n  return session;\n}\n"]}