{"version":3,"file":"index.cjs","names":["IAM_METHOD_ACTION_MAP","iamExtractEnvironment","iamDefaultCsrfCheck","iamRunAdminAuthz","iamWithAdminAudit"],"sources":["../../../src/server/express/index.ts"],"sourcesContent":["import type { IamEngine } from '../../core'\nimport type { AccessControl, IamRequest } from '../../core/types'\nimport {\n  IAM_METHOD_ACTION_MAP,\n  type IamAdminAudit,\n  iamDefaultCsrfCheck,\n  iamExtractEnvironment,\n  iamNoticeCsrfDefaultIfNeeded,\n  iamRunAdminAuthz,\n  iamWithAdminAudit,\n} from '../generic'\n\n/** Minimal IamExpress request shape. */\ninterface Req {\n  method?: string\n  path?: string\n  url?: string\n  ip?: string\n  params?: Record<string, string>\n  headers?: Record<string, string | string[] | undefined>\n  body?: unknown\n  user?: { id: string; [k: string]: unknown }\n  [k: string]: unknown\n}\n/** Minimal IamExpress response shape. */\ninterface Res {\n  status(code: number): Res\n  json(body: unknown): void\n}\n/** IamExpress next function. */\ntype Next = (err?: unknown) => void\n/** IamExpress middleware function. */\ntype Middleware = (req: Req, res: Res, next: Next) => void\n\n/** Minimal IamExpress Router interface for admin routes. */\ninterface ExpressRouterLike {\n  get(path: string, handler: (req: Req, res: Res) => void | Promise<void>): void\n  put(path: string, handler: (req: Req, res: Res) => void | Promise<void>): void\n  post(path: string, handler: (req: Req, res: Res) => void | Promise<void>): void\n  delete(path: string, handler: (req: Req, res: Res) => void | Promise<void>): void\n}\n\n/** IamExpress server integration types. Type-only namespace - zero bundle cost. */\nexport namespace IamExpress {\n  /**\n   * Describes options for {@link iamAccessMiddleware} and {@link iamGuard}.\n   *\n   * Every extractor has a sensible default; override only what your app needs.\n   *\n   * @template TScope - Constrains valid scope strings.\n   */\n  export interface IOptions<TScope extends string = string> {\n    /** Extracts the current user ID from the request. */\n    getUserId?: (req: Req) => string | null\n    /** Derives the target resource from the request. */\n    getResource?: (req: Req) => IamRequest.IResource\n    /** Derives the action being performed from the request. */\n    getAction?: (req: Req) => string\n    /** Extracts environment context (IP, user-agent, etc.) from the request. */\n    getEnvironment?: (req: Req) => IamRequest.IEnvironment\n    /** Determines the scope used for the access check. */\n    getScope?: (req: Req) => TScope | undefined\n    /** Handles a denied request (defaults to 403 JSON). */\n    onDenied?: (req: Req, res: Res) => void\n    /** Handles thrown errors during evaluation (defaults to 500 JSON). */\n    onError?: (err: Error, req: Req, res: Res, next: Next) => void\n  }\n\n  /**\n   * Required iamGuard callback for admin endpoints.\n   *\n   * Returning `false` (or throwing) blocks the mutation; returning `true` lets it\n   * proceed. The admin router writes policies, roles, and assignments directly\n   * to the adapter; mounting it without auth has historically been the most\n   * common foot-gun in authorization systems, so this hook is mandatory.\n   */\n  export type IAdminAuthorize = (req: Req) => boolean | Promise<boolean>\n\n  /** Describes options for {@link iamAdminRouter}. `authorize` is required. */\n  export interface IAdminRouterOptions extends IamAdminAudit.IOptions {\n    /** Required. Runs before every admin handler (read or write). */\n    authorize: IAdminAuthorize\n    /** Overrides the 401 unauthorized response. */\n    onUnauthorized?: (req: Req, res: Res) => void\n    /** Overrides the 500 internal error response. */\n    onError?: (err: Error, req: Req, res: Res) => void\n    /**\n     * Optional audit hook fired AFTER every mutation handler (PUT/POST/\n     * DELETE/PATCH) completes - success or failure. The hook is\n     * fire-and-forget: a slow or throwing implementation never blocks the\n     * request and can never alter the response. GET handlers do not fire it.\n     *\n     * See {@link IamAdminAudit.IOptions} for additional hardening knobs:\n     * `redactPath`, `onAuditHookError`, and `includeErrorMessage`.\n     */\n    onAdminMutation?: IamAdminAudit.Hook\n  }\n}\n\n/**\n * Builds global IamExpress middleware that runs `engine.can(...)` on every request.\n *\n * Replies 401 when no user is present and 403 when denied.\n *\n * @template TAction - Constrains valid action strings.\n * @template TResource - Constrains valid resource strings.\n * @template TRole - Constrains valid role strings.\n * @template TScope - Constrains valid scope strings.\n * @param engine - Provides the access engine to consult.\n * @param opts - Configures extractors and error hooks.\n * @returns An IamExpress middleware function.\n * @example\n * ```ts\n * app.use(iamAccessMiddleware(engine, {\n *   getUserId: (req) => req.user?.id ?? null,\n * }))\n * ```\n */\nexport function iamAccessMiddleware<\n  TAction extends string = string,\n  TResource extends string = string,\n  TRole extends string = string,\n  TScope extends string = string,\n>(engine: IamEngine<TAction, TResource, TRole, TScope>, opts: IamExpress.IOptions<TScope> = {}): Middleware {\n  const {\n    getUserId = (req) => req.user?.id ?? null,\n    getResource = (req) => {\n      const parts = (req.path ?? '/').split('/').filter(Boolean)\n      return { type: parts[0] ?? 'root', id: parts[1], attributes: {} }\n    },\n    getAction = (req) => IAM_METHOD_ACTION_MAP[req.method ?? 'GET'] ?? 'read',\n    getEnvironment = iamExtractEnvironment,\n    getScope,\n    onDenied = (_, res) => res.status(403).json({ error: 'Forbidden' }),\n    onError = (_err, _, res) => res.status(500).json({ error: 'Internal server error' }),\n  } = opts\n\n  return async (req, res, next) => {\n    const userId = getUserId(req)\n    if (!userId) {\n      res.status(401).json({ error: 'Unauthorized' })\n      return\n    }\n\n    try {\n      const allowed = await engine.can(\n        userId,\n        getAction(req) as TAction,\n        getResource(req) as IamRequest.IResource<TResource>,\n        getEnvironment(req),\n        getScope?.(req),\n      )\n      allowed ? next() : onDenied(req, res)\n    } catch (err) {\n      onError(err instanceof Error ? err : new Error(String(err)), req, res, next)\n    }\n  }\n}\n\n/**\n * Builds per-route middleware that checks `(action, resourceType)` for the\n * current user, pulling the resource ID from `req.params.id`.\n *\n * @template TAction - Constrains valid action strings.\n * @template TResource - Constrains valid resource strings.\n * @template TRole - Constrains valid role strings.\n * @template TScope - Constrains valid scope strings.\n * @param engine - Provides the access engine to consult.\n * @param action - Specifies the action being performed.\n * @param resourceType - Specifies the resource type required for the check.\n * @param opts - Configures optional extractors and `scope` override.\n * @returns An IamExpress middleware function.\n * @example\n * ```ts\n * app.delete('/posts/:id', iamGuard(engine, 'delete', 'post'), handler)\n * app.post('/admin/users', iamGuard(engine, 'manage', 'user', { scope: 'admin' }), handler)\n * ```\n */\nexport function iamGuard<\n  TAction extends string = string,\n  TResource extends string = string,\n  TRole extends string = string,\n  TScope extends string = string,\n>(\n  engine: IamEngine<TAction, TResource, TRole, TScope>,\n  action: TAction,\n  resourceType: TResource,\n  opts: Pick<IamExpress.IOptions<TScope>, 'getUserId' | 'getEnvironment' | 'onDenied'> & { scope?: TScope } = {},\n): Middleware {\n  const {\n    getUserId = (req) => req.user?.id ?? null,\n    getEnvironment = iamExtractEnvironment,\n    onDenied = (_, res) => res.status(403).json({ error: 'Forbidden' }),\n    scope,\n  } = opts\n\n  return async (req, res, next) => {\n    const userId = getUserId(req)\n    if (!userId) {\n      res.status(401).json({ error: 'Unauthorized' })\n      return\n    }\n\n    try {\n      const allowed = await engine.can(\n        userId,\n        action,\n        { type: resourceType, id: req.params?.id, attributes: {} },\n        getEnvironment(req),\n        scope,\n      )\n      allowed ? next() : onDenied(req, res)\n    } catch (err) {\n      next(err)\n    }\n  }\n}\n\n/**\n * Builds an IamExpress router for the duck-iam admin API.\n *\n * Returns a factory that accepts the IamExpress `Router` constructor so we never\n * import express at runtime. Throws when `opts.authorize` is missing.\n *\n * @template TAction - Constrains valid action strings.\n * @template TResource - Constrains valid resource strings.\n * @template TRole - Constrains valid role strings.\n * @template TScope - Constrains valid scope strings.\n * @param engine - Provides the access engine whose `admin` operations are exposed.\n * @param opts - Must include `authorize`; mounting unauthenticated is rejected.\n * @returns A factory `(Router) => router` that wires admin endpoints.\n * @throws Error when `opts.authorize` is not a function.\n * @example\n * ```ts\n * import { Router } from 'express'\n * app.use('/api/access-admin', iamAdminRouter(engine, {\n *   authorize: (req) => req.user?.role === 'admin',\n *   onAdminMutation: (e) => auditLog.write(e),\n * })(Router))\n * ```\n * @example\n * Rate limiting is out of scope; compose at the mount point with the\n * caller's library of choice - `express-rate-limit` is the canonical pick:\n * ```ts\n * import rateLimit from 'express-rate-limit'\n * const adminLimiter = rateLimit({ windowMs: 60_000, max: 30 })\n * app.use('/api/access-admin', adminLimiter, iamAdminRouter(engine, { authorize })(Router))\n * ```\n */\nexport function iamAdminRouter<\n  TAction extends string = string,\n  TResource extends string = string,\n  TRole extends string = string,\n  TScope extends string = string,\n>(\n  engine: IamEngine<TAction, TResource, TRole, TScope>,\n  opts: IamExpress.IAdminRouterOptions,\n): (Router: () => ExpressRouterLike) => ExpressRouterLike {\n  if (!opts || typeof opts.authorize !== 'function') {\n    throw new Error(\n      '[@gentleduck/iam] iamAdminRouter requires an `authorize` callback. Mounting admin endpoints unauthenticated is never safe.',\n    )\n  }\n  const { authorize, onAdminMutation, redactPath, onAuditHookError, includeErrorMessage, csrfCheck } = opts\n  const onUnauthorized = opts.onUnauthorized ?? ((_, res) => res.status(401).json({ error: 'Unauthorized' }))\n  const onError = opts.onError ?? ((_, __, res) => res.status(500).json({ error: 'Internal server error' }))\n  const onForbidden = (res: Res) => res.status(403).json({ error: 'Forbidden (CSRF check failed)' })\n  // Default to the built-in Sec-Fetch-Site check; pass `false` to disable.\n  const effectiveCsrfCheck = csrfCheck === false ? null : (csrfCheck ?? iamDefaultCsrfCheck)\n  iamNoticeCsrfDefaultIfNeeded(csrfCheck !== undefined)\n\n  /** Read gate: no audit emission. */\n  const gate = (handler: (req: Req, res: Res) => Promise<void>) => async (req: Req, res: Res) => {\n    try {\n      if (!(await authorize(req))) {\n        onUnauthorized(req, res)\n        return\n      }\n      await handler(req, res)\n    } catch (err) {\n      onError(err instanceof Error ? err : new Error(String(err)), req, res)\n    }\n  }\n\n  /**\n   * Mutation gate: identical to {@link gate} but emits an `onAdminMutation`\n   * event after the handler resolves or rejects. Uses try/finally so the\n   * hook fires even when the handler throws.\n   */\n  const mutate =\n    (\n      action: IamAdminAudit.Action,\n      target: IamAdminAudit.Target,\n      getTargetId: ((req: Req) => string | undefined) | undefined,\n      handler: (req: Req, res: Res) => Promise<void>,\n    ) =>\n    async (req: Req, res: Res) => {\n      // Shared CSRF + authorize phase.\n      const authz = await iamRunAdminAuthz(req, effectiveCsrfCheck, authorize)\n      if (authz.phase === 'forbidden') return onForbidden(res)\n      if (authz.phase === 'unauthorized') return onUnauthorized(req, res)\n      if (authz.phase === 'error') return onError(authz.error, req, res)\n      try {\n        await iamWithAdminAudit(\n          {\n            actor: authz.actor,\n            action,\n            target,\n            targetId: getTargetId?.(req),\n            method: req.method ?? '',\n            path: req.path ?? req.url ?? '',\n            onAdminMutation,\n            redactPath,\n            onAuditHookError,\n            includeErrorMessage,\n          },\n          () => handler(req, res),\n        )\n      } catch (err) {\n        onError(err instanceof Error ? err : new Error(String(err)), req, res)\n      }\n    }\n\n  return (Router: () => ExpressRouterLike) => {\n    const router = Router()\n\n    router.get(\n      '/policies',\n      gate(async (_, res) => {\n        res.json(await engine.admin.listPolicies())\n      }),\n    )\n\n    router.get(\n      '/roles',\n      gate(async (_, res) => {\n        res.json(await engine.admin.listRoles())\n      }),\n    )\n\n    router.put(\n      '/policies',\n      mutate(\n        'replace',\n        'policy',\n        (req) => (req.body as { id?: string } | undefined)?.id,\n        async (req, res) => {\n          await engine.admin.savePolicy(req.body as AccessControl.IPolicy<TAction, TResource, TRole>)\n          res.json({ ok: true })\n        },\n      ),\n    )\n\n    router.put(\n      '/roles',\n      mutate(\n        'replace',\n        'role',\n        (req) => (req.body as { id?: string } | undefined)?.id,\n        async (req, res) => {\n          await engine.admin.saveRole(req.body as AccessControl.IRole<TAction, TResource, TRole, TScope>)\n          res.json({ ok: true })\n        },\n      ),\n    )\n\n    router.post(\n      '/subjects/:id/roles',\n      mutate(\n        'create',\n        'role-assignment',\n        (req) => req.params?.id,\n        async (req, res) => {\n          const body = req.body as Record<string, unknown>\n          await engine.admin.assignRole(req.params?.id as string, body.roleId as TRole, body.scope as TScope)\n          res.json({ ok: true })\n        },\n      ),\n    )\n\n    router.delete(\n      '/subjects/:id/roles/:roleId',\n      mutate(\n        'delete',\n        'role-assignment',\n        (req) => req.params?.id,\n        async (req, res) => {\n          await engine.admin.revokeRole(req.params?.id as string, req.params?.roleId as TRole)\n          res.json({ ok: true })\n        },\n      ),\n    )\n\n    return router\n  }\n}\n"],"mappings":"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"}