@forinda/kickjs 10.0.0 → 10.1.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.
Files changed (126) hide show
  1. package/dist/{adapter-CvEm8Gpp.d.mts → adapter-ClEh-Oos.d.mts} +2 -2
  2. package/dist/{adapter-CvEm8Gpp.d.mts.map → adapter-ClEh-Oos.d.mts.map} +1 -1
  3. package/dist/{application-D21ZVAWG.mjs → application-Dv514EeR.mjs} +3 -3
  4. package/dist/{application-D21ZVAWG.mjs.map → application-Dv514EeR.mjs.map} +1 -1
  5. package/dist/{application-1Hln746x.d.mts → application-oIPzgjfk.d.mts} +20 -9
  6. package/dist/application-oIPzgjfk.d.mts.map +1 -0
  7. package/dist/bootstrap-BdyBlBMe.mjs +12 -0
  8. package/dist/bootstrap-BdyBlBMe.mjs.map +1 -0
  9. package/dist/{client-ip-BkLX8wXf.mjs → client-ip-BbLDp03S.mjs} +2 -2
  10. package/dist/{client-ip-BkLX8wXf.mjs.map → client-ip-BbLDp03S.mjs.map} +1 -1
  11. package/dist/config/index.mjs +2 -2
  12. package/dist/{config-rXsEm1rn.mjs → config-CJiGeOca.mjs} +3 -3
  13. package/dist/{config-rXsEm1rn.mjs.map → config-CJiGeOca.mjs.map} +1 -1
  14. package/dist/{container-CsCdem73.mjs → container-CS9v9UFU.mjs} +3 -3
  15. package/dist/{container-CsCdem73.mjs.map → container-CS9v9UFU.mjs.map} +1 -1
  16. package/dist/{context-Bd02q5Sm.mjs → context-CtL9RxXp.mjs} +3 -3
  17. package/dist/{context-Bd02q5Sm.mjs.map → context-CtL9RxXp.mjs.map} +1 -1
  18. package/dist/{context-errors-BUdMBVWO.mjs → context-errors-7C5TBYn7.mjs} +2 -2
  19. package/dist/{context-errors-BUdMBVWO.mjs.map → context-errors-7C5TBYn7.mjs.map} +1 -1
  20. package/dist/{contributor-runner-CZTAFvla.mjs → contributor-runner-DBpRR5dl.mjs} +3 -3
  21. package/dist/{contributor-runner-CZTAFvla.mjs.map → contributor-runner-DBpRR5dl.mjs.map} +1 -1
  22. package/dist/core/adapter.d.mts +1 -1
  23. package/dist/core/adapter.mjs +1 -1
  24. package/dist/core/app-module.mjs +1 -1
  25. package/dist/core/cache.mjs +1 -1
  26. package/dist/core/container.mjs +2 -2
  27. package/dist/core/cron.mjs +2 -2
  28. package/dist/core/decorators.mjs +2 -2
  29. package/dist/core/errors.mjs +1 -1
  30. package/dist/core/interfaces.mjs +1 -1
  31. package/dist/core/logger.mjs +1 -1
  32. package/dist/core/path.mjs +1 -1
  33. package/dist/core/plugin.d.mts +1 -1
  34. package/dist/core/plugin.mjs +1 -1
  35. package/dist/core/reactivity.mjs +1 -1
  36. package/dist/{core-B0mErNE1.mjs → core-B0JmdBvX.mjs} +3 -3
  37. package/dist/{core-B0mErNE1.mjs.map → core-B0JmdBvX.mjs.map} +1 -1
  38. package/dist/{csrf-BnetlmWY.mjs → csrf--1t4qDov.mjs} +3 -3
  39. package/dist/{csrf-BnetlmWY.mjs.map → csrf--1t4qDov.mjs.map} +1 -1
  40. package/dist/{decorate-B7-z930n.mjs → decorate-BHkdkV4l.mjs} +1 -1
  41. package/dist/{define-adapter-B4D_Rci1.mjs → define-adapter-BLKlUbSL.mjs} +2 -2
  42. package/dist/{define-adapter-B4D_Rci1.mjs.map → define-adapter-BLKlUbSL.mjs.map} +1 -1
  43. package/dist/{disposables-CgQqGmPL.mjs → disposables-CuY2S3bw.mjs} +2 -2
  44. package/dist/{disposables-CgQqGmPL.mjs.map → disposables-CuY2S3bw.mjs.map} +1 -1
  45. package/dist/{env-ngWmBB9d.mjs → env-BwzF9MkZ.mjs} +3 -3
  46. package/dist/{env-ngWmBB9d.mjs.map → env-BwzF9MkZ.mjs.map} +1 -1
  47. package/dist/{error-handler-Chw07m-M.d.mts → error-handler-BB9wAElm.d.mts} +2 -2
  48. package/dist/{error-handler-Chw07m-M.d.mts.map → error-handler-BB9wAElm.d.mts.map} +1 -1
  49. package/dist/{handler-CyRoAqNt.mjs → handler-zKfIgNo4.mjs} +3 -3
  50. package/dist/{handler-CyRoAqNt.mjs.map → handler-zKfIgNo4.mjs.map} +1 -1
  51. package/dist/http/application.d.mts +1 -1
  52. package/dist/http/application.mjs +2 -2
  53. package/dist/http/bootstrap.d.mts +1 -1
  54. package/dist/http/bootstrap.mjs +2 -2
  55. package/dist/http/context.d.mts +1 -1
  56. package/dist/http/context.mjs +2 -2
  57. package/dist/http/middleware/cors.mjs +1 -1
  58. package/dist/http/middleware/csrf.mjs +2 -2
  59. package/dist/http/middleware/error-handler.d.mts +1 -1
  60. package/dist/http/middleware/error-handler.mjs +2 -2
  61. package/dist/http/middleware/helmet.mjs +1 -1
  62. package/dist/http/middleware/rate-limit.mjs +2 -2
  63. package/dist/http/middleware/request-id.mjs +1 -1
  64. package/dist/http/middleware/request-logger.mjs +2 -2
  65. package/dist/http/middleware/session.d.mts +1 -1
  66. package/dist/http/middleware/session.mjs +2 -2
  67. package/dist/http/middleware/session.mjs.map +1 -1
  68. package/dist/http/middleware/spa.d.mts +1 -1
  69. package/dist/http/middleware/spa.mjs +2 -2
  70. package/dist/http/middleware/upload.d.mts +1 -1
  71. package/dist/http/middleware/upload.mjs +2 -2
  72. package/dist/http/middleware/validate.mjs +1 -1
  73. package/dist/http/middleware/views.d.mts +1 -1
  74. package/dist/http/middleware/views.mjs +2 -2
  75. package/dist/http/query/index.mjs +2 -2
  76. package/dist/http/router-builder.d.mts +1 -1
  77. package/dist/http/router-builder.mjs +2 -2
  78. package/dist/http/runtimes/fastify.d.mts +1 -1
  79. package/dist/http/runtimes/fastify.mjs +2 -2
  80. package/dist/http/runtimes/h3-web.mjs +1 -1
  81. package/dist/http/runtimes/h3.d.mts +1 -1
  82. package/dist/http/runtimes/h3.d.mts.map +1 -1
  83. package/dist/http/runtimes/h3.mjs +2 -2
  84. package/dist/http/runtimes/h3.mjs.map +1 -1
  85. package/dist/index.d.mts +7 -7
  86. package/dist/index.mjs +2 -2
  87. package/dist/index.mjs.map +1 -1
  88. package/dist/{jobs-Ccs6PkmC.mjs → jobs-BQ4t3TAb.mjs} +3 -3
  89. package/dist/{jobs-Ccs6PkmC.mjs.map → jobs-BQ4t3TAb.mjs.map} +1 -1
  90. package/dist/{kv-stores-BYeO77zK.d.mts → kv-stores-Co5k2PYA.d.mts} +3 -3
  91. package/dist/{kv-stores-BYeO77zK.d.mts.map → kv-stores-Co5k2PYA.d.mts.map} +1 -1
  92. package/dist/{kv-stores-9IkW7Rjj.mjs → kv-stores-DsfVy2fk.mjs} +3 -3
  93. package/dist/{kv-stores-9IkW7Rjj.mjs.map → kv-stores-DsfVy2fk.mjs.map} +1 -1
  94. package/dist/{metadata-DCWKG472.mjs → metadata-zvd6AWkE.mjs} +2 -2
  95. package/dist/{metadata-DCWKG472.mjs.map → metadata-zvd6AWkE.mjs.map} +1 -1
  96. package/dist/{module-registry-Cl12vSJd.mjs → module-registry-Daf_P2oD.mjs} +2 -2
  97. package/dist/{module-registry-Cl12vSJd.mjs.map → module-registry-Daf_P2oD.mjs.map} +1 -1
  98. package/dist/{observers-9FRE8VCB.mjs → observers-DZgs48UM.mjs} +2 -2
  99. package/dist/{observers-9FRE8VCB.mjs.map → observers-DZgs48UM.mjs.map} +1 -1
  100. package/dist/{query-DUkboS0j.mjs → query-B_k9qn0R.mjs} +2 -2
  101. package/dist/{query-DUkboS0j.mjs.map → query-B_k9qn0R.mjs.map} +1 -1
  102. package/dist/{reply-DElkObMy.mjs → reply-BDK9xt7Z.mjs} +3 -3
  103. package/dist/{reply-DElkObMy.mjs.map → reply-BDK9xt7Z.mjs.map} +1 -1
  104. package/dist/{respond-Dcj1NF_X.mjs → respond-DFRj2-7q.mjs} +3 -3
  105. package/dist/{respond-Dcj1NF_X.mjs.map → respond-DFRj2-7q.mjs.map} +1 -1
  106. package/dist/{route-flag-jd8PDm20.mjs → route-flag-BWuvJsGZ.mjs} +3 -3
  107. package/dist/{route-flag-jd8PDm20.mjs.map → route-flag-BWuvJsGZ.mjs.map} +1 -1
  108. package/dist/{route-policy-DlunIjGc.mjs → route-policy-B3vM6bSf.mjs} +2 -2
  109. package/dist/{route-policy-DlunIjGc.mjs.map → route-policy-B3vM6bSf.mjs.map} +1 -1
  110. package/dist/{router-builder-DZsCjpeV.mjs → router-builder-JvMTp50k.mjs} +3 -3
  111. package/dist/{router-builder-DZsCjpeV.mjs.map → router-builder-JvMTp50k.mjs.map} +1 -1
  112. package/dist/{router-builder-D_SkWTfE.d.mts → router-builder-zWuVIOmd.d.mts} +2 -2
  113. package/dist/{router-builder-D_SkWTfE.d.mts.map → router-builder-zWuVIOmd.d.mts.map} +1 -1
  114. package/dist/{session-4Tb6q0Ny.d.mts → session-B4hOZj90.d.mts} +13 -4
  115. package/dist/session-B4hOZj90.d.mts.map +1 -0
  116. package/dist/{upload-DL_jrBGk.d.mts → upload-CiMtdXWD.d.mts} +2 -2
  117. package/dist/{upload-DL_jrBGk.d.mts.map → upload-CiMtdXWD.d.mts.map} +1 -1
  118. package/dist/{upload-config-BiL1QXkT.mjs → upload-config-Dz2INCaa.mjs} +2 -2
  119. package/dist/{upload-config-BiL1QXkT.mjs.map → upload-config-Dz2INCaa.mjs.map} +1 -1
  120. package/dist/web.d.mts +2 -2
  121. package/dist/web.mjs +2 -2
  122. package/package.json +1 -1
  123. package/dist/application-1Hln746x.d.mts.map +0 -1
  124. package/dist/bootstrap-2hi7Zhs8.mjs +0 -12
  125. package/dist/bootstrap-2hi7Zhs8.mjs.map +0 -1
  126. package/dist/session-4Tb6q0Ny.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"context-Bd02q5Sm.mjs","names":[],"sources":["../src/http/request-store.ts","../src/http/background.ts","../src/http/runtime.ts","../src/http/context.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport type { ContextMetaKey, MetaValue } from '../core/execution-context'\n\n/** Per-request storage for REQUEST-scoped DI and request context propagation */\nexport interface RequestStore {\n /** Unique request identifier */\n requestId: string\n /**\n * Per-request singleton cache for REQUEST-scoped services. Keys are\n * heterogeneous (class constructors, `InjectionToken` objects, raw\n * strings), so the map type stays `Map<any, any>`.\n */\n instances: Map<any, any>\n /**\n * Per-request key/value bag. Canonical backing store for\n * `RequestContext.set/get` and the Context Contributor pipeline (#107)\n * since Phase 3. Anything written here is visible to every concrete\n * execution context constructed for the same request, regardless of\n * which middleware/contributor wrote it.\n *\n * Keys are constrained to `string` per the locked decision in\n * `architecture.md` §20.12 (#4); the value type is `unknown` because\n * the concrete shape at each key is determined by the consumer's\n * `ContextMeta` augmentation, not by the store itself.\n *\n * Prefer the {@link getRequestValue} helper over reaching into this\n * map directly — it threads the augmented `ContextMeta` shape through\n * `MetaValue<K>` so consumers get typed reads without an `as` cast.\n * Writes should flow through `ctx.set(key, value)` or a Context\n * Contributor's return value — see the `setRequestValue` note below\n * for why this package intentionally does NOT ship a service-level\n * write helper.\n */\n values: Map<string, unknown>\n /** Set once {@link disposeRequestStore} has run — guards double-dispose. */\n disposed?: boolean\n}\n\n/** AsyncLocalStorage instance shared across the framework */\nexport const requestStore = new AsyncLocalStorage<RequestStore>()\n\n/**\n * Get the current request store. Throws if called outside a request context.\n * Used by the Container to resolve REQUEST-scoped dependencies.\n */\nexport function getRequestStore(): RequestStore {\n const store = requestStore.getStore()\n if (!store) {\n throw new Error(\n 'No active request context. REQUEST-scoped services can only be resolved during an HTTP request.',\n )\n }\n return store\n}\n\n/**\n * Read a per-request `ContextMeta`-keyed value from outside a controller\n * (services, repositories, anywhere with no `ctx` reference). Returns\n * the augmented type for the key — `MetaValue<K>` — so consumers get the\n * same static safety as `ctx.get(key)` without holding the context.\n *\n * Returns `undefined` outside an active request frame (background jobs,\n * startup code, tests without `requestScopeMiddleware()`) — the helper\n * is intentionally null-tolerant so calling it from a service that\n * runs in both request and non-request paths doesn't throw.\n *\n * @example\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface ContextMeta { tenant: { id: string; name: string } }\n * }\n *\n * @Service()\n * class AuditService {\n * log(action: string) {\n * const tenant = getRequestValue('tenant') // typed: { id; name } | undefined\n * db.audit.insert({ action, tenantId: tenant?.id })\n * }\n * }\n * ```\n */\nexport function getRequestValue<K extends ContextMetaKey>(key: K): MetaValue<K> | undefined {\n return requestStore.getStore()?.values.get(key) as MetaValue<K> | undefined\n}\n\n// Intentionally NO `setRequestValue` export.\n//\n// Writes are deliberately context-scoped — only the controlled write\n// surfaces should mutate the per-request Map:\n//\n// 1. A Context Contributor's `resolve()` / `onError()` return value\n// (the runner does `ctx.set(reg.key, value)` on the contributor's\n// behalf). This is the canonical way to populate a key.\n// 2. A controller / middleware / contributor that holds a\n// `RequestContext` instance can call `ctx.set(key, value)`\n// directly. The write is auditable inline at the call site.\n//\n// Letting arbitrary services reach in and mutate the store from\n// anywhere produces \"spooky action at a distance\" — keys appear in\n// the request bag without an obvious source, and tracing which\n// service polluted what becomes a grep exercise. Services that need\n// to publish per-request state should return the value to the caller\n// (controller or contributor) and let *that* layer write it via\n// `ctx.set`. If a future need genuinely requires a service-level\n// write surface, expose a narrow function (`recordTrace`,\n// `markStartTime`) on a service that captures the side effect, not a\n// generic `setRequestValue`.\n","/**\n * Work that outlives the response — `waitUntil(promise)`.\n *\n * A handler that fires off an audit log, an email, or an analytics call and\n * answers without awaiting it leaves that promise unowned: on serverless the\n * platform freezes or kills the instance once the response is sent, and on a\n * Node server `SIGTERM` exits before it settles. `waitUntil` registers the\n * promise so something waits for it:\n *\n * - on the Node server, `shutdown()` awaits it (within `shutdownTimeout`);\n * - through `createHandler()` and the web entry, it is handed to the\n * platform's own `waitUntil` (Workers `ctx`, Netlify `context`, Vercel's\n * `waitUntil`) when you pass it in.\n *\n * Process-wide, like the DI container: one app per process.\n *\n * @module @forinda/kickjs/http/background\n */\nimport { createLogger } from '../core/logger'\nimport { reportError } from '../core/observers'\nimport { requestStore } from './request-store'\n\nconst log = createLogger('waitUntil')\nconst pending = new Set<Promise<void>>()\n\n/**\n * Keep `promise` alive past the response. A rejection is logged (with the\n * request id when called during a request), never left unhandled.\n *\n * @example\n * ```ts\n * @Post('/')\n * async create(ctx: RequestContext) {\n * const order = await this.orders.create(ctx.body)\n * ctx.waitUntil(this.mailer.sendReceipt(order)) // or waitUntil(...) outside a handler\n * return order\n * }\n * ```\n */\nexport function waitUntil(promise: Promise<unknown>): void {\n const requestId = requestStore.getStore()?.requestId\n const tracked: Promise<void> = Promise.resolve(promise).then(\n () => {},\n (err: unknown) => {\n log.error({ err, requestId }, 'Background work passed to waitUntil() failed')\n reportError(err, { source: 'background', requestId })\n },\n )\n pending.add(tracked)\n void tracked.finally(() => pending.delete(tracked))\n}\n\n/**\n * Resolve once every registered promise has settled — including work\n * registered while waiting. Never rejects.\n */\nexport async function settleBackgroundWork(): Promise<void> {\n while (pending.size > 0) await Promise.all(pending)\n}\n\n/** How many `waitUntil` promises have not settled yet. */\nexport function pendingBackgroundWork(): number {\n return pending.size\n}\n\n/**\n * The platform hook `createHandler()` and the web entry forward background\n * work to. Cloudflare Workers' `ctx`, Netlify's `context`, and `{ waitUntil }`\n * from `@vercel/functions` all have this shape.\n */\nexport interface PlatformContext {\n waitUntil?(promise: Promise<unknown>): void\n}\n\n/** Hand every pending (and later-registered) promise to the platform. */\nexport function forwardBackgroundWork(platform: PlatformContext | undefined): void {\n platform?.waitUntil?.(settleBackgroundWork())\n}\n","// The HttpRuntime seam.\n//\n// Decorators no longer emit an `express.Router` directly. `buildRouteTable()`\n// turns controller metadata into a plain-data `RouteEntry[]`; an `HttpRuntime`\n// materializes that table onto whatever engine it owns. Express is the default\n// runtime and the only one shipped in M1 — its materializer reproduces the exact\n// handler chain the old `buildRoutes()` built, so behavior is unchanged.\n//\n// Fastify / h3 runtimes (later milestones) consume the SAME `RouteEntry` data\n// and the SAME `HttpRuntime` contract; the request/response driver abstraction\n// that lets `RequestContext` run engine-agnostically lands with them (M3), since\n// under Express the drivers ARE the Express request/response objects already.\n//\n// Why a seam: running Fastify / h3 behind an Express-compat layer\n// (`@fastify/express`, h3's node bridge) bypasses their router and\n// serialization, and rebuilding `ctx` over web-standard Request/Response\n// would break SSE / streaming, multer uploads and `ctx.res` for every adopter.\n// The seam keeps `ctx` and swaps only the engine underneath it.\n\nimport { ROUTE_SLOT, type RouteFlags } from '../core/route-flag'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type {\n RequestHandler,\n ErrorRequestHandler,\n Express,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from 'express'\n\nimport type { Constructor, FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core'\nimport type { RequestContext } from './context'\n\n/** HTTP verbs the router emits. Mirrors the decorator surface (@Get/@Post/...). */\nexport type RouteMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n\n/**\n * A context-based handler: the runtime-neutral unit of the request pipeline.\n * Contributors, route middleware (via {@link MiddlewareHandler}), and the\n * terminal controller call all reduce to this shape. The runtime owns how a\n * `CtxHandler` is wrapped onto its engine (Express wraps it in `(req, res,\n * next)`; Fastify in its own handler).\n */\nexport type CtxHandler = (ctx: RequestContext) => unknown | Promise<unknown>\n\n/**\n * Connect-style middleware — the portable middleware format. Built-in\n * middleware and adopter-supplied Express middleware are this shape. The\n * Express runtime hands them straight to `app.use`; later runtimes bridge\n * them (Fastify via `@fastify/middie`, h3 via its node adapter).\n *\n * Typed as the Express handler union for M1 (Express is the only runtime);\n * a future milestone narrows this to bare node `(req, res, next)` primitives\n * once the driver layer lands.\n */\nexport type ConnectMiddleware = RequestHandler | ErrorRequestHandler\n\n/**\n * One materialized route, as plain data. Produced by {@link buildRouteTable}\n * from decorator metadata; consumed by {@link HttpRuntime.mountRoutes}.\n *\n * The shape is deliberately richer than a flat `CtxHandler[]`: `middlewares`\n * keep the `(ctx, next)` signature (they may short-circuit or call `next(err)`),\n * `contributorRunner` is a closure over the pre-built pipeline + container (the\n * pipeline is built — and any cycle / missing-dep error thrown — at table-build\n * time, i.e. boot), and `validation` / `upload` stay as metadata the runtime\n * materializes its own way. Faithfully reproducing the legacy Express chain —\n * not collapsing it — is what keeps M1 behavior-neutral.\n */\nexport interface RouteEntry {\n method: RouteMethod\n /** Path with `:param` segments — the portable subset all engines accept. */\n path: string\n /** Class + method middleware, in execution order; each receives `(ctx, next)`. */\n middlewares: MiddlewareHandler[]\n /**\n * Runs the contributor pipeline against `ctx` (populates `ctx.set(...)`),\n * or `null` when no contributors apply. Closes over the built pipeline and\n * the DI container so it stays engine-neutral; the runtime advances the\n * chain (Express: call `next()`) once it resolves.\n */\n contributorRunner: CtxHandler | null\n /**\n * Contributors registered with `beforeValidation: true`. Runtimes run this\n * right after publishing the matched route — before validation, upload and\n * middleware — so a rejection (401) lands before a malformed body's 422.\n * Absent or `null` when no contributor asks for it.\n */\n earlyContributorRunner?: CtxHandler | null\n /** Terminal handler: resolves the controller per-request and invokes it. */\n handler: CtxHandler\n /** Introspection + runtime-materialized concerns (swagger/typegen read this). */\n meta: RouteMeta\n}\n\n/** Per-route metadata: introspection refs plus runtime-materialized concerns. */\nexport interface RouteMeta {\n /**\n * Owning controller class — for DI resolution and adapter introspection.\n * Absent for ad-hoc routes registered through {@link AdapterHttp.route},\n * which have no controller class behind them.\n */\n controller?: Constructor\n /** Handler method name on the controller, when there is one. */\n handlerName?: string\n /** Zod/JSON-schema validation config from the route decorator, if any. */\n validation?: RouteDefinition['validation']\n /** `@FileUpload` config, if present — the runtime supplies the backend. */\n upload?: FileUploadConfig\n /**\n * Route flags in force here, resolved at build time (method over class).\n * Enabled flags only: a flag declared `false` at a higher-precedence site is\n * absent rather than present-and-false, so `has()` is always the right\n * question. See `core/route-flag.ts`.\n */\n flags?: RouteFlags\n /**\n * The full mounted pattern (`/api/v1/users/:id`) — set by the Application\n * for controller routes. `path` is relative to the module mount.\n */\n pattern?: string\n}\n\n/**\n * The route a request matched, published on `ctx.route`.\n *\n * Everything that holds a {@link RequestContext} — handlers, `@Middleware()`,\n * guards, contributors — can read it, which is what lets one declaration on a\n * route serve every consumer.\n */\n/**\n * Publish the matched route on the request, for `ctx.route` to read.\n *\n * Called by each runtime once per dispatch, before it builds a RequestContext.\n * It lives on `req` rather than on the ctx because the Express runtime\n * constructs a fresh RequestContext per middleware step — anything written to\n * one ctx would be invisible to the next and to the handler.\n */\nexport function publishMatchedRoute(req: unknown, entry: RouteEntry): void {\n ;(req as Record<symbol, MatchedRoute>)[ROUTE_SLOT] = {\n method: entry.method,\n path: entry.path,\n controller: entry.meta.controller,\n handlerName: entry.meta.handlerName,\n flags: entry.meta.flags ?? EMPTY_FLAGS,\n pattern: entry.meta.pattern ?? entry.path,\n }\n}\n\nconst EMPTY_FLAGS: RouteFlags = new Map() as RouteFlags\n\nexport interface MatchedRoute {\n method: RouteMethod\n /** Path as declared on the handler, e.g. `/:id`. */\n path: string\n /**\n * The full route pattern including the mount prefix and version, e.g.\n * `/api/v1/users/:id` — what metrics and spans should be keyed by. Falls\n * back to `path` for routes registered without a mount.\n */\n pattern: string\n controller?: Constructor\n handlerName?: string\n /** Enabled flags only — see {@link RouteMeta.flags}. */\n flags: RouteFlags\n}\n\n/** A controller's routes grouped under the module mount prefix. */\nexport type RouteTable = { mountPath: string; routes: RouteEntry[] }[]\n\n/**\n * The response surface {@link RequestContext}'s helpers (`json` / `html` /\n * `sse` / `download` / `render` / `problem`) drive, instead of calling Express\n * `res` methods directly (spec §4.3). Sized so `express.Response` satisfies it\n * structurally — under the Express runtime the driver IS the Express response,\n * so there is no wrapping and no behavior change. Other runtimes (Fastify / h3)\n * provide a thin object implementing this over their native reply.\n *\n * The terminal methods return `RuntimeResponse` for fluent chaining (Express's\n * `Response` is assignable, since it is a superset).\n */\n/** True for the TypeError `JSON.stringify` throws on a bigint. */\nexport function isBigIntJsonError(err: unknown): boolean {\n return err instanceof TypeError && /BigInt/.test(err.message)\n}\n\n/**\n * `JSON.stringify` that writes a bigint as its decimal string instead of\n * throwing — database counts and `bigint` columns reach responses as bigints.\n * Plain `JSON.stringify` runs first, so a body without one costs nothing extra.\n */\nexport function stringifyJson(data: unknown): string {\n try {\n return JSON.stringify(data)\n } catch (err) {\n if (!isBigIntJsonError(err)) throw err\n return JSON.stringify(data, (_key, value) =>\n typeof value === 'bigint' ? value.toString() : value,\n )\n }\n}\n\nexport interface RuntimeResponse {\n status(code: number): RuntimeResponse\n json(data: unknown): RuntimeResponse\n send(data: unknown): RuntimeResponse\n type(contentType: string): RuntimeResponse\n setHeader(name: string, value: string | number | readonly string[]): unknown\n render(view: string, data?: Record<string, unknown>): unknown\n writeHead(statusCode: number, headers?: Record<string, string>): unknown\n flushHeaders(): unknown\n write(chunk: string | Buffer): boolean\n end(data?: unknown): unknown\n once(event: 'close' | 'drain', listener: () => void): unknown\n readonly headersSent: boolean\n}\n\n// ── Runtime-typed escape hatches (spec §4.3b) ─────────────────────────────\n\n/**\n * Per-runtime type map: the engine-native request / response / app types an\n * `HttpRuntime` exposes through `ctx.req.raw` / `ctx.res.raw` / `AdapterContext.app`.\n */\nexport interface RuntimeTypeMap {\n request: unknown\n response: unknown\n app: unknown\n}\n\n/** The Express runtime's native types — the default when nothing is augmented. */\nexport interface ExpressRuntimeTypes {\n request: ExpressRequest\n response: ExpressResponse\n app: Express\n}\n\n/**\n * Augmentable runtime registry. Empty by default; the `kick/runtime` typegen\n * plugin emits a `{ runtime: <RuntimeTypeMap> }` augmentation from the configured\n * runtime, which flips the engine-native escape-hatch types — `AdapterContext.app`,\n * `getRuntimeApp()`, and (once the driver layer lands) `ctx.req.raw` / `ctx.res.raw`\n * — to that runtime. Mirrors `KickDbRegister` / `KickEnv`, so the type story is\n * uniform with `kick/db` and env typing.\n *\n * @example\n * ```ts\n * // .kickjs/types/kick__runtime.d.ts — generated by typegen\n * declare module '@forinda/kickjs' {\n * interface KickRuntimeRegister {\n * runtime: import('@forinda/kickjs/fastify').FastifyRuntimeTypes\n * }\n * }\n * ```\n */\n\nexport interface KickRuntimeRegister {}\n\n/** The active runtime's type map — the augmented one, or Express by default. */\nexport type ActiveRuntime = KickRuntimeRegister extends { runtime: infer R extends RuntimeTypeMap }\n ? R\n : ExpressRuntimeTypes\n\n/** Options passed to {@link HttpRuntime.createApp}. */\nexport interface RuntimeAppOptions {\n /**\n * Express `trust proxy` setting (and the equivalent on other engines). The\n * function form is Express-specific; other runtimes interpret the simpler\n * shapes (or normalize X-Forwarded-For themselves — see spec §10 Q1).\n */\n trustProxy?: boolean | string | number | string[] | ((ip: string, hopIndex: number) => boolean)\n /**\n * When the app is mounted inside another handler (`kick dev`'s Vite server,\n * `app.handle(req, res, next)` in a host server), hand requests no route\n * matched back to it — `true`, the default — or answer them here with the\n * app's not-found handling — `false`. Express always answers.\n */\n notFoundFallthrough?: boolean\n}\n\n/** Where a connect middleware sits and what it scopes to. */\nexport interface UseConnectOptions {\n path?: string | RegExp | ReadonlyArray<string | RegExp>\n}\n\n/**\n * Optional engine capabilities. Absence means the corresponding `ctx` feature\n * raises a clear error on that runtime rather than failing obscurely.\n */\nexport interface RuntimeCapabilities {\n /** View-engine rendering (`ctx.render`). Express: true; Fastify/h3: false initially. */\n render: boolean\n /** File uploads (`ctx.file` / `@FileUpload`). All engines, different backends. */\n uploads: boolean\n /** Connect-style middleware. Express/Fastify(middie): true; h3: best-effort. */\n connectMiddleware: boolean\n /**\n * The engine parses request bodies itself (Fastify, h3). When true, the\n * Application skips its default `express.json()` so the body stream isn't\n * consumed twice (which would deadlock the engine's own parser). Express:\n * false — it relies on the `express.json()` connect middleware.\n */\n nativeBodyParsing: boolean\n /**\n * The engine's `nodeHandler` works behind `node:http2` (the HTTP/2\n * compatibility API), so `server: { http2: true }` may use it. Fastify/h3:\n * true. Express: false — it does not run on that layer. Absent = false, so\n * custom runtimes opt in explicitly.\n */\n http2?: boolean\n}\n\n/**\n * The engine driver. KickJS owns decorators → {@link RouteTable}, the\n * contributor pipeline, the `RequestContext` surface, and error mapping. The\n * runtime owns app/server creation, route materialization, connect-middleware\n * registration, static serving, and the terminal not-found / error handlers.\n *\n * `expressRuntime()` is the default and the M1 reference implementation.\n */\nexport interface HttpRuntime<TApp = unknown> {\n readonly name: 'express' | 'fastify' | 'h3' | (string & {})\n\n /** Create the engine app. Called once per Application (and per HMR rebuild). */\n createApp(options?: RuntimeAppOptions): TApp\n\n /**\n * Node-compatible request listener — the transport contract.\n * `http.createServer(handler)` in prod, Vite post-middleware in dev. MUST\n * invoke `next` (when given) instead of 404-ing if no route matched — the\n * Vite dev chain depends on fall-through.\n */\n nodeHandler(\n app: TApp,\n ): (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void\n\n /** Materialize the framework-built route table onto the engine. */\n mountRoutes(app: TApp, table: RouteTable): void\n\n /** Register a connect-style middleware (built-ins + adopter middleware). */\n useConnect(app: TApp, mw: ConnectMiddleware, opts?: UseConnectOptions): void\n\n /** Serve a static directory (swagger-ui assets, devtools SPA). */\n serveStatic(app: TApp, prefix: string, dir: string): void\n\n /** Terminal not-found handler. */\n setNotFound(app: TApp, mw: ConnectMiddleware): void\n\n /** Terminal error handler. */\n setErrorHandler(app: TApp, mw: ConnectMiddleware): void\n\n readonly capabilities: RuntimeCapabilities\n}\n\n/**\n * The supported surface adapters use to add HTTP routes, mounts, static dirs,\n * and middleware — engine-agnostic, in place of reaching for the raw Express\n * `app` (spec §4.4). Exposed as {@link AdapterContext.http}. Built by the\n * Application over the active {@link HttpRuntime}, so an adapter written against\n * this works under any runtime; the raw `app` escape hatch stays available for\n * the rare engine-specific need.\n */\nexport interface AdapterHttp {\n /** Register a single context-handler route (e.g. a docs or transport endpoint). */\n route(method: RouteMethod, path: string, handler: CtxHandler): void\n /** Mount a pre-built route table under a path prefix. */\n mount(prefix: string, routes: RouteEntry[]): void\n /** Serve a static directory under a path prefix. */\n serveStatic(prefix: string, dir: string): void\n /** Register a connect-style middleware (optionally path-scoped). */\n use(mw: ConnectMiddleware, opts?: UseConnectOptions): void\n}\n","/// <reference types=\"multer\" />\nimport type { Request, NextFunction } from 'express'\nimport { ROUTE_SLOT } from '../core/route-flag'\nimport {\n isBigIntJsonError,\n stringifyJson,\n type ActiveRuntime,\n type MatchedRoute,\n type RuntimeResponse,\n} from './runtime'\nimport {\n type ContextMetaKey,\n type ExecutionContext,\n type MetaValue,\n} from '../core/execution-context'\nimport { normalizeProblem, type ProblemDetails, type ValidationError } from '../core/errors'\nimport { MissingContextValueError } from '../core/context-errors'\nimport { requestStore } from './request-store'\nimport { waitUntil } from './background'\nimport { resolveClientIp, type ClientRequestLike } from './client-ip'\nimport type { Session } from './middleware/session'\nimport {\n parseQuery,\n type ParseQueryOptions,\n type QueryFieldConfig,\n type PaginatedResponse,\n type TypedParsedQuery,\n} from './query'\n\n/**\n * Input shape for the typed problem-detail convenience methods on\n * {@link ProblemSender} (`ctx.problem.notFound`, `.badRequest`, etc.) —\n * RFC 9457 details minus the `status`, which each method pre-fills.\n */\ntype ProblemInput = Omit<ProblemDetails, 'status'>\n\n/**\n * Callable response helper for RFC 9457 — Problem Details responses.\n * Returned by `ctx.problem`. Invoke directly for a fully-specified\n * payload, or use the named convenience methods to pre-fill `status` +\n * `title` for the common codes.\n *\n * Each call sets `Content-Type: application/problem+json` and serializes\n * the canonical shape (defaulting `type` to `about:blank` and `title` to\n * the IANA reason phrase per RFC 9457 §3.1.1 / §3.1.4).\n *\n * @example\n * ```ts\n * ctx.problem({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * detail: 'Your balance is 30, but that costs 50.',\n * balance: 30, // extension per §3.2\n * })\n *\n * ctx.problem.notFound({ detail: 'User abc not found' })\n * ctx.problem.validation(zodResult.error.issues)\n * ```\n */\ninterface ProblemSender {\n (input: ProblemDetails): RuntimeResponse\n /** 400 Bad Request — RFC 9457. */\n badRequest(input?: ProblemInput): RuntimeResponse\n /** 401 Unauthorized — RFC 9457. */\n unauthorized(input?: ProblemInput): RuntimeResponse\n /** 403 Forbidden — RFC 9457. */\n forbidden(input?: ProblemInput): RuntimeResponse\n /** 404 Not Found — RFC 9457. */\n notFound(input?: ProblemInput): RuntimeResponse\n /** 409 Conflict — RFC 9457. */\n conflict(input?: ProblemInput): RuntimeResponse\n /** 422 Unprocessable Entity — RFC 9457. */\n unprocessable(input?: ProblemInput): RuntimeResponse\n /** 429 Too Many Requests — RFC 9457. */\n tooManyRequests(input?: ProblemInput): RuntimeResponse\n /** 500 Internal Server Error — RFC 9457. */\n internal(input?: ProblemInput): RuntimeResponse\n /**\n * 422 with Zod issues serialized into the `errors` extension per\n * RFC 9457 §3.2's array convention. Pass `error.issues` from a Zod\n * `safeParse` failure or `error.issues` from a thrown ZodError.\n */\n validation(issues: any[], input?: ProblemInput): RuntimeResponse\n}\n\n/**\n * Recursively marks every property of `T` as `readonly`. Preserves\n * `Function`, `Date`, and `RegExp` unchanged, and converts `Map` / `Set`\n * to `ReadonlyMap` / `ReadonlySet` so their mutating methods (`set`,\n * `delete`, `add`, `clear`) are removed from the public surface.\n * Tuples keep their positional element types — only their per-element\n * mutability is locked.\n *\n * Exposed as a utility for adopters who want to opt into compile-time\n * sealing of their own shapes. **No longer applied to\n * `RequestContext.{body,params,query,headers,file,files}`** — that\n * broke TS narrowing / discriminated-union drilldowns on typed request\n * payloads. Runtime read-only enforcement now lives in\n * {@link makeReadOnlyProxy} (dev-only Proxy that warns on writes,\n * no-op in production).\n *\n * Type-only. No runtime cost. `DeepReadonly<any>` is `any` (TS\n * distributes conditional types over `any`).\n */\nexport type DeepReadonly<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends Date | RegExp\n ? T\n : T extends Map<infer K, infer V>\n ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>\n : T extends Set<infer V>\n ? ReadonlySet<DeepReadonly<V>>\n : T extends readonly (infer U)[]\n ? // Distinguish a regular array from a tuple. A regular `T[]` widens\n // its `length` to `number`; a tuple's `length` is a literal\n // (e.g. `2`). The literal branch maps over `keyof T` so each\n // positional slot keeps its own type — `DeepReadonly<[1, 2]>` →\n // `readonly [1, 2]`, not `readonly (1 | 2)[]`.\n number extends T['length']\n ? ReadonlyArray<DeepReadonly<U>>\n : { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T\n\n/**\n * Re-export the augmentable {@link ContextMeta} registry from core/.\n * Declared in `core/execution-context.ts` so non-HTTP transports\n * (WS/queue/cron, V2) share one declaration site. Apps still augment\n * via the canonical `declare module '@forinda/kickjs'` form.\n */\nexport type { ContextMeta } from '../core/execution-context'\n\n/**\n * Symbol key under which the per-request `AbortController` backing\n * `RequestContext.signal` is cached on the Express `req` object.\n * Shared across every `RequestContext` instance built for the same\n * request (router-builder constructs separate wrappers for\n * middleware, the contributor pipeline, and the main handler — they\n * all need the same signal).\n */\nconst signalControllerKey = Symbol.for('kickjs.requestContext.signalController')\n\n/**\n * Symbol key under which per-target read-only Proxy wrappers are\n * cached on the Express `req` object. Used by {@link makeReadOnlyProxy}\n * so repeat access of `ctx.body` / `ctx.params` / etc. returns the same\n * Proxy instance (cheap, and stable under `===` for adopters comparing\n * values across middleware).\n */\nconst readOnlyProxyCacheKey = Symbol.for('kickjs.requestContext.readOnlyProxyCache')\n\n/**\n * `true` iff `v` is a plain object (`{}` / `Object.create(null)`) or an\n * array — the two shapes JSON.parse / Express body-parser produce and\n * the only ones the read-only Proxy recurses into. Class instances,\n * `Date`, `RegExp`, `Map`, `Set`, `Buffer`, streams, and functions\n * pass through unwrapped so their method dispatch and `instanceof`\n * checks keep working.\n */\nfunction isPlainObjectOrArray(v: unknown): v is object {\n if (v === null || typeof v !== 'object') return false\n if (Array.isArray(v)) return true\n const proto = Object.getPrototypeOf(v)\n return proto === Object.prototype || proto === null\n}\n\n/**\n * In dev, wrap `target` in a `Proxy` whose `set` / `deleteProperty` /\n * `defineProperty` traps log a warning and otherwise leave the\n * underlying object untouched. A `get` trap recursively wraps nested\n * plain objects and arrays on access, so deep mutations\n * (`ctx.body.user.name = 'x'`, `ctx.files[0].fieldname = 'y'`,\n * `ctx.body.tags.push(...)`) all surface a warning — matching the\n * prior `DeepReadonly<T>` contract at runtime instead of at the type\n * level. In production (`process.env.NODE_ENV === 'production'`) the\n * proxy is bypassed entirely and `target` is returned as-is so the\n * hot path stays zero-cost.\n *\n * Why a Proxy instead of `DeepReadonly<T>` at the type level: the\n * recursive conditional type interfered with TS narrowing (notably\n * discriminated unions on `ctx.body`) and slowed type-checking on\n * deeply-nested Zod-inferred shapes. Compile-time sealing is gone;\n * runtime warning catches the same class of bug during dev and tests.\n *\n * Trap returns `true` so strict-mode call sites (which is all of them\n * — ES modules are strict) don't *throw* on assignment — they just\n * get a warning + the underlying value stays untouched. Adopters\n * discover the violation when the subsequent read returns the\n * original value, not the one they \"set\".\n *\n * Targets are cached per-`req` under {@link readOnlyProxyCacheKey} so\n * (1) repeat getter access returns the same proxy (`ctx.body ===\n * ctx.body`, `ctx.body.user === ctx.body.user`); and (2) cyclic refs\n * (`body.self = body`) terminate via the cache hit on the second\n * visit. Primitives, `null`, and non-plain values pass through.\n */\nfunction makeReadOnlyProxy<T>(target: T, req: object, label: string): T {\n if (process.env.NODE_ENV === 'production') return target\n if (!isPlainObjectOrArray(target)) return target\n\n type Cache = WeakMap<object, unknown>\n const reqObj = req as Record<symbol, Cache | undefined>\n let cache = reqObj[readOnlyProxyCacheKey]\n if (!cache) {\n cache = new WeakMap()\n reqObj[readOnlyProxyCacheKey] = cache\n }\n const targetObj = target as unknown as object\n const cached = cache.get(targetObj)\n if (cached) return cached as T\n\n const warnWrite = (verb: string, prop: PropertyKey) => {\n console.warn(\n `[kickjs] Attempted to ${verb} \\`ctx.${label}.${String(prop)}\\` — ` +\n `request data is read-only. ` +\n `Stash computed values via \\`ctx.set('key', value)\\` or a Context Contributor, ` +\n `or reach \\`ctx.req.${label}\\` if you genuinely need the raw mutable handle.`,\n )\n }\n\n const proxy = new Proxy(targetObj, {\n get(t, prop, receiver) {\n const value = Reflect.get(t, prop, receiver)\n // Recurse only into plain objects / arrays. Functions (incl.\n // array methods like `.push`) pass through with `receiver` as\n // their `this`, so internal `this[i] = x` writes still hit the\n // `set` trap and warn.\n if (isPlainObjectOrArray(value)) {\n return makeReadOnlyProxy(value, req, `${label}.${String(prop)}`)\n }\n return value\n },\n set(_t, prop) {\n warnWrite('assign', prop)\n return true\n },\n deleteProperty(_t, prop) {\n warnWrite('delete', prop)\n return true\n },\n defineProperty(_t, prop) {\n warnWrite('Object.defineProperty on', prop)\n return true\n },\n })\n cache.set(targetObj, proxy)\n return proxy as T\n}\n\n/**\n * The shape of a single route entry in `KickRoutes`. Generated by\n * `kick typegen` and consumed via the `Ctx<T>` helper. Each field is\n * `unknown` by default and is filled in as the typegen learns more\n * about the route (URL params from the path, body/query from validation\n * schemas, etc.).\n */\nexport interface RouteShape {\n params?: unknown\n body?: unknown\n query?: unknown\n response?: unknown\n /**\n * Union of context keys `kick typegen` proved are populated for this\n * route — the keys of every context contributor applied at method or\n * class level. Drives {@link RequestContext.require} narrowing, so\n * calling `ctx.require()` for a key this route doesn't carry is a\n * compile error instead of a request-time throw.\n *\n * `string` (the default) means \"not proven\", not \"no keys\". Typegen\n * emits `string` whenever it cannot see the full picture — a\n * contributor registered at module / adapter / bootstrap level, or an\n * unrecognised decorator that might bundle contributors of its own.\n * Narrowing an unprovable route would produce compile errors for keys\n * that really are present.\n */\n contextKeys?: string\n}\n\n/**\n * Unified request/response abstraction passed to every controller method.\n * Shields handlers from raw Express objects and provides convenience methods.\n *\n * Implements {@link ExecutionContext} so the transport-agnostic Context\n * Contributor pipeline (#107) can run against this concrete HTTP context\n * the same way it will run against future WS / queue / cron contexts.\n */\nexport class RequestContext<\n TBody = any,\n TParams = any,\n TQuery = any,\n /**\n * Context keys proven present for this route. Defaults to\n * {@link ContextMetaKey} — every key the project has registered — which\n * collapses to `string` when neither registry has been augmented, so a\n * handler typed as plain `RequestContext` keeps today's behaviour. That\n * default is also the deliberate escape hatch when typegen's view of a\n * route is incomplete.\n */\n TKeys extends ContextMetaKey = ContextMetaKey,\n> implements ExecutionContext<TKeys> {\n /**\n * Engine-agnostic response driver the response helpers write through. Under\n * the Express runtime it IS `res` (Express's Response satisfies\n * {@link RuntimeResponse} structurally); other runtimes pass a thin wrapper\n * over their native reply as the optional fourth argument.\n */\n private readonly _response: RuntimeResponse\n\n /**\n * The route this request matched: its method, path, controller, handler name\n * and resolved {@link MatchedRoute.flags}.\n *\n * Available to anything running inside the matched route — route middleware,\n * contributors, guards, the handler. Global middleware runs before route\n * matching, so it sees `undefined` there.\n *\n * ```ts\n * if (ctx.route?.flags.has('auth.public')) return next()\n * ```\n */\n get route(): MatchedRoute | undefined {\n return (this.req as unknown as Record<symbol, MatchedRoute | undefined>)?.[ROUTE_SLOT]\n }\n\n constructor(\n public readonly req: ActiveRuntime['request'],\n public readonly res: ActiveRuntime['response'],\n public readonly next: NextFunction,\n responseDriver?: RuntimeResponse,\n ) {\n // Under the Express runtime `res` IS a RuntimeResponse (structural); other\n // runtimes pass an explicit driver wrapping their native reply.\n this._response = responseDriver ?? (res as RuntimeResponse)\n }\n\n /**\n * Memo of the most recent {@link qs} call this request. The query\n * object is immutable for a request's lifetime, so a repeat call with\n * the same `(fieldConfig, options)` references returns the cached\n * parse instead of re-walking the filter/sort strings.\n */\n private _qsMemo?: { config: unknown; options: unknown; result: unknown }\n\n /** Memoized {@link problem} sender — built lazily on first access. */\n private _problem?: ProblemSender\n\n /**\n * Read-side accessor for the per-request metadata map.\n *\n * Returns the AsyncLocalStorage map (`requestStore.getStore().values`)\n * shared with the Context Contributor pipeline when an ALS frame is\n * active. Returns `null` outside any frame — read-side callers\n * (`ctx.get`, `ctx.user`, `ctx.tenantId`, `ctx.roles`) treat that as\n * \"no metadata\" and fall through to their `req.*` defaults. Reads\n * never throw because outside an ALS frame nobody could have written\n * via `ctx.set` anyway, so an empty answer is the truthful one.\n */\n private metadataReadOnly(): Map<string, any> | null {\n return (requestStore.getStore()?.values as Map<string, any> | undefined) ?? null\n }\n\n /**\n * Write-side accessor for the per-request metadata map.\n *\n * Returns the same map as {@link metadataReadOnly} when an ALS frame\n * is active. Throws otherwise — writing to a fallback map outside\n * the ALS frame would silently lose data because other call sites\n * (downstream contributors, request-logger, etc.) read from the\n * canonical store and would never see it. Mount\n * `requestScopeMiddleware()` (Application does this for you in\n * `'auto'` mode) or wrap your test setup in\n * `requestStore.run({...}, () => …)` to fix.\n */\n private metadataForWrite(): Map<string, any> {\n const store = requestStore.getStore()\n if (store) return store.values as Map<string, any>\n throw new Error(\n 'RequestContext.set called outside an AsyncLocalStorage frame. ' +\n 'Mount requestScopeMiddleware() before constructing RequestContext, ' +\n 'or wrap test setup in requestStore.run({ requestId, instances: new Map(), values: new Map() }, () => ...).',\n )\n }\n\n // ── Request Data ────────────────────────────────────────────────────\n\n /**\n * Parsed request body. Wrapped in a dev-only Proxy that warns when\n * adopters try to mutate Zod-validated payloads in place — compute\n * new values and stash them via `ctx.set('key', value)` or a Context\n * Contributor instead. Reach for `ctx.req.body` if you genuinely\n * need the raw, mutable Express handle (rare).\n *\n * In production the Proxy is bypassed and `this.req.body` is\n * returned as-is — no runtime cost on the hot path.\n */\n get body(): TBody {\n return makeReadOnlyProxy<TBody>(this.req.body, this.req, 'body')\n }\n\n /** Path parameters parsed from the route — read-only, see {@link body}. */\n get params(): TParams {\n return makeReadOnlyProxy<TParams>(this.req.params as TParams, this.req, 'params')\n }\n\n /** Parsed query string — read-only, see {@link body}. */\n get query(): TQuery {\n return makeReadOnlyProxy<TQuery>(this.req.query as TQuery, this.req, 'query')\n }\n\n /** Inbound HTTP headers — read-only, see {@link body}. */\n get headers(): Request['headers'] {\n return makeReadOnlyProxy<Request['headers']>(this.req.headers, this.req, 'headers')\n }\n\n get requestId(): string | undefined {\n return (this.req as any).requestId ?? (this.req.headers['x-request-id'] as string | undefined)\n }\n\n /**\n * `AbortSignal` that fires when the underlying HTTP request is\n * closed — either because the client disconnected before the\n * response was sent, or because Node's socket-close event ran\n * after a normal response. Multiple `RequestContext` wrappers for\n * the same request (router-builder constructs one per\n * middleware/contributor pipeline / main handler) share a single\n * `AbortController` cached on the underlying `req` object, so the\n * signal is stable across the request lifecycle.\n *\n * Thread it through anything that takes an `AbortSignal` so the\n * work cancels as soon as the client gives up — e.g. a kickjs-db\n * relational query:\n *\n * ```ts\n * @Get('/:id/full')\n * async show(ctx: RequestContext) {\n * const row = await this.repo.findFullById(ctx.params.id, ctx.signal)\n * if (!row) return ctx.notFound()\n * ctx.json(row)\n * }\n * ```\n *\n * The repo passes `signal` to `db.query.<table>.findUnique({ signal })`;\n * if the client disconnects mid-flight, the in-flight query rejects\n * with `RelationalQueryCancelledError` instead of consuming the\n * connection until completion.\n *\n * Implementation note — both `req.on('close')` and `res.on('close')`\n * are wired so the signal aborts regardless of whether the close\n * event lands on the request stream or the response stream first.\n * Subsequent fires are no-ops (`AbortController.abort()` is\n * idempotent).\n */\n get signal(): AbortSignal {\n const req = this.req as Request & { [signalControllerKey]?: AbortController }\n const cached = req[signalControllerKey]\n if (cached) return cached.signal\n\n const ctrl = new AbortController()\n req[signalControllerKey] = ctrl\n const abort = () => ctrl.abort('request closed')\n this.req.once('close', abort)\n this._response.once('close', abort)\n return ctrl.signal\n }\n\n /**\n * The request's session — set by the `session()` middleware, which must be\n * installed. Type your keys once by augmenting `SessionData`:\n *\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface SessionData { userId?: string }\n * }\n * ```\n */\n get session(): Session {\n return (this.req as { session?: Session }).session as Session\n }\n\n /**\n * The authenticated user set by AuthAdapter.\n * Reads from the per-request metadata store first (`ctx.set('user', ...)`),\n * then falls back to `req.user` (set directly by AuthAdapter middleware).\n *\n * Extend the `AuthUser` interface via module augmentation in your own auth\n * module — see the BYO Auth recipe in the docs\n * to get full typing.\n *\n * @example\n * ```ts\n * @Get('/me')\n * @Authenticated()\n * getProfile(ctx: RequestContext) {\n * const user = ctx.user\n * if (!user) return ctx.notFound()\n * return ctx.json({ user })\n * }\n * ```\n */\n get user(): MetaValue<'user', Record<string, any>> | undefined {\n return this.metadataReadOnly()?.get('user') ?? (this.req as any).user\n }\n\n /**\n * Tenant identifier for multi-tenant apps. Reads `user.tenantId` —\n * populated either by `AuthAdapter.testMode({ tenantId })`, a tenant\n * resolver middleware, or manually on the strategy's `mapPayload`.\n *\n * Augment `AuthUser` (or `ContextMeta['user']`) to widen the return\n * type if your user shape guarantees a non-`string | undefined` id.\n */\n get tenantId(): string | undefined {\n const u = this.user as Record<string, any> | undefined\n return u?.tenantId\n }\n\n /**\n * Effective role list for the current user. Prefers `user.tenantRoles`\n * (populated by `AuthAdapterOptions.roleResolver` under a tenant) and\n * falls back to `user.roles`. Returns an empty array when the user is\n * absent or the shape has neither field.\n */\n get roles(): string[] {\n const u = this.user as Record<string, any> | undefined\n return u?.tenantRoles ?? u?.roles ?? []\n }\n\n // ── Query String Parsing ───────────────────────────────────────────\n\n /**\n * Parse the request query string into structured filters, sort, pagination, and search.\n * Pass the result to an ORM query builder adapter (Drizzle, Prisma, Sequelize, etc.).\n *\n * Generic over the field config: when passed an inline literal (or\n * `as const`), `filters[].field` and `sort[].field` are narrowed to\n * the exact whitelisted field names. Pass nothing or a non-literal\n * config and you get the loose `string` fallback.\n *\n * @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields\n * @param options - Limits (`maxLimit`, `defaultLimit`, `maxSearchLength`) and an\n * `onReject` hook. By default a rejected/over-broad field is logged via\n * `console.warn` with the request id; pass an explicit `onReject` to override\n * (e.g. throw to return a 400), or `onReject: () => {}` to silence.\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * } as const)\n * parsed.filters[0].field // typed as 'status' | 'priority'\n * parsed.sort[0].field // typed as 'createdAt' | 'title'\n * }\n * ```\n */\n qs<TConfig extends QueryFieldConfig | undefined = undefined>(\n fieldConfig?: TConfig,\n options?: ParseQueryOptions,\n ): TypedParsedQuery<TConfig> {\n // Cheap per-request memo — repeat calls with the same args (the\n // common controller pattern) skip re-parsing. `req.query` is fixed\n // for the request's lifetime, so caching is always sound.\n const memo = this._qsMemo\n if (memo && memo.config === fieldConfig && memo.options === options) {\n return memo.result as TypedParsedQuery<TConfig>\n }\n const effective: ParseQueryOptions = {\n ...options,\n onReject:\n options?.onReject ??\n ((rejection) => {\n const id = this.requestId ? `[${this.requestId}] ` : ''\n console.warn(\n `${id}query ${rejection.kind} '${rejection.field}' rejected (${rejection.reason})`,\n )\n }),\n }\n const result = parseQuery<TConfig>(\n this.req.query as Record<string, any>,\n fieldConfig,\n effective,\n )\n this._qsMemo = { config: fieldConfig, options, result }\n return result\n }\n\n // ── File Uploads ────────────────────────────────────────────────────\n\n /**\n * Single uploaded file (requires `@FileUpload({ mode: 'single' })`).\n * Wrapped in a dev-only Proxy that warns on mutation — see {@link body}.\n * File metadata is set by the upload middleware and shouldn't be\n * mutated downstream.\n */\n get file(): Express.Multer.File | undefined {\n const f = this.req.file\n return f ? makeReadOnlyProxy<Express.Multer.File>(f, this.req, 'file') : undefined\n }\n\n /**\n * Array of uploaded files (requires `@FileUpload({ mode: 'array' })`).\n * The array (and each entry in dev) is wrapped in a read-only Proxy\n * that warns on `push`/`pop` and per-file mutation. Use\n * `ctx.req.files` if you genuinely need the mutable Express handle.\n */\n get files(): Express.Multer.File[] | undefined {\n // multer's `Request['files']` is a union — array (mode: 'array' /\n // 'any') or `{ [fieldname]: File[] }` (mode: 'fields'). The\n // `@FileUpload({ mode: 'array' })` decorator only sets the array\n // shape, but `Request['files']` is the broad union. Narrow back to\n // `File[]` here for the common-case getter; adopters using\n // `mode: 'fields'` can read `ctx.req.files` directly for the\n // dictionary shape.\n const f = this.req.files\n if (!Array.isArray(f)) return undefined\n return makeReadOnlyProxy<Express.Multer.File[]>(f, this.req, 'files')\n }\n\n // ── Metadata Store ──────────────────────────────────────────────────\n\n /**\n * Read a value from the per-request metadata store.\n *\n * The key is constrained to {@link ContextMetaKey} — the keys registered in\n * `ContextMeta` / `ContextKeys` — so a typo is a compile error rather than a\n * permanent `undefined`. Projects that have augmented neither registry get\n * `string`, exactly as before.\n *\n * Constrained by `ContextMetaKey` and deliberately NOT by `TKeys`: \"does this\n * key exist in the app\" is a different question from \"does this route carry\n * it\", and narrowing by the latter would turn an incomplete typegen view into\n * false errors for keys written by middleware. Presence is `require`'s job;\n * this always returns `| undefined`.\n *\n * When `ContextMeta` has a matching key the return type is inferred\n * automatically. For ad-hoc keys, pass a generic: `ctx.get<MyType>('custom')`.\n */\n get<K extends ContextMetaKey>(key: K): MetaValue<K> | undefined {\n return this.metadataReadOnly()?.get(key) as MetaValue<K> | undefined\n }\n\n /**\n * Read a value that a contributor is expected to have produced, and\n * throw {@link MissingContextValueError} when it hasn't.\n *\n * Use this instead of `ctx.get(key)!` anywhere the value is a\n * precondition rather than an optional extra — permissions, tenants,\n * the resolved subject of the route. The non-null assertion compiles\n * whether or not the producing contributor is actually applied to the\n * route, so a dropped `@LoadTenant` turns into `undefined` flowing\n * silently into an auth check. `require()` fails loudly at that point\n * instead, naming the key.\n *\n * ```ts\n * // before — compiles even if @OperatorPerm was never applied\n * const perm = ctx.get('operatorPerm')!\n *\n * // after — throws MissingContextValueError naming the key + route\n * const perm = ctx.require('operatorPerm')\n * ```\n *\n * When `TKeys` is narrowed — which it is for a handler typed as\n * `Ctx<KickRoutes…>`, from typegen's per-route `contextKeys` — asking\n * for a key the route doesn't carry is a compile error, so a dropped\n * decorator fails the build. `TKeys` stays `string` (no narrowing) for\n * a plain `RequestContext` and for any route typegen couldn't prove\n * complete; there this is a runtime guarantee only.\n * See `architecture.md` §20.14.\n *\n * `null` counts as present; only `undefined` (or a missing entry)\n * throws. A contributor that deliberately resolves to `null` is\n * expressing \"looked, found nothing\", which is a real value — which\n * is why the return type is `Exclude<…, undefined>` and NOT\n * `NonNullable<…>`. The latter would strip `null` from the type while\n * the runtime still hands it back, so a caller would be told a value\n * is non-null and then get `null`.\n */\n require<K extends TKeys>(key: K): Exclude<MetaValue<K>, undefined> {\n const value = this.metadataReadOnly()?.get(key)\n if (value === undefined) {\n throw new MissingContextValueError(key, this.routeLabel())\n }\n return value as Exclude<MetaValue<K>, undefined>\n }\n\n /**\n * Best-effort `\"GET /path\"` label for error messages. Runtime-agnostic:\n * express/fastify requests and web-standard `Request` all expose\n * `method` + `url`, but none of them are guaranteed here, so every\n * access is defensive and the whole thing is optional.\n */\n private routeLabel(): string | undefined {\n const req = this.req as { method?: unknown; url?: unknown } | undefined\n const method = typeof req?.method === 'string' ? req.method : undefined\n const url = typeof req?.url === 'string' ? req.url : undefined\n if (method && url) return `${method} ${url}`\n return method ?? url\n }\n\n /**\n * Write a value to the per-request metadata store. Throws when no\n * AsyncLocalStorage frame is active — see {@link metadataForWrite}\n * for why a silent fallback is the wrong default.\n */\n set<K extends ContextMetaKey>(key: K, value: MetaValue<K>): void {\n this.metadataForWrite().set(key, value)\n }\n\n // ── Response Helpers ────────────────────────────────────────────────\n\n /**\n * Set a response header. Runtime-neutral — works on every engine and on\n * the web/edge entry. No-op guards are the runtime's job (headers set\n * after the response settles are ignored or throw per engine).\n */\n setHeader(name: string, value: string | number | readonly string[]): this {\n this._response.setHeader(name, value)\n return this\n }\n\n json(data: any, status = 200) {\n return this.sendJson(status, data)\n }\n\n created(data: any) {\n return this.sendJson(201, data)\n }\n\n private sendJson(status: number, data: unknown): RuntimeResponse {\n try {\n return this._response.status(status).json(data)\n } catch (err) {\n // Express serialises inside `res.json` and throws on a bigint before\n // anything is written; the other runtimes' drivers use stringifyJson.\n if (!isBigIntJsonError(err) || this._response.headersSent) throw err\n return this._response.status(status).json(JSON.parse(stringifyJson(data)))\n }\n }\n\n noContent() {\n return this._response.status(204).end()\n }\n\n /**\n * @deprecated Prefer {@link ProblemSender.notFound | ctx.problem.notFound()},\n * which emits an RFC 9457 — Problem Details response. This helper stays\n * for backward compatibility and will not be removed.\n */\n notFound(message = 'Not Found') {\n return this._response.status(404).json({ message })\n }\n\n /**\n * @deprecated Prefer {@link ProblemSender.badRequest | ctx.problem.badRequest()},\n * which emits an RFC 9457 — Problem Details response. This helper stays\n * for backward compatibility and will not be removed.\n */\n badRequest(message: string) {\n return this._response.status(400).json({ message })\n }\n\n // ── RFC 9457 Problem Details ───────────────────────────────────────\n\n /**\n * RFC 9457 — Problem Details for HTTP APIs response helper.\n * https://datatracker.ietf.org/doc/html/rfc9457\n *\n * `ctx.problem(...)` sends a canonical problem-detail response with\n * `Content-Type: application/problem+json`. The convenience methods\n * (`ctx.problem.notFound`, `.badRequest`, etc.) pre-fill `status` +\n * `title` for the common codes.\n *\n * `type` defaults to `about:blank` (§3.1.1); `title` defaults to the\n * IANA reason phrase for `status` (§3.1.4). Extension members per §3.2\n * pass through unchanged.\n *\n * @example\n * ```ts\n * ctx.problem({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * detail: 'Your balance is 30, but that costs 50.',\n * instance: `/account/${ctx.user.id}/messages/${ctx.params.id}`,\n * balance: 30,\n * })\n *\n * ctx.problem.notFound({ detail: 'User abc not found' })\n * ctx.problem.validation(zodResult.error.issues)\n * ```\n *\n * @see {@link ProblemException} for the exception-throwing equivalent\n * (use it from services where `ctx` isn't in scope).\n */\n get problem(): ProblemSender {\n // Memoized — building the sender allocates ~10 closures, so pay that only\n // on the first access per context instead of per `ctx.problem.*` call.\n if (this._problem) return this._problem\n const send = (input: ProblemDetails): RuntimeResponse => this.sendProblem(input)\n send.badRequest = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 400, ...input })\n send.unauthorized = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 401, ...input })\n send.forbidden = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 403, ...input })\n send.notFound = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 404, ...input })\n send.conflict = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 409, ...input })\n send.unprocessable = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 422, ...input })\n send.tooManyRequests = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 429, ...input })\n send.internal = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 500, ...input })\n send.validation = (issues: any[], input: ProblemInput = {}): RuntimeResponse => {\n const errors: ValidationError[] = (issues || []).map((issue: any) => ({\n field: issue.path?.join?.('.') ?? '',\n message: issue.message ?? '',\n code: issue.code,\n }))\n return this.sendProblem({\n status: 422,\n detail: issues?.[0]?.message,\n ...input,\n errors,\n })\n }\n this._problem = send as ProblemSender\n return this._problem\n }\n\n private sendProblem(input: ProblemDetails): RuntimeResponse {\n const body = normalizeProblem(input)\n this._response.setHeader('Content-Type', 'application/problem+json')\n return this._response.status(body.status).json(body)\n }\n\n /**\n * Send an HTML response.\n *\n * Passes the full MIME type rather than the `'html'` shorthand. The\n * shorthand is an Express affordance — `res.type()` runs it through a MIME\n * lookup — but the Fastify and h3 drivers forward the string to their own\n * `type()` / `setHeader()` verbatim, so `'html'` reached the wire as a\n * malformed `Content-Type: html` on those engines. Fastify 5.10 started\n * normalising that invalid value to `text/plain`, which is what surfaced it.\n *\n * Express produces `text/html; charset=utf-8` for `.type('html')`, so\n * spelling it out here is byte-identical there and correct everywhere else.\n */\n /**\n * The client's IP address, or `undefined` when it cannot be determined.\n *\n * Prefers the value the runtime computed (Express derives `req.ip` from the\n * app's `trust proxy` setting; Fastify from `trustProxy`), because raw\n * forwarded headers are client-SPOOFABLE on deployments that do not\n * normalize them. The header fallbacks exist for runtimes with no `ip`\n * field at all — notably the web/edge entry.\n *\n * `ctx.req.ip` is Express-only, so reaching for it directly broke on\n * Fastify and h3.\n */\n get ip(): string | undefined {\n return resolveClientIp(this.req as ClientRequestLike)\n }\n\n /**\n * Redirect the client. Works on every runtime.\n *\n * Every engine spells this differently — Express and Fastify have\n * `res.redirect()`, h3 has `sendRedirect(event, location, code)` — and none\n * of them is shared, so this writes the status and `Location` header through\n * the runtime response surface, which all four expose.\n *\n * The response carries no body. Express sends a short text line and h3 an\n * HTML meta-refresh fallback; a bare `Location` with a 3xx is sufficient per\n * RFC 9110, and interpolating the URL into an HTML body would add an\n * injection surface to a value this method already warns is dangerous.\n *\n * ::: warning\n * Never pass an unvalidated user-supplied `url`. An attacker-controlled\n * destination turns this into an open redirect, which is a common phishing\n * primitive. Allow-list the target or keep it relative.\n * :::\n */\n redirect(url: string, status = 302) {\n this._response.status(status).setHeader('location', url)\n return this._response.end()\n }\n\n html(content: string, status = 200) {\n return this._response.status(status).type('text/html; charset=utf-8').send(content)\n }\n\n /**\n * Send a body with its content type — XML, plain text, CSV, an image.\n * Runtime-neutral, like `json()` and `html()`. `type` defaults to\n * `text/plain; charset=utf-8`.\n *\n * @example\n * ```ts\n * ctx.send(sitemapXml, { type: 'application/xml' })\n * ctx.send(csv, { type: 'text/csv; charset=utf-8', status: 200 })\n * ```\n */\n send(body: string | Uint8Array, options: { type?: string; status?: number } = {}) {\n return this._response\n .status(options.status ?? 200)\n .type(options.type ?? 'text/plain; charset=utf-8')\n .send(body)\n }\n\n /**\n * Send `buffer` as a file download named `filename`. The name is written\n * per RFC 6266 — an ASCII fallback plus `filename*` in UTF-8 — so a name\n * from an upload (quotes, line breaks, non-Latin text) can't break the header.\n */\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this._response.setHeader('Content-Disposition', contentDisposition(filename))\n this._response.setHeader('Content-Type', contentType)\n return this._response.send(buffer)\n }\n\n /**\n * Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).\n * Requires a ViewAdapter to be configured in bootstrap().\n *\n * @param template - Template name (without extension, relative to viewsDir)\n * @param data - Data to pass to the template\n *\n * @example\n * ```ts\n * ctx.render('dashboard', { user, title: 'Dashboard' })\n * ctx.render('emails/welcome', { name: 'Alice' })\n * ```\n */\n render(template: string, data: Record<string, any> = {}) {\n return this._response.render(template, data)\n }\n\n /**\n * Parse query params and return a standardized paginated response.\n * Calls `ctx.qs()` internally, then wraps your data with pagination meta.\n *\n * @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`\n * @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * return ctx.paginate(\n * async (parsed) => {\n * const data = await db.select().from(users)\n * .where(query.where).limit(parsed.pagination.limit)\n * .offset(parsed.pagination.offset).all()\n * const total = await db.select({ count: count() }).from(users).get()\n * return { data, total: total?.count ?? 0 }\n * },\n * { filterable: ['name', 'role'], sortable: ['createdAt'] },\n * )\n * }\n * ```\n */\n async paginate<T, TConfig extends QueryFieldConfig | undefined = undefined>(\n fetcher: (parsed: TypedParsedQuery<TConfig>) => Promise<{ data: T[]; total: number }>,\n fieldConfig?: TConfig,\n ): Promise<PaginatedResponse<T>> {\n const parsed = this.qs<TConfig>(fieldConfig)\n const { data, total } = await fetcher(parsed)\n const { page, limit } = parsed.pagination\n const totalPages = Math.ceil(total / limit) || 1\n\n const response: PaginatedResponse<T> = {\n data,\n meta: {\n page,\n limit,\n total,\n totalPages,\n hasNext: page < totalPages,\n hasPrev: page > 1,\n },\n }\n\n this.json(response)\n // Sends AND returns the payload. Returning `this.json(...)` handed back\n // the engine's `RuntimeResponse`, so the documented usage —\n // `return ctx.paginate(...)` — emitted `response: RuntimeResponse` into\n // `KickRoutes`, and the typed client offered `.status()` / `.setHeader()`\n // where the caller expected `data` and `meta`.\n //\n // Returning the payload rather than tagging the response reuses the\n // existing return-value inference (the same path `return user` and\n // `return reply(201, user)` take) instead of adding a second mechanism\n // beside `reply`. Sending here is kept for handlers that call `paginate`\n // without returning it; the runtimes only auto-send a returned value when\n // nothing was written (`if (!res.headersSent)`), so there is no double\n // send.\n return response\n }\n\n // ── Server-Sent Events ──────────────────────────────────────────────\n\n /**\n * Start an SSE (Server-Sent Events) stream.\n * Sets the correct headers and returns helpers to send events.\n *\n * Pass a payload type to get typed sends — and `return sse` from the\n * handler so `kick typegen` carries the event type into\n * `KickRoutes.Api`, where `@forinda/kickjs-client`'s `api.stream()`\n * picks it up (the returned handler is ignored at runtime — the\n * response is already streaming).\n *\n * @example\n * ```ts\n * interface Tick { time: string }\n *\n * @Get('/events')\n * async stream(ctx: RequestContext) {\n * const sse = ctx.sse<Tick>()\n *\n * const interval = setInterval(() => {\n * sse.send({ time: new Date().toISOString() }, 'tick')\n * }, 1000)\n *\n * sse.onClose(() => clearInterval(interval))\n * return sse // typed as SseHandler<Tick> → response inference\n * }\n * ```\n */\n /**\n * Send a web-standard `Response` — status, headers (every `Set-Cookie`\n * kept) and body, streamed chunk by chunk — on any runtime. For handing off\n * to libraries that speak `Request`/`Response`, such as the MCP SDK's\n * web-standard transport.\n *\n * Resolves once the body is fully written. Stops reading the body when the\n * client disconnects.\n *\n * @example\n * ```ts\n * @Post('/mcp')\n * async mcp(ctx: RequestContext) {\n * await ctx.sendResponse(await transport.handleRequest(webRequest))\n * }\n * ```\n */\n async sendResponse(response: Response): Promise<void> {\n const headers: Record<string, string | string[]> = {}\n response.headers.forEach((value, name) => {\n if (name !== 'set-cookie') headers[name] = value\n })\n const cookies = response.headers.getSetCookie()\n if (cookies.length > 0) headers['set-cookie'] = cookies\n\n if (!response.body) {\n // Not writeHead() + end(): on the web driver writeHead switches to a\n // streamed Response, which a 204/304 cannot be, and on Fastify end()\n // bypasses the headers set on the reply. send() is each runtime's own\n // buffered reply path.\n this._response.status(response.status)\n for (const [name, value] of Object.entries(headers)) this._response.setHeader(name, value)\n this._response.send('')\n return\n }\n this._response.writeHead(response.status, headers as Record<string, string>)\n // Send the headers now: a stream whose first chunk comes later (SSE)\n // would otherwise leave the client waiting to see the response open.\n this._response.flushHeaders()\n const reader = response.body.getReader()\n const stop = () => {\n reader.cancel().catch(() => {})\n }\n this.signal.addEventListener('abort', stop, { once: true })\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n // Honour backpressure: on a slow client, wait for the socket to drain\n // (or the client to go away) before reading more of the body.\n if (this._response.write(Buffer.from(value)) === false && !this.signal.aborted) {\n await new Promise<void>((resolve) => {\n const resume = () => {\n this.signal.removeEventListener('abort', resume)\n resolve()\n }\n this._response.once('drain', resume)\n this.signal.addEventListener('abort', resume, { once: true })\n })\n }\n }\n } finally {\n this.signal.removeEventListener('abort', stop)\n this._response.end()\n }\n }\n\n /**\n * Keep `promise` running after the response is sent — an audit log, an\n * email, an analytics call. The Node server's `shutdown()` waits for it;\n * `createHandler()` and the web entry hand it to the platform's\n * `waitUntil` when you pass the platform context in. A rejection is\n * logged, never unhandled. Same as the standalone `waitUntil()` export.\n */\n waitUntil(promise: Promise<unknown>): void {\n waitUntil(promise)\n }\n\n sse<T = unknown>(): SseHandler<T> {\n this._response.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n })\n this._response.flushHeaders()\n\n const closeCallbacks: Array<() => void> = []\n\n this.req.on('close', () => {\n for (const cb of closeCallbacks) cb()\n })\n\n return {\n /** Send an SSE event with optional event name and id */\n send: (data: T, event?: string, id?: string) => {\n if (id) this._response.write(`id: ${id}\\n`)\n if (event) this._response.write(`event: ${event}\\n`)\n this._response.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this._response.write(`: ${text}\\n\\n`)\n },\n /** Register a callback when the client disconnects */\n onClose: (fn: () => void) => {\n closeCallbacks.push(fn)\n },\n /** End the SSE stream */\n close: () => {\n this._response.end()\n },\n }\n }\n}\n\n/**\n * The handle {@link RequestContext.sse} returns. The optional phantom\n * `__sse` property carries the event payload type STRUCTURALLY — never set\n * at runtime — so `kick typegen`'s response inference (handler `return sse`)\n * flows it into `KickRoutes.Api`, and `@forinda/kickjs-client` detects SSE\n * routes without importing any server types.\n */\nexport interface SseHandler<T = unknown> {\n /** Phantom event-payload type marker — never assigned at runtime. */\n readonly __sse?: T\n /** Send an SSE event with optional event name and id. */\n send(data: T, event?: string, id?: string): void\n /** Send a comment line (keeps the connection alive through proxies). */\n comment(text: string): void\n /** Register a callback for client disconnect. */\n onClose(fn: () => void): void\n /** End the SSE stream. */\n close(): void\n}\n\n/**\n * Short alias for `RequestContext` parameterised by a route shape.\n *\n * Use with `kick typegen`'s generated `KickRoutes` namespace for\n * fully-typed `ctx.params`, `ctx.body`, and `ctx.query`. Metadata\n * typing (`ctx.get()`, `ctx.set()`, `ctx.user`) comes from\n * `ContextMeta` augmentation and works on all `RequestContext` instances.\n *\n * @example\n * ```ts\n * @Get('/:id')\n * getUser(ctx: Ctx<KickRoutes.UserController['getUser']>) {\n * ctx.params.id // typed from RouteShape\n * ctx.body // typed from RouteShape\n * ctx.user // typed from ContextMeta\n * ctx.get('tenant') // typed from ContextMeta\n * }\n * ```\n */\nexport type Ctx<TRoute extends RouteShape = RouteShape> = RequestContext<\n TRoute extends { body: infer B } ? B : any,\n TRoute extends { params: infer P } ? P : any,\n TRoute extends { query: infer Q } ? Q : any,\n // Falls back to `string` (no narrowing) whenever typegen couldn't prove\n // the route's full contributor set — see `RouteShape.contextKeys`.\n TRoute extends { contextKeys: infer K }\n ? K extends ContextMetaKey\n ? K\n : ContextMetaKey\n : ContextMetaKey\n>\n\n/**\n * Global ambient registry of typed routes, populated by `kick typegen`.\n *\n * Interfaces inside `KickRoutes` correspond to controller classes (each\n * property one route method, conforming to `RouteShape`) — plus the\n * reserved `KickRoutes.Api`: a flat `'METHOD /mounted/path'` map over the\n * same shapes, consumed by `@forinda/kickjs-client`. Don't name a\n * controller class `Api` — it would declaration-merge into that map.\n *\n * Empty by default — declarations come from `.kickjs/types/kick__routes.ts`\n * generated alongside the rest of the typegen output.\n */\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace KickRoutes {}\n}\n\n/** `attachment; filename=\"…\"; filename*=UTF-8''…` for any file name (RFC 6266 / RFC 8187). */\nexport function contentDisposition(filename: string): string {\n const fallback = filename.replace(/[^\\x20-\\x7e]|[\"\\\\]/g, '_')\n const encoded = encodeURIComponent(filename).replace(\n /['()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n )\n return `attachment; filename=\"${fallback}\"; filename*=UTF-8''${encoded}`\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"context-CtL9RxXp.mjs","names":[],"sources":["../src/http/request-store.ts","../src/http/background.ts","../src/http/runtime.ts","../src/http/context.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport type { ContextMetaKey, MetaValue } from '../core/execution-context'\n\n/** Per-request storage for REQUEST-scoped DI and request context propagation */\nexport interface RequestStore {\n /** Unique request identifier */\n requestId: string\n /**\n * Per-request singleton cache for REQUEST-scoped services. Keys are\n * heterogeneous (class constructors, `InjectionToken` objects, raw\n * strings), so the map type stays `Map<any, any>`.\n */\n instances: Map<any, any>\n /**\n * Per-request key/value bag. Canonical backing store for\n * `RequestContext.set/get` and the Context Contributor pipeline (#107)\n * since Phase 3. Anything written here is visible to every concrete\n * execution context constructed for the same request, regardless of\n * which middleware/contributor wrote it.\n *\n * Keys are constrained to `string` per the locked decision in\n * `architecture.md` §20.12 (#4); the value type is `unknown` because\n * the concrete shape at each key is determined by the consumer's\n * `ContextMeta` augmentation, not by the store itself.\n *\n * Prefer the {@link getRequestValue} helper over reaching into this\n * map directly — it threads the augmented `ContextMeta` shape through\n * `MetaValue<K>` so consumers get typed reads without an `as` cast.\n * Writes should flow through `ctx.set(key, value)` or a Context\n * Contributor's return value — see the `setRequestValue` note below\n * for why this package intentionally does NOT ship a service-level\n * write helper.\n */\n values: Map<string, unknown>\n /** Set once {@link disposeRequestStore} has run — guards double-dispose. */\n disposed?: boolean\n}\n\n/** AsyncLocalStorage instance shared across the framework */\nexport const requestStore = new AsyncLocalStorage<RequestStore>()\n\n/**\n * Get the current request store. Throws if called outside a request context.\n * Used by the Container to resolve REQUEST-scoped dependencies.\n */\nexport function getRequestStore(): RequestStore {\n const store = requestStore.getStore()\n if (!store) {\n throw new Error(\n 'No active request context. REQUEST-scoped services can only be resolved during an HTTP request.',\n )\n }\n return store\n}\n\n/**\n * Read a per-request `ContextMeta`-keyed value from outside a controller\n * (services, repositories, anywhere with no `ctx` reference). Returns\n * the augmented type for the key — `MetaValue<K>` — so consumers get the\n * same static safety as `ctx.get(key)` without holding the context.\n *\n * Returns `undefined` outside an active request frame (background jobs,\n * startup code, tests without `requestScopeMiddleware()`) — the helper\n * is intentionally null-tolerant so calling it from a service that\n * runs in both request and non-request paths doesn't throw.\n *\n * @example\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface ContextMeta { tenant: { id: string; name: string } }\n * }\n *\n * @Service()\n * class AuditService {\n * log(action: string) {\n * const tenant = getRequestValue('tenant') // typed: { id; name } | undefined\n * db.audit.insert({ action, tenantId: tenant?.id })\n * }\n * }\n * ```\n */\nexport function getRequestValue<K extends ContextMetaKey>(key: K): MetaValue<K> | undefined {\n return requestStore.getStore()?.values.get(key) as MetaValue<K> | undefined\n}\n\n// Intentionally NO `setRequestValue` export.\n//\n// Writes are deliberately context-scoped — only the controlled write\n// surfaces should mutate the per-request Map:\n//\n// 1. A Context Contributor's `resolve()` / `onError()` return value\n// (the runner does `ctx.set(reg.key, value)` on the contributor's\n// behalf). This is the canonical way to populate a key.\n// 2. A controller / middleware / contributor that holds a\n// `RequestContext` instance can call `ctx.set(key, value)`\n// directly. The write is auditable inline at the call site.\n//\n// Letting arbitrary services reach in and mutate the store from\n// anywhere produces \"spooky action at a distance\" — keys appear in\n// the request bag without an obvious source, and tracing which\n// service polluted what becomes a grep exercise. Services that need\n// to publish per-request state should return the value to the caller\n// (controller or contributor) and let *that* layer write it via\n// `ctx.set`. If a future need genuinely requires a service-level\n// write surface, expose a narrow function (`recordTrace`,\n// `markStartTime`) on a service that captures the side effect, not a\n// generic `setRequestValue`.\n","/**\n * Work that outlives the response — `waitUntil(promise)`.\n *\n * A handler that fires off an audit log, an email, or an analytics call and\n * answers without awaiting it leaves that promise unowned: on serverless the\n * platform freezes or kills the instance once the response is sent, and on a\n * Node server `SIGTERM` exits before it settles. `waitUntil` registers the\n * promise so something waits for it:\n *\n * - on the Node server, `shutdown()` awaits it (within `shutdownTimeout`);\n * - through `createHandler()` and the web entry, it is handed to the\n * platform's own `waitUntil` (Workers `ctx`, Netlify `context`, Vercel's\n * `waitUntil`) when you pass it in.\n *\n * Process-wide, like the DI container: one app per process.\n *\n * @module @forinda/kickjs/http/background\n */\nimport { createLogger } from '../core/logger'\nimport { reportError } from '../core/observers'\nimport { requestStore } from './request-store'\n\nconst log = createLogger('waitUntil')\nconst pending = new Set<Promise<void>>()\n\n/**\n * Keep `promise` alive past the response. A rejection is logged (with the\n * request id when called during a request), never left unhandled.\n *\n * @example\n * ```ts\n * @Post('/')\n * async create(ctx: RequestContext) {\n * const order = await this.orders.create(ctx.body)\n * ctx.waitUntil(this.mailer.sendReceipt(order)) // or waitUntil(...) outside a handler\n * return order\n * }\n * ```\n */\nexport function waitUntil(promise: Promise<unknown>): void {\n const requestId = requestStore.getStore()?.requestId\n const tracked: Promise<void> = Promise.resolve(promise).then(\n () => {},\n (err: unknown) => {\n log.error({ err, requestId }, 'Background work passed to waitUntil() failed')\n reportError(err, { source: 'background', requestId })\n },\n )\n pending.add(tracked)\n void tracked.finally(() => pending.delete(tracked))\n}\n\n/**\n * Resolve once every registered promise has settled — including work\n * registered while waiting. Never rejects.\n */\nexport async function settleBackgroundWork(): Promise<void> {\n while (pending.size > 0) await Promise.all(pending)\n}\n\n/** How many `waitUntil` promises have not settled yet. */\nexport function pendingBackgroundWork(): number {\n return pending.size\n}\n\n/**\n * The platform hook `createHandler()` and the web entry forward background\n * work to. Cloudflare Workers' `ctx`, Netlify's `context`, and `{ waitUntil }`\n * from `@vercel/functions` all have this shape.\n */\nexport interface PlatformContext {\n waitUntil?(promise: Promise<unknown>): void\n}\n\n/** Hand every pending (and later-registered) promise to the platform. */\nexport function forwardBackgroundWork(platform: PlatformContext | undefined): void {\n platform?.waitUntil?.(settleBackgroundWork())\n}\n","// The HttpRuntime seam.\n//\n// Decorators no longer emit an `express.Router` directly. `buildRouteTable()`\n// turns controller metadata into a plain-data `RouteEntry[]`; an `HttpRuntime`\n// materializes that table onto whatever engine it owns. Express is the default\n// runtime and the only one shipped in M1 — its materializer reproduces the exact\n// handler chain the old `buildRoutes()` built, so behavior is unchanged.\n//\n// Fastify / h3 runtimes (later milestones) consume the SAME `RouteEntry` data\n// and the SAME `HttpRuntime` contract; the request/response driver abstraction\n// that lets `RequestContext` run engine-agnostically lands with them (M3), since\n// under Express the drivers ARE the Express request/response objects already.\n//\n// Why a seam: running Fastify / h3 behind an Express-compat layer\n// (`@fastify/express`, h3's node bridge) bypasses their router and\n// serialization, and rebuilding `ctx` over web-standard Request/Response\n// would break SSE / streaming, multer uploads and `ctx.res` for every adopter.\n// The seam keeps `ctx` and swaps only the engine underneath it.\n\nimport { ROUTE_SLOT, type RouteFlags } from '../core/route-flag'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type {\n RequestHandler,\n ErrorRequestHandler,\n Express,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from 'express'\n\nimport type { Constructor, FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core'\nimport type { RequestContext } from './context'\n\n/** HTTP verbs the router emits. Mirrors the decorator surface (@Get/@Post/...). */\nexport type RouteMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n\n/**\n * A context-based handler: the runtime-neutral unit of the request pipeline.\n * Contributors, route middleware (via {@link MiddlewareHandler}), and the\n * terminal controller call all reduce to this shape. The runtime owns how a\n * `CtxHandler` is wrapped onto its engine (Express wraps it in `(req, res,\n * next)`; Fastify in its own handler).\n */\nexport type CtxHandler = (ctx: RequestContext) => unknown | Promise<unknown>\n\n/**\n * Connect-style middleware — the portable middleware format. Built-in\n * middleware and adopter-supplied Express middleware are this shape. The\n * Express runtime hands them straight to `app.use`; later runtimes bridge\n * them (Fastify via `@fastify/middie`, h3 via its node adapter).\n *\n * Typed as the Express handler union for M1 (Express is the only runtime);\n * a future milestone narrows this to bare node `(req, res, next)` primitives\n * once the driver layer lands.\n */\nexport type ConnectMiddleware = RequestHandler | ErrorRequestHandler\n\n/**\n * One materialized route, as plain data. Produced by {@link buildRouteTable}\n * from decorator metadata; consumed by {@link HttpRuntime.mountRoutes}.\n *\n * The shape is deliberately richer than a flat `CtxHandler[]`: `middlewares`\n * keep the `(ctx, next)` signature (they may short-circuit or call `next(err)`),\n * `contributorRunner` is a closure over the pre-built pipeline + container (the\n * pipeline is built — and any cycle / missing-dep error thrown — at table-build\n * time, i.e. boot), and `validation` / `upload` stay as metadata the runtime\n * materializes its own way. Faithfully reproducing the legacy Express chain —\n * not collapsing it — is what keeps M1 behavior-neutral.\n */\nexport interface RouteEntry {\n method: RouteMethod\n /** Path with `:param` segments — the portable subset all engines accept. */\n path: string\n /** Class + method middleware, in execution order; each receives `(ctx, next)`. */\n middlewares: MiddlewareHandler[]\n /**\n * Runs the contributor pipeline against `ctx` (populates `ctx.set(...)`),\n * or `null` when no contributors apply. Closes over the built pipeline and\n * the DI container so it stays engine-neutral; the runtime advances the\n * chain (Express: call `next()`) once it resolves.\n */\n contributorRunner: CtxHandler | null\n /**\n * Contributors registered with `beforeValidation: true`. Runtimes run this\n * right after publishing the matched route — before validation, upload and\n * middleware — so a rejection (401) lands before a malformed body's 422.\n * Absent or `null` when no contributor asks for it.\n */\n earlyContributorRunner?: CtxHandler | null\n /** Terminal handler: resolves the controller per-request and invokes it. */\n handler: CtxHandler\n /** Introspection + runtime-materialized concerns (swagger/typegen read this). */\n meta: RouteMeta\n}\n\n/** Per-route metadata: introspection refs plus runtime-materialized concerns. */\nexport interface RouteMeta {\n /**\n * Owning controller class — for DI resolution and adapter introspection.\n * Absent for ad-hoc routes registered through {@link AdapterHttp.route},\n * which have no controller class behind them.\n */\n controller?: Constructor\n /** Handler method name on the controller, when there is one. */\n handlerName?: string\n /** Zod/JSON-schema validation config from the route decorator, if any. */\n validation?: RouteDefinition['validation']\n /** `@FileUpload` config, if present — the runtime supplies the backend. */\n upload?: FileUploadConfig\n /**\n * Route flags in force here, resolved at build time (method over class).\n * Enabled flags only: a flag declared `false` at a higher-precedence site is\n * absent rather than present-and-false, so `has()` is always the right\n * question. See `core/route-flag.ts`.\n */\n flags?: RouteFlags\n /**\n * The full mounted pattern (`/api/v1/users/:id`) — set by the Application\n * for controller routes. `path` is relative to the module mount.\n */\n pattern?: string\n}\n\n/**\n * The route a request matched, published on `ctx.route`.\n *\n * Everything that holds a {@link RequestContext} — handlers, `@Middleware()`,\n * guards, contributors — can read it, which is what lets one declaration on a\n * route serve every consumer.\n */\n/**\n * Publish the matched route on the request, for `ctx.route` to read.\n *\n * Called by each runtime once per dispatch, before it builds a RequestContext.\n * It lives on `req` rather than on the ctx because the Express runtime\n * constructs a fresh RequestContext per middleware step — anything written to\n * one ctx would be invisible to the next and to the handler.\n */\nexport function publishMatchedRoute(req: unknown, entry: RouteEntry): void {\n ;(req as Record<symbol, MatchedRoute>)[ROUTE_SLOT] = {\n method: entry.method,\n path: entry.path,\n controller: entry.meta.controller,\n handlerName: entry.meta.handlerName,\n flags: entry.meta.flags ?? EMPTY_FLAGS,\n pattern: entry.meta.pattern ?? entry.path,\n }\n}\n\nconst EMPTY_FLAGS: RouteFlags = new Map() as RouteFlags\n\nexport interface MatchedRoute {\n method: RouteMethod\n /** Path as declared on the handler, e.g. `/:id`. */\n path: string\n /**\n * The full route pattern including the mount prefix and version, e.g.\n * `/api/v1/users/:id` — what metrics and spans should be keyed by. Falls\n * back to `path` for routes registered without a mount.\n */\n pattern: string\n controller?: Constructor\n handlerName?: string\n /** Enabled flags only — see {@link RouteMeta.flags}. */\n flags: RouteFlags\n}\n\n/** A controller's routes grouped under the module mount prefix. */\nexport type RouteTable = { mountPath: string; routes: RouteEntry[] }[]\n\n/**\n * The response surface {@link RequestContext}'s helpers (`json` / `html` /\n * `sse` / `download` / `render` / `problem`) drive, instead of calling Express\n * `res` methods directly (spec §4.3). Sized so `express.Response` satisfies it\n * structurally — under the Express runtime the driver IS the Express response,\n * so there is no wrapping and no behavior change. Other runtimes (Fastify / h3)\n * provide a thin object implementing this over their native reply.\n *\n * The terminal methods return `RuntimeResponse` for fluent chaining (Express's\n * `Response` is assignable, since it is a superset).\n */\n/** True for the TypeError `JSON.stringify` throws on a bigint. */\nexport function isBigIntJsonError(err: unknown): boolean {\n return err instanceof TypeError && /BigInt/.test(err.message)\n}\n\n/**\n * `JSON.stringify` that writes a bigint as its decimal string instead of\n * throwing — database counts and `bigint` columns reach responses as bigints.\n * Plain `JSON.stringify` runs first, so a body without one costs nothing extra.\n */\nexport function stringifyJson(data: unknown): string {\n try {\n return JSON.stringify(data)\n } catch (err) {\n if (!isBigIntJsonError(err)) throw err\n return JSON.stringify(data, (_key, value) =>\n typeof value === 'bigint' ? value.toString() : value,\n )\n }\n}\n\nexport interface RuntimeResponse {\n status(code: number): RuntimeResponse\n json(data: unknown): RuntimeResponse\n send(data: unknown): RuntimeResponse\n type(contentType: string): RuntimeResponse\n setHeader(name: string, value: string | number | readonly string[]): unknown\n render(view: string, data?: Record<string, unknown>): unknown\n writeHead(statusCode: number, headers?: Record<string, string>): unknown\n flushHeaders(): unknown\n write(chunk: string | Buffer): boolean\n end(data?: unknown): unknown\n once(event: 'close' | 'drain', listener: () => void): unknown\n readonly headersSent: boolean\n}\n\n// ── Runtime-typed escape hatches (spec §4.3b) ─────────────────────────────\n\n/**\n * Per-runtime type map: the engine-native request / response / app types an\n * `HttpRuntime` exposes through `ctx.req.raw` / `ctx.res.raw` / `AdapterContext.app`.\n */\nexport interface RuntimeTypeMap {\n request: unknown\n response: unknown\n app: unknown\n}\n\n/** The Express runtime's native types — the default when nothing is augmented. */\nexport interface ExpressRuntimeTypes {\n request: ExpressRequest\n response: ExpressResponse\n app: Express\n}\n\n/**\n * Augmentable runtime registry. Empty by default; the `kick/runtime` typegen\n * plugin emits a `{ runtime: <RuntimeTypeMap> }` augmentation from the configured\n * runtime, which flips the engine-native escape-hatch types — `AdapterContext.app`,\n * `getRuntimeApp()`, and (once the driver layer lands) `ctx.req.raw` / `ctx.res.raw`\n * — to that runtime. Mirrors `KickDbRegister` / `KickEnv`, so the type story is\n * uniform with `kick/db` and env typing.\n *\n * @example\n * ```ts\n * // .kickjs/types/kick__runtime.d.ts — generated by typegen\n * declare module '@forinda/kickjs' {\n * interface KickRuntimeRegister {\n * runtime: import('@forinda/kickjs/fastify').FastifyRuntimeTypes\n * }\n * }\n * ```\n */\n\nexport interface KickRuntimeRegister {}\n\n/** The active runtime's type map — the augmented one, or Express by default. */\nexport type ActiveRuntime = KickRuntimeRegister extends { runtime: infer R extends RuntimeTypeMap }\n ? R\n : ExpressRuntimeTypes\n\n/** Options passed to {@link HttpRuntime.createApp}. */\nexport interface RuntimeAppOptions {\n /**\n * Express `trust proxy` setting (and the equivalent on other engines). The\n * function form is Express-specific; other runtimes interpret the simpler\n * shapes (or normalize X-Forwarded-For themselves — see spec §10 Q1).\n */\n trustProxy?: boolean | string | number | string[] | ((ip: string, hopIndex: number) => boolean)\n /**\n * When the app is mounted inside another handler (`kick dev`'s Vite server,\n * `app.handle(req, res, next)` in a host server), hand requests no route\n * matched back to it — `true`, the default — or answer them here with the\n * app's not-found handling — `false`. Express always answers.\n */\n notFoundFallthrough?: boolean\n}\n\n/** Where a connect middleware sits and what it scopes to. */\nexport interface UseConnectOptions {\n path?: string | RegExp | ReadonlyArray<string | RegExp>\n}\n\n/**\n * Optional engine capabilities. Absence means the corresponding `ctx` feature\n * raises a clear error on that runtime rather than failing obscurely.\n */\nexport interface RuntimeCapabilities {\n /** View-engine rendering (`ctx.render`). Express: true; Fastify/h3: false initially. */\n render: boolean\n /** File uploads (`ctx.file` / `@FileUpload`). All engines, different backends. */\n uploads: boolean\n /** Connect-style middleware. Express/Fastify(middie): true; h3: best-effort. */\n connectMiddleware: boolean\n /**\n * The engine parses request bodies itself (Fastify, h3). When true, the\n * Application skips its default `express.json()` so the body stream isn't\n * consumed twice (which would deadlock the engine's own parser). Express:\n * false — it relies on the `express.json()` connect middleware.\n */\n nativeBodyParsing: boolean\n /**\n * The engine's `nodeHandler` works behind `node:http2` (the HTTP/2\n * compatibility API), so `server: { http2: true }` may use it. Fastify/h3:\n * true. Express: false — it does not run on that layer. Absent = false, so\n * custom runtimes opt in explicitly.\n */\n http2?: boolean\n}\n\n/**\n * The engine driver. KickJS owns decorators → {@link RouteTable}, the\n * contributor pipeline, the `RequestContext` surface, and error mapping. The\n * runtime owns app/server creation, route materialization, connect-middleware\n * registration, static serving, and the terminal not-found / error handlers.\n *\n * `expressRuntime()` is the default and the M1 reference implementation.\n */\nexport interface HttpRuntime<TApp = unknown> {\n readonly name: 'express' | 'fastify' | 'h3' | (string & {})\n\n /** Create the engine app. Called once per Application (and per HMR rebuild). */\n createApp(options?: RuntimeAppOptions): TApp\n\n /**\n * Node-compatible request listener — the transport contract.\n * `http.createServer(handler)` in prod, Vite post-middleware in dev. MUST\n * invoke `next` (when given) instead of 404-ing if no route matched — the\n * Vite dev chain depends on fall-through.\n */\n nodeHandler(\n app: TApp,\n ): (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void\n\n /** Materialize the framework-built route table onto the engine. */\n mountRoutes(app: TApp, table: RouteTable): void\n\n /** Register a connect-style middleware (built-ins + adopter middleware). */\n useConnect(app: TApp, mw: ConnectMiddleware, opts?: UseConnectOptions): void\n\n /** Serve a static directory (swagger-ui assets, devtools SPA). */\n serveStatic(app: TApp, prefix: string, dir: string): void\n\n /** Terminal not-found handler. */\n setNotFound(app: TApp, mw: ConnectMiddleware): void\n\n /** Terminal error handler. */\n setErrorHandler(app: TApp, mw: ConnectMiddleware): void\n\n readonly capabilities: RuntimeCapabilities\n}\n\n/**\n * The supported surface adapters use to add HTTP routes, mounts, static dirs,\n * and middleware — engine-agnostic, in place of reaching for the raw Express\n * `app` (spec §4.4). Exposed as {@link AdapterContext.http}. Built by the\n * Application over the active {@link HttpRuntime}, so an adapter written against\n * this works under any runtime; the raw `app` escape hatch stays available for\n * the rare engine-specific need.\n */\nexport interface AdapterHttp {\n /** Register a single context-handler route (e.g. a docs or transport endpoint). */\n route(method: RouteMethod, path: string, handler: CtxHandler): void\n /** Mount a pre-built route table under a path prefix. */\n mount(prefix: string, routes: RouteEntry[]): void\n /** Serve a static directory under a path prefix. */\n serveStatic(prefix: string, dir: string): void\n /** Register a connect-style middleware (optionally path-scoped). */\n use(mw: ConnectMiddleware, opts?: UseConnectOptions): void\n}\n","/// <reference types=\"multer\" />\nimport type { Request, NextFunction } from 'express'\nimport { ROUTE_SLOT } from '../core/route-flag'\nimport {\n isBigIntJsonError,\n stringifyJson,\n type ActiveRuntime,\n type MatchedRoute,\n type RuntimeResponse,\n} from './runtime'\nimport {\n type ContextMetaKey,\n type ExecutionContext,\n type MetaValue,\n} from '../core/execution-context'\nimport { normalizeProblem, type ProblemDetails, type ValidationError } from '../core/errors'\nimport { MissingContextValueError } from '../core/context-errors'\nimport { requestStore } from './request-store'\nimport { waitUntil } from './background'\nimport { resolveClientIp, type ClientRequestLike } from './client-ip'\nimport type { Session } from './middleware/session'\nimport {\n parseQuery,\n type ParseQueryOptions,\n type QueryFieldConfig,\n type PaginatedResponse,\n type TypedParsedQuery,\n} from './query'\n\n/**\n * Input shape for the typed problem-detail convenience methods on\n * {@link ProblemSender} (`ctx.problem.notFound`, `.badRequest`, etc.) —\n * RFC 9457 details minus the `status`, which each method pre-fills.\n */\ntype ProblemInput = Omit<ProblemDetails, 'status'>\n\n/**\n * Callable response helper for RFC 9457 — Problem Details responses.\n * Returned by `ctx.problem`. Invoke directly for a fully-specified\n * payload, or use the named convenience methods to pre-fill `status` +\n * `title` for the common codes.\n *\n * Each call sets `Content-Type: application/problem+json` and serializes\n * the canonical shape (defaulting `type` to `about:blank` and `title` to\n * the IANA reason phrase per RFC 9457 §3.1.1 / §3.1.4).\n *\n * @example\n * ```ts\n * ctx.problem({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * detail: 'Your balance is 30, but that costs 50.',\n * balance: 30, // extension per §3.2\n * })\n *\n * ctx.problem.notFound({ detail: 'User abc not found' })\n * ctx.problem.validation(zodResult.error.issues)\n * ```\n */\ninterface ProblemSender {\n (input: ProblemDetails): RuntimeResponse\n /** 400 Bad Request — RFC 9457. */\n badRequest(input?: ProblemInput): RuntimeResponse\n /** 401 Unauthorized — RFC 9457. */\n unauthorized(input?: ProblemInput): RuntimeResponse\n /** 403 Forbidden — RFC 9457. */\n forbidden(input?: ProblemInput): RuntimeResponse\n /** 404 Not Found — RFC 9457. */\n notFound(input?: ProblemInput): RuntimeResponse\n /** 409 Conflict — RFC 9457. */\n conflict(input?: ProblemInput): RuntimeResponse\n /** 422 Unprocessable Entity — RFC 9457. */\n unprocessable(input?: ProblemInput): RuntimeResponse\n /** 429 Too Many Requests — RFC 9457. */\n tooManyRequests(input?: ProblemInput): RuntimeResponse\n /** 500 Internal Server Error — RFC 9457. */\n internal(input?: ProblemInput): RuntimeResponse\n /**\n * 422 with Zod issues serialized into the `errors` extension per\n * RFC 9457 §3.2's array convention. Pass `error.issues` from a Zod\n * `safeParse` failure or `error.issues` from a thrown ZodError.\n */\n validation(issues: any[], input?: ProblemInput): RuntimeResponse\n}\n\n/**\n * Recursively marks every property of `T` as `readonly`. Preserves\n * `Function`, `Date`, and `RegExp` unchanged, and converts `Map` / `Set`\n * to `ReadonlyMap` / `ReadonlySet` so their mutating methods (`set`,\n * `delete`, `add`, `clear`) are removed from the public surface.\n * Tuples keep their positional element types — only their per-element\n * mutability is locked.\n *\n * Exposed as a utility for adopters who want to opt into compile-time\n * sealing of their own shapes. **No longer applied to\n * `RequestContext.{body,params,query,headers,file,files}`** — that\n * broke TS narrowing / discriminated-union drilldowns on typed request\n * payloads. Runtime read-only enforcement now lives in\n * {@link makeReadOnlyProxy} (dev-only Proxy that warns on writes,\n * no-op in production).\n *\n * Type-only. No runtime cost. `DeepReadonly<any>` is `any` (TS\n * distributes conditional types over `any`).\n */\nexport type DeepReadonly<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends Date | RegExp\n ? T\n : T extends Map<infer K, infer V>\n ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>\n : T extends Set<infer V>\n ? ReadonlySet<DeepReadonly<V>>\n : T extends readonly (infer U)[]\n ? // Distinguish a regular array from a tuple. A regular `T[]` widens\n // its `length` to `number`; a tuple's `length` is a literal\n // (e.g. `2`). The literal branch maps over `keyof T` so each\n // positional slot keeps its own type — `DeepReadonly<[1, 2]>` →\n // `readonly [1, 2]`, not `readonly (1 | 2)[]`.\n number extends T['length']\n ? ReadonlyArray<DeepReadonly<U>>\n : { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T\n\n/**\n * Re-export the augmentable {@link ContextMeta} registry from core/.\n * Declared in `core/execution-context.ts` so non-HTTP transports\n * (WS/queue/cron, V2) share one declaration site. Apps still augment\n * via the canonical `declare module '@forinda/kickjs'` form.\n */\nexport type { ContextMeta } from '../core/execution-context'\n\n/**\n * Symbol key under which the per-request `AbortController` backing\n * `RequestContext.signal` is cached on the Express `req` object.\n * Shared across every `RequestContext` instance built for the same\n * request (router-builder constructs separate wrappers for\n * middleware, the contributor pipeline, and the main handler — they\n * all need the same signal).\n */\nconst signalControllerKey = Symbol.for('kickjs.requestContext.signalController')\n\n/**\n * Symbol key under which per-target read-only Proxy wrappers are\n * cached on the Express `req` object. Used by {@link makeReadOnlyProxy}\n * so repeat access of `ctx.body` / `ctx.params` / etc. returns the same\n * Proxy instance (cheap, and stable under `===` for adopters comparing\n * values across middleware).\n */\nconst readOnlyProxyCacheKey = Symbol.for('kickjs.requestContext.readOnlyProxyCache')\n\n/**\n * `true` iff `v` is a plain object (`{}` / `Object.create(null)`) or an\n * array — the two shapes JSON.parse / Express body-parser produce and\n * the only ones the read-only Proxy recurses into. Class instances,\n * `Date`, `RegExp`, `Map`, `Set`, `Buffer`, streams, and functions\n * pass through unwrapped so their method dispatch and `instanceof`\n * checks keep working.\n */\nfunction isPlainObjectOrArray(v: unknown): v is object {\n if (v === null || typeof v !== 'object') return false\n if (Array.isArray(v)) return true\n const proto = Object.getPrototypeOf(v)\n return proto === Object.prototype || proto === null\n}\n\n/**\n * In dev, wrap `target` in a `Proxy` whose `set` / `deleteProperty` /\n * `defineProperty` traps log a warning and otherwise leave the\n * underlying object untouched. A `get` trap recursively wraps nested\n * plain objects and arrays on access, so deep mutations\n * (`ctx.body.user.name = 'x'`, `ctx.files[0].fieldname = 'y'`,\n * `ctx.body.tags.push(...)`) all surface a warning — matching the\n * prior `DeepReadonly<T>` contract at runtime instead of at the type\n * level. In production (`process.env.NODE_ENV === 'production'`) the\n * proxy is bypassed entirely and `target` is returned as-is so the\n * hot path stays zero-cost.\n *\n * Why a Proxy instead of `DeepReadonly<T>` at the type level: the\n * recursive conditional type interfered with TS narrowing (notably\n * discriminated unions on `ctx.body`) and slowed type-checking on\n * deeply-nested Zod-inferred shapes. Compile-time sealing is gone;\n * runtime warning catches the same class of bug during dev and tests.\n *\n * Trap returns `true` so strict-mode call sites (which is all of them\n * — ES modules are strict) don't *throw* on assignment — they just\n * get a warning + the underlying value stays untouched. Adopters\n * discover the violation when the subsequent read returns the\n * original value, not the one they \"set\".\n *\n * Targets are cached per-`req` under {@link readOnlyProxyCacheKey} so\n * (1) repeat getter access returns the same proxy (`ctx.body ===\n * ctx.body`, `ctx.body.user === ctx.body.user`); and (2) cyclic refs\n * (`body.self = body`) terminate via the cache hit on the second\n * visit. Primitives, `null`, and non-plain values pass through.\n */\nfunction makeReadOnlyProxy<T>(target: T, req: object, label: string): T {\n if (process.env.NODE_ENV === 'production') return target\n if (!isPlainObjectOrArray(target)) return target\n\n type Cache = WeakMap<object, unknown>\n const reqObj = req as Record<symbol, Cache | undefined>\n let cache = reqObj[readOnlyProxyCacheKey]\n if (!cache) {\n cache = new WeakMap()\n reqObj[readOnlyProxyCacheKey] = cache\n }\n const targetObj = target as unknown as object\n const cached = cache.get(targetObj)\n if (cached) return cached as T\n\n const warnWrite = (verb: string, prop: PropertyKey) => {\n console.warn(\n `[kickjs] Attempted to ${verb} \\`ctx.${label}.${String(prop)}\\` — ` +\n `request data is read-only. ` +\n `Stash computed values via \\`ctx.set('key', value)\\` or a Context Contributor, ` +\n `or reach \\`ctx.req.${label}\\` if you genuinely need the raw mutable handle.`,\n )\n }\n\n const proxy = new Proxy(targetObj, {\n get(t, prop, receiver) {\n const value = Reflect.get(t, prop, receiver)\n // Recurse only into plain objects / arrays. Functions (incl.\n // array methods like `.push`) pass through with `receiver` as\n // their `this`, so internal `this[i] = x` writes still hit the\n // `set` trap and warn.\n if (isPlainObjectOrArray(value)) {\n return makeReadOnlyProxy(value, req, `${label}.${String(prop)}`)\n }\n return value\n },\n set(_t, prop) {\n warnWrite('assign', prop)\n return true\n },\n deleteProperty(_t, prop) {\n warnWrite('delete', prop)\n return true\n },\n defineProperty(_t, prop) {\n warnWrite('Object.defineProperty on', prop)\n return true\n },\n })\n cache.set(targetObj, proxy)\n return proxy as T\n}\n\n/**\n * The shape of a single route entry in `KickRoutes`. Generated by\n * `kick typegen` and consumed via the `Ctx<T>` helper. Each field is\n * `unknown` by default and is filled in as the typegen learns more\n * about the route (URL params from the path, body/query from validation\n * schemas, etc.).\n */\nexport interface RouteShape {\n params?: unknown\n body?: unknown\n query?: unknown\n response?: unknown\n /**\n * Union of context keys `kick typegen` proved are populated for this\n * route — the keys of every context contributor applied at method or\n * class level. Drives {@link RequestContext.require} narrowing, so\n * calling `ctx.require()` for a key this route doesn't carry is a\n * compile error instead of a request-time throw.\n *\n * `string` (the default) means \"not proven\", not \"no keys\". Typegen\n * emits `string` whenever it cannot see the full picture — a\n * contributor registered at module / adapter / bootstrap level, or an\n * unrecognised decorator that might bundle contributors of its own.\n * Narrowing an unprovable route would produce compile errors for keys\n * that really are present.\n */\n contextKeys?: string\n}\n\n/**\n * Unified request/response abstraction passed to every controller method.\n * Shields handlers from raw Express objects and provides convenience methods.\n *\n * Implements {@link ExecutionContext} so the transport-agnostic Context\n * Contributor pipeline (#107) can run against this concrete HTTP context\n * the same way it will run against future WS / queue / cron contexts.\n */\nexport class RequestContext<\n TBody = any,\n TParams = any,\n TQuery = any,\n /**\n * Context keys proven present for this route. Defaults to\n * {@link ContextMetaKey} — every key the project has registered — which\n * collapses to `string` when neither registry has been augmented, so a\n * handler typed as plain `RequestContext` keeps today's behaviour. That\n * default is also the deliberate escape hatch when typegen's view of a\n * route is incomplete.\n */\n TKeys extends ContextMetaKey = ContextMetaKey,\n> implements ExecutionContext<TKeys> {\n /**\n * Engine-agnostic response driver the response helpers write through. Under\n * the Express runtime it IS `res` (Express's Response satisfies\n * {@link RuntimeResponse} structurally); other runtimes pass a thin wrapper\n * over their native reply as the optional fourth argument.\n */\n private readonly _response: RuntimeResponse\n\n /**\n * The route this request matched: its method, path, controller, handler name\n * and resolved {@link MatchedRoute.flags}.\n *\n * Available to anything running inside the matched route — route middleware,\n * contributors, guards, the handler. Global middleware runs before route\n * matching, so it sees `undefined` there.\n *\n * ```ts\n * if (ctx.route?.flags.has('auth.public')) return next()\n * ```\n */\n get route(): MatchedRoute | undefined {\n return (this.req as unknown as Record<symbol, MatchedRoute | undefined>)?.[ROUTE_SLOT]\n }\n\n constructor(\n public readonly req: ActiveRuntime['request'],\n public readonly res: ActiveRuntime['response'],\n public readonly next: NextFunction,\n responseDriver?: RuntimeResponse,\n ) {\n // Under the Express runtime `res` IS a RuntimeResponse (structural); other\n // runtimes pass an explicit driver wrapping their native reply.\n this._response = responseDriver ?? (res as RuntimeResponse)\n }\n\n /**\n * Memo of the most recent {@link qs} call this request. The query\n * object is immutable for a request's lifetime, so a repeat call with\n * the same `(fieldConfig, options)` references returns the cached\n * parse instead of re-walking the filter/sort strings.\n */\n private _qsMemo?: { config: unknown; options: unknown; result: unknown }\n\n /** Memoized {@link problem} sender — built lazily on first access. */\n private _problem?: ProblemSender\n\n /**\n * Read-side accessor for the per-request metadata map.\n *\n * Returns the AsyncLocalStorage map (`requestStore.getStore().values`)\n * shared with the Context Contributor pipeline when an ALS frame is\n * active. Returns `null` outside any frame — read-side callers\n * (`ctx.get`, `ctx.user`, `ctx.tenantId`, `ctx.roles`) treat that as\n * \"no metadata\" and fall through to their `req.*` defaults. Reads\n * never throw because outside an ALS frame nobody could have written\n * via `ctx.set` anyway, so an empty answer is the truthful one.\n */\n private metadataReadOnly(): Map<string, any> | null {\n return (requestStore.getStore()?.values as Map<string, any> | undefined) ?? null\n }\n\n /**\n * Write-side accessor for the per-request metadata map.\n *\n * Returns the same map as {@link metadataReadOnly} when an ALS frame\n * is active. Throws otherwise — writing to a fallback map outside\n * the ALS frame would silently lose data because other call sites\n * (downstream contributors, request-logger, etc.) read from the\n * canonical store and would never see it. Mount\n * `requestScopeMiddleware()` (Application does this for you in\n * `'auto'` mode) or wrap your test setup in\n * `requestStore.run({...}, () => …)` to fix.\n */\n private metadataForWrite(): Map<string, any> {\n const store = requestStore.getStore()\n if (store) return store.values as Map<string, any>\n throw new Error(\n 'RequestContext.set called outside an AsyncLocalStorage frame. ' +\n 'Mount requestScopeMiddleware() before constructing RequestContext, ' +\n 'or wrap test setup in requestStore.run({ requestId, instances: new Map(), values: new Map() }, () => ...).',\n )\n }\n\n // ── Request Data ────────────────────────────────────────────────────\n\n /**\n * Parsed request body. Wrapped in a dev-only Proxy that warns when\n * adopters try to mutate Zod-validated payloads in place — compute\n * new values and stash them via `ctx.set('key', value)` or a Context\n * Contributor instead. Reach for `ctx.req.body` if you genuinely\n * need the raw, mutable Express handle (rare).\n *\n * In production the Proxy is bypassed and `this.req.body` is\n * returned as-is — no runtime cost on the hot path.\n */\n get body(): TBody {\n return makeReadOnlyProxy<TBody>(this.req.body, this.req, 'body')\n }\n\n /** Path parameters parsed from the route — read-only, see {@link body}. */\n get params(): TParams {\n return makeReadOnlyProxy<TParams>(this.req.params as TParams, this.req, 'params')\n }\n\n /** Parsed query string — read-only, see {@link body}. */\n get query(): TQuery {\n return makeReadOnlyProxy<TQuery>(this.req.query as TQuery, this.req, 'query')\n }\n\n /** Inbound HTTP headers — read-only, see {@link body}. */\n get headers(): Request['headers'] {\n return makeReadOnlyProxy<Request['headers']>(this.req.headers, this.req, 'headers')\n }\n\n get requestId(): string | undefined {\n return (this.req as any).requestId ?? (this.req.headers['x-request-id'] as string | undefined)\n }\n\n /**\n * `AbortSignal` that fires when the underlying HTTP request is\n * closed — either because the client disconnected before the\n * response was sent, or because Node's socket-close event ran\n * after a normal response. Multiple `RequestContext` wrappers for\n * the same request (router-builder constructs one per\n * middleware/contributor pipeline / main handler) share a single\n * `AbortController` cached on the underlying `req` object, so the\n * signal is stable across the request lifecycle.\n *\n * Thread it through anything that takes an `AbortSignal` so the\n * work cancels as soon as the client gives up — e.g. a kickjs-db\n * relational query:\n *\n * ```ts\n * @Get('/:id/full')\n * async show(ctx: RequestContext) {\n * const row = await this.repo.findFullById(ctx.params.id, ctx.signal)\n * if (!row) return ctx.notFound()\n * ctx.json(row)\n * }\n * ```\n *\n * The repo passes `signal` to `db.query.<table>.findUnique({ signal })`;\n * if the client disconnects mid-flight, the in-flight query rejects\n * with `RelationalQueryCancelledError` instead of consuming the\n * connection until completion.\n *\n * Implementation note — both `req.on('close')` and `res.on('close')`\n * are wired so the signal aborts regardless of whether the close\n * event lands on the request stream or the response stream first.\n * Subsequent fires are no-ops (`AbortController.abort()` is\n * idempotent).\n */\n get signal(): AbortSignal {\n const req = this.req as Request & { [signalControllerKey]?: AbortController }\n const cached = req[signalControllerKey]\n if (cached) return cached.signal\n\n const ctrl = new AbortController()\n req[signalControllerKey] = ctrl\n const abort = () => ctrl.abort('request closed')\n this.req.once('close', abort)\n this._response.once('close', abort)\n return ctrl.signal\n }\n\n /**\n * The request's session — set by the `session()` middleware, which must be\n * installed. Type your keys once by augmenting `SessionData`:\n *\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface SessionData { userId?: string }\n * }\n * ```\n */\n get session(): Session {\n return (this.req as { session?: Session }).session as Session\n }\n\n /**\n * The authenticated user set by AuthAdapter.\n * Reads from the per-request metadata store first (`ctx.set('user', ...)`),\n * then falls back to `req.user` (set directly by AuthAdapter middleware).\n *\n * Extend the `AuthUser` interface via module augmentation in your own auth\n * module — see the BYO Auth recipe in the docs\n * to get full typing.\n *\n * @example\n * ```ts\n * @Get('/me')\n * @Authenticated()\n * getProfile(ctx: RequestContext) {\n * const user = ctx.user\n * if (!user) return ctx.notFound()\n * return ctx.json({ user })\n * }\n * ```\n */\n get user(): MetaValue<'user', Record<string, any>> | undefined {\n return this.metadataReadOnly()?.get('user') ?? (this.req as any).user\n }\n\n /**\n * Tenant identifier for multi-tenant apps. Reads `user.tenantId` —\n * populated either by `AuthAdapter.testMode({ tenantId })`, a tenant\n * resolver middleware, or manually on the strategy's `mapPayload`.\n *\n * Augment `AuthUser` (or `ContextMeta['user']`) to widen the return\n * type if your user shape guarantees a non-`string | undefined` id.\n */\n get tenantId(): string | undefined {\n const u = this.user as Record<string, any> | undefined\n return u?.tenantId\n }\n\n /**\n * Effective role list for the current user. Prefers `user.tenantRoles`\n * (populated by `AuthAdapterOptions.roleResolver` under a tenant) and\n * falls back to `user.roles`. Returns an empty array when the user is\n * absent or the shape has neither field.\n */\n get roles(): string[] {\n const u = this.user as Record<string, any> | undefined\n return u?.tenantRoles ?? u?.roles ?? []\n }\n\n // ── Query String Parsing ───────────────────────────────────────────\n\n /**\n * Parse the request query string into structured filters, sort, pagination, and search.\n * Pass the result to an ORM query builder adapter (Drizzle, Prisma, Sequelize, etc.).\n *\n * Generic over the field config: when passed an inline literal (or\n * `as const`), `filters[].field` and `sort[].field` are narrowed to\n * the exact whitelisted field names. Pass nothing or a non-literal\n * config and you get the loose `string` fallback.\n *\n * @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields\n * @param options - Limits (`maxLimit`, `defaultLimit`, `maxSearchLength`) and an\n * `onReject` hook. By default a rejected/over-broad field is logged via\n * `console.warn` with the request id; pass an explicit `onReject` to override\n * (e.g. throw to return a 400), or `onReject: () => {}` to silence.\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * } as const)\n * parsed.filters[0].field // typed as 'status' | 'priority'\n * parsed.sort[0].field // typed as 'createdAt' | 'title'\n * }\n * ```\n */\n qs<TConfig extends QueryFieldConfig | undefined = undefined>(\n fieldConfig?: TConfig,\n options?: ParseQueryOptions,\n ): TypedParsedQuery<TConfig> {\n // Cheap per-request memo — repeat calls with the same args (the\n // common controller pattern) skip re-parsing. `req.query` is fixed\n // for the request's lifetime, so caching is always sound.\n const memo = this._qsMemo\n if (memo && memo.config === fieldConfig && memo.options === options) {\n return memo.result as TypedParsedQuery<TConfig>\n }\n const effective: ParseQueryOptions = {\n ...options,\n onReject:\n options?.onReject ??\n ((rejection) => {\n const id = this.requestId ? `[${this.requestId}] ` : ''\n console.warn(\n `${id}query ${rejection.kind} '${rejection.field}' rejected (${rejection.reason})`,\n )\n }),\n }\n const result = parseQuery<TConfig>(\n this.req.query as Record<string, any>,\n fieldConfig,\n effective,\n )\n this._qsMemo = { config: fieldConfig, options, result }\n return result\n }\n\n // ── File Uploads ────────────────────────────────────────────────────\n\n /**\n * Single uploaded file (requires `@FileUpload({ mode: 'single' })`).\n * Wrapped in a dev-only Proxy that warns on mutation — see {@link body}.\n * File metadata is set by the upload middleware and shouldn't be\n * mutated downstream.\n */\n get file(): Express.Multer.File | undefined {\n const f = this.req.file\n return f ? makeReadOnlyProxy<Express.Multer.File>(f, this.req, 'file') : undefined\n }\n\n /**\n * Array of uploaded files (requires `@FileUpload({ mode: 'array' })`).\n * The array (and each entry in dev) is wrapped in a read-only Proxy\n * that warns on `push`/`pop` and per-file mutation. Use\n * `ctx.req.files` if you genuinely need the mutable Express handle.\n */\n get files(): Express.Multer.File[] | undefined {\n // multer's `Request['files']` is a union — array (mode: 'array' /\n // 'any') or `{ [fieldname]: File[] }` (mode: 'fields'). The\n // `@FileUpload({ mode: 'array' })` decorator only sets the array\n // shape, but `Request['files']` is the broad union. Narrow back to\n // `File[]` here for the common-case getter; adopters using\n // `mode: 'fields'` can read `ctx.req.files` directly for the\n // dictionary shape.\n const f = this.req.files\n if (!Array.isArray(f)) return undefined\n return makeReadOnlyProxy<Express.Multer.File[]>(f, this.req, 'files')\n }\n\n // ── Metadata Store ──────────────────────────────────────────────────\n\n /**\n * Read a value from the per-request metadata store.\n *\n * The key is constrained to {@link ContextMetaKey} — the keys registered in\n * `ContextMeta` / `ContextKeys` — so a typo is a compile error rather than a\n * permanent `undefined`. Projects that have augmented neither registry get\n * `string`, exactly as before.\n *\n * Constrained by `ContextMetaKey` and deliberately NOT by `TKeys`: \"does this\n * key exist in the app\" is a different question from \"does this route carry\n * it\", and narrowing by the latter would turn an incomplete typegen view into\n * false errors for keys written by middleware. Presence is `require`'s job;\n * this always returns `| undefined`.\n *\n * When `ContextMeta` has a matching key the return type is inferred\n * automatically. For ad-hoc keys, pass a generic: `ctx.get<MyType>('custom')`.\n */\n get<K extends ContextMetaKey>(key: K): MetaValue<K> | undefined {\n return this.metadataReadOnly()?.get(key) as MetaValue<K> | undefined\n }\n\n /**\n * Read a value that a contributor is expected to have produced, and\n * throw {@link MissingContextValueError} when it hasn't.\n *\n * Use this instead of `ctx.get(key)!` anywhere the value is a\n * precondition rather than an optional extra — permissions, tenants,\n * the resolved subject of the route. The non-null assertion compiles\n * whether or not the producing contributor is actually applied to the\n * route, so a dropped `@LoadTenant` turns into `undefined` flowing\n * silently into an auth check. `require()` fails loudly at that point\n * instead, naming the key.\n *\n * ```ts\n * // before — compiles even if @OperatorPerm was never applied\n * const perm = ctx.get('operatorPerm')!\n *\n * // after — throws MissingContextValueError naming the key + route\n * const perm = ctx.require('operatorPerm')\n * ```\n *\n * When `TKeys` is narrowed — which it is for a handler typed as\n * `Ctx<KickRoutes…>`, from typegen's per-route `contextKeys` — asking\n * for a key the route doesn't carry is a compile error, so a dropped\n * decorator fails the build. `TKeys` stays `string` (no narrowing) for\n * a plain `RequestContext` and for any route typegen couldn't prove\n * complete; there this is a runtime guarantee only.\n * See `architecture.md` §20.14.\n *\n * `null` counts as present; only `undefined` (or a missing entry)\n * throws. A contributor that deliberately resolves to `null` is\n * expressing \"looked, found nothing\", which is a real value — which\n * is why the return type is `Exclude<…, undefined>` and NOT\n * `NonNullable<…>`. The latter would strip `null` from the type while\n * the runtime still hands it back, so a caller would be told a value\n * is non-null and then get `null`.\n */\n require<K extends TKeys>(key: K): Exclude<MetaValue<K>, undefined> {\n const value = this.metadataReadOnly()?.get(key)\n if (value === undefined) {\n throw new MissingContextValueError(key, this.routeLabel())\n }\n return value as Exclude<MetaValue<K>, undefined>\n }\n\n /**\n * Best-effort `\"GET /path\"` label for error messages. Runtime-agnostic:\n * express/fastify requests and web-standard `Request` all expose\n * `method` + `url`, but none of them are guaranteed here, so every\n * access is defensive and the whole thing is optional.\n */\n private routeLabel(): string | undefined {\n const req = this.req as { method?: unknown; url?: unknown } | undefined\n const method = typeof req?.method === 'string' ? req.method : undefined\n const url = typeof req?.url === 'string' ? req.url : undefined\n if (method && url) return `${method} ${url}`\n return method ?? url\n }\n\n /**\n * Write a value to the per-request metadata store. Throws when no\n * AsyncLocalStorage frame is active — see {@link metadataForWrite}\n * for why a silent fallback is the wrong default.\n */\n set<K extends ContextMetaKey>(key: K, value: MetaValue<K>): void {\n this.metadataForWrite().set(key, value)\n }\n\n // ── Response Helpers ────────────────────────────────────────────────\n\n /**\n * Set a response header. Runtime-neutral — works on every engine and on\n * the web/edge entry. No-op guards are the runtime's job (headers set\n * after the response settles are ignored or throw per engine).\n */\n setHeader(name: string, value: string | number | readonly string[]): this {\n this._response.setHeader(name, value)\n return this\n }\n\n json(data: any, status = 200) {\n return this.sendJson(status, data)\n }\n\n created(data: any) {\n return this.sendJson(201, data)\n }\n\n private sendJson(status: number, data: unknown): RuntimeResponse {\n try {\n return this._response.status(status).json(data)\n } catch (err) {\n // Express serialises inside `res.json` and throws on a bigint before\n // anything is written; the other runtimes' drivers use stringifyJson.\n if (!isBigIntJsonError(err) || this._response.headersSent) throw err\n return this._response.status(status).json(JSON.parse(stringifyJson(data)))\n }\n }\n\n noContent() {\n return this._response.status(204).end()\n }\n\n /**\n * @deprecated Prefer {@link ProblemSender.notFound | ctx.problem.notFound()},\n * which emits an RFC 9457 — Problem Details response. This helper stays\n * for backward compatibility and will not be removed.\n */\n notFound(message = 'Not Found') {\n return this._response.status(404).json({ message })\n }\n\n /**\n * @deprecated Prefer {@link ProblemSender.badRequest | ctx.problem.badRequest()},\n * which emits an RFC 9457 — Problem Details response. This helper stays\n * for backward compatibility and will not be removed.\n */\n badRequest(message: string) {\n return this._response.status(400).json({ message })\n }\n\n // ── RFC 9457 Problem Details ───────────────────────────────────────\n\n /**\n * RFC 9457 — Problem Details for HTTP APIs response helper.\n * https://datatracker.ietf.org/doc/html/rfc9457\n *\n * `ctx.problem(...)` sends a canonical problem-detail response with\n * `Content-Type: application/problem+json`. The convenience methods\n * (`ctx.problem.notFound`, `.badRequest`, etc.) pre-fill `status` +\n * `title` for the common codes.\n *\n * `type` defaults to `about:blank` (§3.1.1); `title` defaults to the\n * IANA reason phrase for `status` (§3.1.4). Extension members per §3.2\n * pass through unchanged.\n *\n * @example\n * ```ts\n * ctx.problem({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * detail: 'Your balance is 30, but that costs 50.',\n * instance: `/account/${ctx.user.id}/messages/${ctx.params.id}`,\n * balance: 30,\n * })\n *\n * ctx.problem.notFound({ detail: 'User abc not found' })\n * ctx.problem.validation(zodResult.error.issues)\n * ```\n *\n * @see {@link ProblemException} for the exception-throwing equivalent\n * (use it from services where `ctx` isn't in scope).\n */\n get problem(): ProblemSender {\n // Memoized — building the sender allocates ~10 closures, so pay that only\n // on the first access per context instead of per `ctx.problem.*` call.\n if (this._problem) return this._problem\n const send = (input: ProblemDetails): RuntimeResponse => this.sendProblem(input)\n send.badRequest = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 400, ...input })\n send.unauthorized = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 401, ...input })\n send.forbidden = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 403, ...input })\n send.notFound = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 404, ...input })\n send.conflict = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 409, ...input })\n send.unprocessable = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 422, ...input })\n send.tooManyRequests = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 429, ...input })\n send.internal = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 500, ...input })\n send.validation = (issues: any[], input: ProblemInput = {}): RuntimeResponse => {\n const errors: ValidationError[] = (issues || []).map((issue: any) => ({\n field: issue.path?.join?.('.') ?? '',\n message: issue.message ?? '',\n code: issue.code,\n }))\n return this.sendProblem({\n status: 422,\n detail: issues?.[0]?.message,\n ...input,\n errors,\n })\n }\n this._problem = send as ProblemSender\n return this._problem\n }\n\n private sendProblem(input: ProblemDetails): RuntimeResponse {\n const body = normalizeProblem(input)\n this._response.setHeader('Content-Type', 'application/problem+json')\n return this._response.status(body.status).json(body)\n }\n\n /**\n * Send an HTML response.\n *\n * Passes the full MIME type rather than the `'html'` shorthand. The\n * shorthand is an Express affordance — `res.type()` runs it through a MIME\n * lookup — but the Fastify and h3 drivers forward the string to their own\n * `type()` / `setHeader()` verbatim, so `'html'` reached the wire as a\n * malformed `Content-Type: html` on those engines. Fastify 5.10 started\n * normalising that invalid value to `text/plain`, which is what surfaced it.\n *\n * Express produces `text/html; charset=utf-8` for `.type('html')`, so\n * spelling it out here is byte-identical there and correct everywhere else.\n */\n /**\n * The client's IP address, or `undefined` when it cannot be determined.\n *\n * Prefers the value the runtime computed (Express derives `req.ip` from the\n * app's `trust proxy` setting; Fastify from `trustProxy`), because raw\n * forwarded headers are client-SPOOFABLE on deployments that do not\n * normalize them. The header fallbacks exist for runtimes with no `ip`\n * field at all — notably the web/edge entry.\n *\n * `ctx.req.ip` is Express-only, so reaching for it directly broke on\n * Fastify and h3.\n */\n get ip(): string | undefined {\n return resolveClientIp(this.req as ClientRequestLike)\n }\n\n /**\n * Redirect the client. Works on every runtime.\n *\n * Every engine spells this differently — Express and Fastify have\n * `res.redirect()`, h3 has `sendRedirect(event, location, code)` — and none\n * of them is shared, so this writes the status and `Location` header through\n * the runtime response surface, which all four expose.\n *\n * The response carries no body. Express sends a short text line and h3 an\n * HTML meta-refresh fallback; a bare `Location` with a 3xx is sufficient per\n * RFC 9110, and interpolating the URL into an HTML body would add an\n * injection surface to a value this method already warns is dangerous.\n *\n * ::: warning\n * Never pass an unvalidated user-supplied `url`. An attacker-controlled\n * destination turns this into an open redirect, which is a common phishing\n * primitive. Allow-list the target or keep it relative.\n * :::\n */\n redirect(url: string, status = 302) {\n this._response.status(status).setHeader('location', url)\n return this._response.end()\n }\n\n html(content: string, status = 200) {\n return this._response.status(status).type('text/html; charset=utf-8').send(content)\n }\n\n /**\n * Send a body with its content type — XML, plain text, CSV, an image.\n * Runtime-neutral, like `json()` and `html()`. `type` defaults to\n * `text/plain; charset=utf-8`.\n *\n * @example\n * ```ts\n * ctx.send(sitemapXml, { type: 'application/xml' })\n * ctx.send(csv, { type: 'text/csv; charset=utf-8', status: 200 })\n * ```\n */\n send(body: string | Uint8Array, options: { type?: string; status?: number } = {}) {\n return this._response\n .status(options.status ?? 200)\n .type(options.type ?? 'text/plain; charset=utf-8')\n .send(body)\n }\n\n /**\n * Send `buffer` as a file download named `filename`. The name is written\n * per RFC 6266 — an ASCII fallback plus `filename*` in UTF-8 — so a name\n * from an upload (quotes, line breaks, non-Latin text) can't break the header.\n */\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this._response.setHeader('Content-Disposition', contentDisposition(filename))\n this._response.setHeader('Content-Type', contentType)\n return this._response.send(buffer)\n }\n\n /**\n * Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).\n * Requires a ViewAdapter to be configured in bootstrap().\n *\n * @param template - Template name (without extension, relative to viewsDir)\n * @param data - Data to pass to the template\n *\n * @example\n * ```ts\n * ctx.render('dashboard', { user, title: 'Dashboard' })\n * ctx.render('emails/welcome', { name: 'Alice' })\n * ```\n */\n render(template: string, data: Record<string, any> = {}) {\n return this._response.render(template, data)\n }\n\n /**\n * Parse query params and return a standardized paginated response.\n * Calls `ctx.qs()` internally, then wraps your data with pagination meta.\n *\n * @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`\n * @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * return ctx.paginate(\n * async (parsed) => {\n * const data = await db.select().from(users)\n * .where(query.where).limit(parsed.pagination.limit)\n * .offset(parsed.pagination.offset).all()\n * const total = await db.select({ count: count() }).from(users).get()\n * return { data, total: total?.count ?? 0 }\n * },\n * { filterable: ['name', 'role'], sortable: ['createdAt'] },\n * )\n * }\n * ```\n */\n async paginate<T, TConfig extends QueryFieldConfig | undefined = undefined>(\n fetcher: (parsed: TypedParsedQuery<TConfig>) => Promise<{ data: T[]; total: number }>,\n fieldConfig?: TConfig,\n ): Promise<PaginatedResponse<T>> {\n const parsed = this.qs<TConfig>(fieldConfig)\n const { data, total } = await fetcher(parsed)\n const { page, limit } = parsed.pagination\n const totalPages = Math.ceil(total / limit) || 1\n\n const response: PaginatedResponse<T> = {\n data,\n meta: {\n page,\n limit,\n total,\n totalPages,\n hasNext: page < totalPages,\n hasPrev: page > 1,\n },\n }\n\n this.json(response)\n // Sends AND returns the payload. Returning `this.json(...)` handed back\n // the engine's `RuntimeResponse`, so the documented usage —\n // `return ctx.paginate(...)` — emitted `response: RuntimeResponse` into\n // `KickRoutes`, and the typed client offered `.status()` / `.setHeader()`\n // where the caller expected `data` and `meta`.\n //\n // Returning the payload rather than tagging the response reuses the\n // existing return-value inference (the same path `return user` and\n // `return reply(201, user)` take) instead of adding a second mechanism\n // beside `reply`. Sending here is kept for handlers that call `paginate`\n // without returning it; the runtimes only auto-send a returned value when\n // nothing was written (`if (!res.headersSent)`), so there is no double\n // send.\n return response\n }\n\n // ── Server-Sent Events ──────────────────────────────────────────────\n\n /**\n * Start an SSE (Server-Sent Events) stream.\n * Sets the correct headers and returns helpers to send events.\n *\n * Pass a payload type to get typed sends — and `return sse` from the\n * handler so `kick typegen` carries the event type into\n * `KickRoutes.Api`, where `@forinda/kickjs-client`'s `api.stream()`\n * picks it up (the returned handler is ignored at runtime — the\n * response is already streaming).\n *\n * @example\n * ```ts\n * interface Tick { time: string }\n *\n * @Get('/events')\n * async stream(ctx: RequestContext) {\n * const sse = ctx.sse<Tick>()\n *\n * const interval = setInterval(() => {\n * sse.send({ time: new Date().toISOString() }, 'tick')\n * }, 1000)\n *\n * sse.onClose(() => clearInterval(interval))\n * return sse // typed as SseHandler<Tick> → response inference\n * }\n * ```\n */\n /**\n * Send a web-standard `Response` — status, headers (every `Set-Cookie`\n * kept) and body, streamed chunk by chunk — on any runtime. For handing off\n * to libraries that speak `Request`/`Response`, such as the MCP SDK's\n * web-standard transport.\n *\n * Resolves once the body is fully written. Stops reading the body when the\n * client disconnects.\n *\n * @example\n * ```ts\n * @Post('/mcp')\n * async mcp(ctx: RequestContext) {\n * await ctx.sendResponse(await transport.handleRequest(webRequest))\n * }\n * ```\n */\n async sendResponse(response: Response): Promise<void> {\n const headers: Record<string, string | string[]> = {}\n response.headers.forEach((value, name) => {\n if (name !== 'set-cookie') headers[name] = value\n })\n const cookies = response.headers.getSetCookie()\n if (cookies.length > 0) headers['set-cookie'] = cookies\n\n if (!response.body) {\n // Not writeHead() + end(): on the web driver writeHead switches to a\n // streamed Response, which a 204/304 cannot be, and on Fastify end()\n // bypasses the headers set on the reply. send() is each runtime's own\n // buffered reply path.\n this._response.status(response.status)\n for (const [name, value] of Object.entries(headers)) this._response.setHeader(name, value)\n this._response.send('')\n return\n }\n this._response.writeHead(response.status, headers as Record<string, string>)\n // Send the headers now: a stream whose first chunk comes later (SSE)\n // would otherwise leave the client waiting to see the response open.\n this._response.flushHeaders()\n const reader = response.body.getReader()\n const stop = () => {\n reader.cancel().catch(() => {})\n }\n this.signal.addEventListener('abort', stop, { once: true })\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n // Honour backpressure: on a slow client, wait for the socket to drain\n // (or the client to go away) before reading more of the body.\n if (this._response.write(Buffer.from(value)) === false && !this.signal.aborted) {\n await new Promise<void>((resolve) => {\n const resume = () => {\n this.signal.removeEventListener('abort', resume)\n resolve()\n }\n this._response.once('drain', resume)\n this.signal.addEventListener('abort', resume, { once: true })\n })\n }\n }\n } finally {\n this.signal.removeEventListener('abort', stop)\n this._response.end()\n }\n }\n\n /**\n * Keep `promise` running after the response is sent — an audit log, an\n * email, an analytics call. The Node server's `shutdown()` waits for it;\n * `createHandler()` and the web entry hand it to the platform's\n * `waitUntil` when you pass the platform context in. A rejection is\n * logged, never unhandled. Same as the standalone `waitUntil()` export.\n */\n waitUntil(promise: Promise<unknown>): void {\n waitUntil(promise)\n }\n\n sse<T = unknown>(): SseHandler<T> {\n this._response.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n })\n this._response.flushHeaders()\n\n const closeCallbacks: Array<() => void> = []\n\n this.req.on('close', () => {\n for (const cb of closeCallbacks) cb()\n })\n\n return {\n /** Send an SSE event with optional event name and id */\n send: (data: T, event?: string, id?: string) => {\n if (id) this._response.write(`id: ${id}\\n`)\n if (event) this._response.write(`event: ${event}\\n`)\n this._response.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this._response.write(`: ${text}\\n\\n`)\n },\n /** Register a callback when the client disconnects */\n onClose: (fn: () => void) => {\n closeCallbacks.push(fn)\n },\n /** End the SSE stream */\n close: () => {\n this._response.end()\n },\n }\n }\n}\n\n/**\n * The handle {@link RequestContext.sse} returns. The optional phantom\n * `__sse` property carries the event payload type STRUCTURALLY — never set\n * at runtime — so `kick typegen`'s response inference (handler `return sse`)\n * flows it into `KickRoutes.Api`, and `@forinda/kickjs-client` detects SSE\n * routes without importing any server types.\n */\nexport interface SseHandler<T = unknown> {\n /** Phantom event-payload type marker — never assigned at runtime. */\n readonly __sse?: T\n /** Send an SSE event with optional event name and id. */\n send(data: T, event?: string, id?: string): void\n /** Send a comment line (keeps the connection alive through proxies). */\n comment(text: string): void\n /** Register a callback for client disconnect. */\n onClose(fn: () => void): void\n /** End the SSE stream. */\n close(): void\n}\n\n/**\n * Short alias for `RequestContext` parameterised by a route shape.\n *\n * Use with `kick typegen`'s generated `KickRoutes` namespace for\n * fully-typed `ctx.params`, `ctx.body`, and `ctx.query`. Metadata\n * typing (`ctx.get()`, `ctx.set()`, `ctx.user`) comes from\n * `ContextMeta` augmentation and works on all `RequestContext` instances.\n *\n * @example\n * ```ts\n * @Get('/:id')\n * getUser(ctx: Ctx<KickRoutes.UserController['getUser']>) {\n * ctx.params.id // typed from RouteShape\n * ctx.body // typed from RouteShape\n * ctx.user // typed from ContextMeta\n * ctx.get('tenant') // typed from ContextMeta\n * }\n * ```\n */\nexport type Ctx<TRoute extends RouteShape = RouteShape> = RequestContext<\n TRoute extends { body: infer B } ? B : any,\n TRoute extends { params: infer P } ? P : any,\n TRoute extends { query: infer Q } ? Q : any,\n // Falls back to `string` (no narrowing) whenever typegen couldn't prove\n // the route's full contributor set — see `RouteShape.contextKeys`.\n TRoute extends { contextKeys: infer K }\n ? K extends ContextMetaKey\n ? K\n : ContextMetaKey\n : ContextMetaKey\n>\n\n/**\n * Global ambient registry of typed routes, populated by `kick typegen`.\n *\n * Interfaces inside `KickRoutes` correspond to controller classes (each\n * property one route method, conforming to `RouteShape`) — plus the\n * reserved `KickRoutes.Api`: a flat `'METHOD /mounted/path'` map over the\n * same shapes, consumed by `@forinda/kickjs-client`. Don't name a\n * controller class `Api` — it would declaration-merge into that map.\n *\n * Empty by default — declarations come from `.kickjs/types/kick__routes.ts`\n * generated alongside the rest of the typegen output.\n */\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace KickRoutes {}\n}\n\n/** `attachment; filename=\"…\"; filename*=UTF-8''…` for any file name (RFC 6266 / RFC 8187). */\nexport function contentDisposition(filename: string): string {\n const fallback = filename.replace(/[^\\x20-\\x7e]|[\"\\\\]/g, '_')\n const encoded = encodeURIComponent(filename).replace(\n /['()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n )\n return `attachment; filename=\"${fallback}\"; filename*=UTF-8''${encoded}`\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v10.0.0
2
+ * @forinda/kickjs v10.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -10,4 +10,4 @@
10
10
  */
11
11
  var MissingContributorError=class extends Error{key;dependent;route;dependentDefinedAt;constructor(key,dependent,route,dependentDefinedAt){let where=route?` on route ${route}`:``,declaredAt=formatDeclaredAt(dependentDefinedAt);super(`Missing context contributor '${key}' required by '${dependent}'${where}${declaredAt}`),this.name=`MissingContributorError`,this.key=key,this.dependent=dependent,this.route=route,this.dependentDefinedAt=dependentDefinedAt}},MissingContextValueError=class extends Error{key;route;constructor(key,route){let where=route?` on ${route}`:``;super(`No context value for '${key}'${where}\n Nothing wrote this key before the handler ran. Either the contributor\n that produces it isn't applied to this route, or it ran and resolved\n to undefined (check \`optional: true\` contributors).`),this.name=`MissingContextValueError`,this.key=key,this.route=route}};function formatDeclaredAt(stack){if(!stack)return``;let lines=stack.split(`
12
12
  `);for(let line of lines){let trimmed=line.trim();if(trimmed.startsWith(`at `)&&!trimmed.includes(`context-decorator.`)&&!trimmed.includes(`context-errors.`))return`\n declared at ${trimmed.replace(/^at /,``)}`}return``}var ContributorCycleError=class extends Error{cycle;route;constructor(cycle,route){let where=route?` on route ${route}`:``;super(`Cycle in context contributors${where}: ${cycle.join(` → `)}`),this.name=`ContributorCycleError`,this.cycle=Object.freeze([...cycle]),this.route=route}},DuplicateContributorError=class extends Error{key;sources;constructor(key,sources){super(`Duplicate context contributor for key '${key}' at the same precedence level. Sources: ${sources.join(`, `)}`),this.name=`DuplicateContributorError`,this.key=key,this.sources=Object.freeze([...sources])}};export{MissingContributorError as i,DuplicateContributorError as n,MissingContextValueError as r,ContributorCycleError as t};
13
- //# sourceMappingURL=context-errors-BUdMBVWO.mjs.map
13
+ //# sourceMappingURL=context-errors-7C5TBYn7.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-errors-BUdMBVWO.mjs","names":[],"sources":["../src/core/context-errors.ts"],"sourcesContent":["/**\n * Errors thrown by the Context Contributor pipeline.\n *\n * `MissingContributorError`, `ContributorCycleError`, and\n * `DuplicateContributorError` are startup-time errors raised during\n * pipeline build (topo-sort + validation), not per-request — a\n * misconfigured app fails to boot rather than fails a request. Per the\n * locked design in `architecture.md` §20.4, the pipeline is validated\n * once when each route is mounted and the resolved order is cached on\n * the route.\n *\n * `MissingContextValueError` is the one request-time error here: it is\n * raised by `ctx.require(key)` when a value a handler declared it needs\n * is absent. See its doc comment for why that case can't be caught at\n * startup today.\n */\n\n/**\n * A contributor declared `dependsOn: ['<key>']` but no other contributor\n * (at any precedence level) produces that key for the same route.\n *\n * @example\n * ```text\n * MissingContributorError: Missing context contributor 'tenant' required by 'project' on route GET /projects/:id\n * declared at src/contributors/load-project.ts:42:18\n * ```\n */\nexport class MissingContributorError extends Error {\n readonly key: string\n readonly dependent: string\n readonly route?: string\n readonly dependentDefinedAt?: string\n\n constructor(key: string, dependent: string, route?: string, dependentDefinedAt?: string) {\n const where = route ? ` on route ${route}` : ''\n const declaredAt = formatDeclaredAt(dependentDefinedAt)\n super(`Missing context contributor '${key}' required by '${dependent}'${where}${declaredAt}`)\n this.name = 'MissingContributorError'\n this.key = key\n this.dependent = dependent\n this.route = route\n this.dependentDefinedAt = dependentDefinedAt\n }\n}\n\n/**\n * `ctx.require(key)` found no value under `key` in the per-request\n * metadata store.\n *\n * Thrown at request time, by design. `ctx.get(key)` returns\n * `T | undefined` for every key, so the usual workaround is a\n * non-null assertion — `ctx.get('tenant')!` — which compiles whether or\n * not the route actually carries the contributor that produces it. On\n * an authorization value that is the worst possible thing to leave\n * untypeable: dropping the decorator produces no diagnostic anywhere,\n * and the handler reads `undefined` as though it were a real value.\n *\n * `ctx.require()` converts that silent hole into a loud, named failure\n * that points at the missing contributor.\n *\n * A handler typed with a generated route type (`Ctx<KickRoutes…>`) also\n * gets compile-time narrowing, so a dropped decorator fails `tsc` rather\n * than reaching this error at all. This error remains the backstop for\n * routes typegen couldn't prove (module/adapter/bootstrap registrations),\n * handlers typed as plain `RequestContext`, and contributors that ran but\n * resolved to `undefined`. See `architecture.md` §20.14.\n *\n * @example\n * ```text\n * MissingContextValueError: No context value for 'tenantPerm' on GET /projects/:id\n * Nothing wrote this key before the handler ran. Either the contributor\n * that produces it isn't applied to this route, or it ran and resolved\n * to undefined (check `optional: true` contributors).\n * ```\n */\nexport class MissingContextValueError extends Error {\n readonly key: string\n readonly route?: string\n\n constructor(key: string, route?: string) {\n const where = route ? ` on ${route}` : ''\n super(\n `No context value for '${key}'${where}\\n` +\n ` Nothing wrote this key before the handler ran. Either the contributor\\n` +\n ` that produces it isn't applied to this route, or it ran and resolved\\n` +\n ` to undefined (check \\`optional: true\\` contributors).`,\n )\n this.name = 'MissingContextValueError'\n this.key = key\n this.route = route\n }\n}\n\n/**\n * Slice the first non-framework frame off a captured `Error.stack`\n * and format it as `\\n declared at <frame>` for inclusion in\n * boot-time error messages. Returns `''` when no useful frame is\n * available (hand-rolled registrations, missing stacks).\n */\nfunction formatDeclaredAt(stack: string | undefined): string {\n if (!stack) return ''\n const lines = stack.split('\\n')\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('at ')) continue\n // Skip frames inside this file or the decorator factory — the\n // adopter's call site is the next frame after those.\n if (trimmed.includes('context-decorator.')) continue\n if (trimmed.includes('context-errors.')) continue\n return `\\n declared at ${trimmed.replace(/^at /, '')}`\n }\n return ''\n}\n\n/**\n * Topo-sort detected a cycle in `dependsOn` declarations. The `cycle` field\n * lists the keys in the order they form the loop, with the first key\n * repeated at the end so the path reads naturally.\n *\n * @example\n * ```text\n * ContributorCycleError: Cycle in context contributors on route GET /projects/:id: tenant → project → tenant\n * ```\n */\nexport class ContributorCycleError extends Error {\n readonly cycle: readonly string[]\n readonly route?: string\n\n constructor(cycle: readonly string[], route?: string) {\n const where = route ? ` on route ${route}` : ''\n super(`Cycle in context contributors${where}: ${cycle.join(' → ')}`)\n this.name = 'ContributorCycleError'\n this.cycle = Object.freeze([...cycle])\n this.route = route\n }\n}\n\n/**\n * Two contributors at the same precedence level both produce the same key\n * for the same route. Precedence resolution (method > class > module >\n * adapter > global, per architecture.md §20.4) silently drops\n * lower-precedence duplicates; this error fires only when ties occur\n * within a single level — e.g., two method-level decorators declaring\n * `key: 'tenant'`.\n *\n * `sources` is best-effort identifying labels for each conflicting\n * registration (typically the decorator's call site or registering\n * module/adapter name).\n *\n * @example\n * ```text\n * DuplicateContributorError: Duplicate context contributor for key 'tenant' at the same precedence level. Sources: LoadTenantFromHeader, LoadTenantFromSubdomain\n * ```\n */\nexport class DuplicateContributorError extends Error {\n readonly key: string\n readonly sources: readonly string[]\n\n constructor(key: string, sources: readonly string[]) {\n super(\n `Duplicate context contributor for key '${key}' at the same precedence level. ` +\n `Sources: ${sources.join(', ')}`,\n )\n this.name = 'DuplicateContributorError'\n this.key = key\n this.sources = Object.freeze([...sources])\n }\n}\n"],"mappings":";;;;;;;;;;AA2BA,IAAa,wBAAb,cAA6C,KAAM,CACjD,IACA,UACA,MACA,mBAEA,YAAY,IAAa,UAAmB,MAAgB,mBAA6B,CACvF,IAAM,MAAQ,MAAQ,aAAa,QAAU,GACvC,WAAa,iBAAiB,kBAAkB,EACtD,MAAM,gCAAgC,IAAI,iBAAiB,UAAU,GAAG,QAAQ,YAAY,EAC5F,KAAK,KAAO,0BACZ,KAAK,IAAM,IACX,KAAK,UAAY,UACjB,KAAK,MAAQ,MACb,KAAK,mBAAqB,kBAC5B,CACF,EAgCa,yBAAb,cAA8C,KAAM,CAClD,IACA,MAEA,YAAY,IAAa,MAAgB,CACvC,IAAM,MAAQ,MAAQ,OAAO,QAAU,GACvC,MACE,yBAAyB,IAAI,GAAG,MAAM,iNAIxC,EACA,KAAK,KAAO,2BACZ,KAAK,IAAM,IACX,KAAK,MAAQ,KACf,CACF,EAQA,SAAS,iBAAiB,MAAmC,CAC3D,GAAI,CAAC,MAAO,MAAO,GACnB,IAAM,MAAQ,MAAM,MAAM;CAAI,EAC9B,IAAK,IAAM,QAAQ,MAAO,CACxB,IAAM,QAAU,KAAK,KAAK,EAC1B,GAAK,QAAQ,WAAW,KAAK,GAGzB,SAAQ,SAAS,oBAAoB,GACrC,SAAQ,SAAS,iBAAiB,EACtC,MAAO,qBAAqB,QAAQ,QAAQ,OAAQ,EAAE,GACxD,CACA,MAAO,EACT,CAYA,IAAa,sBAAb,cAA2C,KAAM,CAC/C,MACA,MAEA,YAAY,MAA0B,MAAgB,CACpD,IAAM,MAAQ,MAAQ,aAAa,QAAU,GAC7C,MAAM,gCAAgC,MAAM,IAAI,MAAM,KAAK,KAAK,GAAG,EACnE,KAAK,KAAO,wBACZ,KAAK,MAAQ,OAAO,OAAO,CAAC,GAAG,KAAK,CAAC,EACrC,KAAK,MAAQ,KACf,CACF,EAmBa,0BAAb,cAA+C,KAAM,CACnD,IACA,QAEA,YAAY,IAAa,QAA4B,CACnD,MACE,0CAA0C,IAAI,2CAChC,QAAQ,KAAK,IAAI,GACjC,EACA,KAAK,KAAO,4BACZ,KAAK,IAAM,IACX,KAAK,QAAU,OAAO,OAAO,CAAC,GAAG,OAAO,CAAC,CAC3C,CACF"}
1
+ {"version":3,"file":"context-errors-7C5TBYn7.mjs","names":[],"sources":["../src/core/context-errors.ts"],"sourcesContent":["/**\n * Errors thrown by the Context Contributor pipeline.\n *\n * `MissingContributorError`, `ContributorCycleError`, and\n * `DuplicateContributorError` are startup-time errors raised during\n * pipeline build (topo-sort + validation), not per-request — a\n * misconfigured app fails to boot rather than fails a request. Per the\n * locked design in `architecture.md` §20.4, the pipeline is validated\n * once when each route is mounted and the resolved order is cached on\n * the route.\n *\n * `MissingContextValueError` is the one request-time error here: it is\n * raised by `ctx.require(key)` when a value a handler declared it needs\n * is absent. See its doc comment for why that case can't be caught at\n * startup today.\n */\n\n/**\n * A contributor declared `dependsOn: ['<key>']` but no other contributor\n * (at any precedence level) produces that key for the same route.\n *\n * @example\n * ```text\n * MissingContributorError: Missing context contributor 'tenant' required by 'project' on route GET /projects/:id\n * declared at src/contributors/load-project.ts:42:18\n * ```\n */\nexport class MissingContributorError extends Error {\n readonly key: string\n readonly dependent: string\n readonly route?: string\n readonly dependentDefinedAt?: string\n\n constructor(key: string, dependent: string, route?: string, dependentDefinedAt?: string) {\n const where = route ? ` on route ${route}` : ''\n const declaredAt = formatDeclaredAt(dependentDefinedAt)\n super(`Missing context contributor '${key}' required by '${dependent}'${where}${declaredAt}`)\n this.name = 'MissingContributorError'\n this.key = key\n this.dependent = dependent\n this.route = route\n this.dependentDefinedAt = dependentDefinedAt\n }\n}\n\n/**\n * `ctx.require(key)` found no value under `key` in the per-request\n * metadata store.\n *\n * Thrown at request time, by design. `ctx.get(key)` returns\n * `T | undefined` for every key, so the usual workaround is a\n * non-null assertion — `ctx.get('tenant')!` — which compiles whether or\n * not the route actually carries the contributor that produces it. On\n * an authorization value that is the worst possible thing to leave\n * untypeable: dropping the decorator produces no diagnostic anywhere,\n * and the handler reads `undefined` as though it were a real value.\n *\n * `ctx.require()` converts that silent hole into a loud, named failure\n * that points at the missing contributor.\n *\n * A handler typed with a generated route type (`Ctx<KickRoutes…>`) also\n * gets compile-time narrowing, so a dropped decorator fails `tsc` rather\n * than reaching this error at all. This error remains the backstop for\n * routes typegen couldn't prove (module/adapter/bootstrap registrations),\n * handlers typed as plain `RequestContext`, and contributors that ran but\n * resolved to `undefined`. See `architecture.md` §20.14.\n *\n * @example\n * ```text\n * MissingContextValueError: No context value for 'tenantPerm' on GET /projects/:id\n * Nothing wrote this key before the handler ran. Either the contributor\n * that produces it isn't applied to this route, or it ran and resolved\n * to undefined (check `optional: true` contributors).\n * ```\n */\nexport class MissingContextValueError extends Error {\n readonly key: string\n readonly route?: string\n\n constructor(key: string, route?: string) {\n const where = route ? ` on ${route}` : ''\n super(\n `No context value for '${key}'${where}\\n` +\n ` Nothing wrote this key before the handler ran. Either the contributor\\n` +\n ` that produces it isn't applied to this route, or it ran and resolved\\n` +\n ` to undefined (check \\`optional: true\\` contributors).`,\n )\n this.name = 'MissingContextValueError'\n this.key = key\n this.route = route\n }\n}\n\n/**\n * Slice the first non-framework frame off a captured `Error.stack`\n * and format it as `\\n declared at <frame>` for inclusion in\n * boot-time error messages. Returns `''` when no useful frame is\n * available (hand-rolled registrations, missing stacks).\n */\nfunction formatDeclaredAt(stack: string | undefined): string {\n if (!stack) return ''\n const lines = stack.split('\\n')\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('at ')) continue\n // Skip frames inside this file or the decorator factory — the\n // adopter's call site is the next frame after those.\n if (trimmed.includes('context-decorator.')) continue\n if (trimmed.includes('context-errors.')) continue\n return `\\n declared at ${trimmed.replace(/^at /, '')}`\n }\n return ''\n}\n\n/**\n * Topo-sort detected a cycle in `dependsOn` declarations. The `cycle` field\n * lists the keys in the order they form the loop, with the first key\n * repeated at the end so the path reads naturally.\n *\n * @example\n * ```text\n * ContributorCycleError: Cycle in context contributors on route GET /projects/:id: tenant → project → tenant\n * ```\n */\nexport class ContributorCycleError extends Error {\n readonly cycle: readonly string[]\n readonly route?: string\n\n constructor(cycle: readonly string[], route?: string) {\n const where = route ? ` on route ${route}` : ''\n super(`Cycle in context contributors${where}: ${cycle.join(' → ')}`)\n this.name = 'ContributorCycleError'\n this.cycle = Object.freeze([...cycle])\n this.route = route\n }\n}\n\n/**\n * Two contributors at the same precedence level both produce the same key\n * for the same route. Precedence resolution (method > class > module >\n * adapter > global, per architecture.md §20.4) silently drops\n * lower-precedence duplicates; this error fires only when ties occur\n * within a single level — e.g., two method-level decorators declaring\n * `key: 'tenant'`.\n *\n * `sources` is best-effort identifying labels for each conflicting\n * registration (typically the decorator's call site or registering\n * module/adapter name).\n *\n * @example\n * ```text\n * DuplicateContributorError: Duplicate context contributor for key 'tenant' at the same precedence level. Sources: LoadTenantFromHeader, LoadTenantFromSubdomain\n * ```\n */\nexport class DuplicateContributorError extends Error {\n readonly key: string\n readonly sources: readonly string[]\n\n constructor(key: string, sources: readonly string[]) {\n super(\n `Duplicate context contributor for key '${key}' at the same precedence level. ` +\n `Sources: ${sources.join(', ')}`,\n )\n this.name = 'DuplicateContributorError'\n this.key = key\n this.sources = Object.freeze([...sources])\n }\n}\n"],"mappings":";;;;;;;;;;AA2BA,IAAa,wBAAb,cAA6C,KAAM,CACjD,IACA,UACA,MACA,mBAEA,YAAY,IAAa,UAAmB,MAAgB,mBAA6B,CACvF,IAAM,MAAQ,MAAQ,aAAa,QAAU,GACvC,WAAa,iBAAiB,kBAAkB,EACtD,MAAM,gCAAgC,IAAI,iBAAiB,UAAU,GAAG,QAAQ,YAAY,EAC5F,KAAK,KAAO,0BACZ,KAAK,IAAM,IACX,KAAK,UAAY,UACjB,KAAK,MAAQ,MACb,KAAK,mBAAqB,kBAC5B,CACF,EAgCa,yBAAb,cAA8C,KAAM,CAClD,IACA,MAEA,YAAY,IAAa,MAAgB,CACvC,IAAM,MAAQ,MAAQ,OAAO,QAAU,GACvC,MACE,yBAAyB,IAAI,GAAG,MAAM,iNAIxC,EACA,KAAK,KAAO,2BACZ,KAAK,IAAM,IACX,KAAK,MAAQ,KACf,CACF,EAQA,SAAS,iBAAiB,MAAmC,CAC3D,GAAI,CAAC,MAAO,MAAO,GACnB,IAAM,MAAQ,MAAM,MAAM;CAAI,EAC9B,IAAK,IAAM,QAAQ,MAAO,CACxB,IAAM,QAAU,KAAK,KAAK,EAC1B,GAAK,QAAQ,WAAW,KAAK,GAGzB,SAAQ,SAAS,oBAAoB,GACrC,SAAQ,SAAS,iBAAiB,EACtC,MAAO,qBAAqB,QAAQ,QAAQ,OAAQ,EAAE,GACxD,CACA,MAAO,EACT,CAYA,IAAa,sBAAb,cAA2C,KAAM,CAC/C,MACA,MAEA,YAAY,MAA0B,MAAgB,CACpD,IAAM,MAAQ,MAAQ,aAAa,QAAU,GAC7C,MAAM,gCAAgC,MAAM,IAAI,MAAM,KAAK,KAAK,GAAG,EACnE,KAAK,KAAO,wBACZ,KAAK,MAAQ,OAAO,OAAO,CAAC,GAAG,KAAK,CAAC,EACrC,KAAK,MAAQ,KACf,CACF,EAmBa,0BAAb,cAA+C,KAAM,CACnD,IACA,QAEA,YAAY,IAAa,QAA4B,CACnD,MACE,0CAA0C,IAAI,2CAChC,QAAQ,KAAK,IAAI,GACjC,EACA,KAAK,KAAO,4BACZ,KAAK,IAAM,IACX,KAAK,QAAU,OAAO,OAAO,CAAC,GAAG,OAAO,CAAC,CAC3C,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v10.0.0
2
+ * @forinda/kickjs v10.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{s as matchesFlagTest}from"./route-flag-jd8PDm20.mjs";function shouldRun(reg,ctx,flags){if(reg.skipWhen===void 0&&reg.onlyWhen===void 0)return!0;let resolved=flags??EMPTY_FLAGS,route=ctx.route;return!(reg.skipWhen!==void 0&&matchesFlagTest(reg.skipWhen,resolved,route)||reg.onlyWhen!==void 0&&!matchesFlagTest(reg.onlyWhen,resolved,route))}const EMPTY_FLAGS=/* @__PURE__ */ new Map;async function runContributors(options){let{pipeline,ctx,container,flags}=options;for(let reg of pipeline.contributors)shouldRun(reg,ctx,flags)&&await runOne(reg,ctx,container)}async function runOne(reg,ctx,container){let value;try{let deps=resolveDeps(reg,container);value=await reg.resolve(ctx,deps)}catch(err){if(reg.optional)return;if(reg.onError){let replacement=await reg.onError(err,ctx);if(replacement===void 0)return;ctx.set(reg.key,replacement);return}throw err}ctx.set(reg.key,value)}function resolveDeps(reg,container){let out={};for(let[key,token]of Object.entries(reg.deps))out[key]=container.resolve(token);return out}export{runContributors as t};
12
- //# sourceMappingURL=contributor-runner-CZTAFvla.mjs.map
11
+ import{s as matchesFlagTest}from"./route-flag-BWuvJsGZ.mjs";function shouldRun(reg,ctx,flags){if(reg.skipWhen===void 0&&reg.onlyWhen===void 0)return!0;let resolved=flags??EMPTY_FLAGS,route=ctx.route;return!(reg.skipWhen!==void 0&&matchesFlagTest(reg.skipWhen,resolved,route)||reg.onlyWhen!==void 0&&!matchesFlagTest(reg.onlyWhen,resolved,route))}const EMPTY_FLAGS=/* @__PURE__ */ new Map;async function runContributors(options){let{pipeline,ctx,container,flags}=options;for(let reg of pipeline.contributors)shouldRun(reg,ctx,flags)&&await runOne(reg,ctx,container)}async function runOne(reg,ctx,container){let value;try{let deps=resolveDeps(reg,container);value=await reg.resolve(ctx,deps)}catch(err){if(reg.optional)return;if(reg.onError){let replacement=await reg.onError(err,ctx);if(replacement===void 0)return;ctx.set(reg.key,replacement);return}throw err}ctx.set(reg.key,value)}function resolveDeps(reg,container){let out={};for(let[key,token]of Object.entries(reg.deps))out[key]=container.resolve(token);return out}export{runContributors as t};
12
+ //# sourceMappingURL=contributor-runner-DBpRR5dl.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"contributor-runner-CZTAFvla.mjs","names":[],"sources":["../src/core/contributor-runner.ts"],"sourcesContent":["import type { Container } from './container'\nimport type { ContributorRegistration } from './context-decorator'\nimport type { ExecutionContext } from './execution-context'\nimport type { ContributorPipeline } from './contributor-pipeline'\nimport { matchesFlagTest, type RouteFlags } from './route-flag'\nimport type { MatchedRoute } from '../http/runtime'\n\nexport interface RunContributorsOptions {\n /** Pre-built, validated, topo-sorted pipeline. */\n pipeline: ContributorPipeline\n /** The execution context contributors read from and write into. */\n ctx: ExecutionContext\n /** DI container used to resolve declared `deps`. */\n container: Container\n /**\n * Route flags in force for the matched route, used to honour a\n * registration's `skipWhen` / `onlyWhen`. Omitted outside a route (the\n * pipeline then runs every contributor, as before).\n */\n flags?: RouteFlags\n}\n\n/**\n * Should this contributor run on a route carrying `flags`?\n *\n * `skipWhen` is what makes exemption composable: today a contributor can only\n * be opted out of by registering a permissive twin under the same key, which\n * requires owning that key. A flag is declared on the route, so an adopter can\n * exempt a contributor shipped by a plugin without forking it.\n */\nfunction shouldRun(\n reg: ContributorRegistration,\n ctx: ExecutionContext,\n flags?: RouteFlags,\n): boolean {\n if (reg.skipWhen === undefined && reg.onlyWhen === undefined) return true\n const resolved = flags ?? EMPTY_FLAGS\n const route = (ctx as { route?: MatchedRoute }).route\n if (reg.skipWhen !== undefined && matchesFlagTest(reg.skipWhen, resolved, route)) return false\n if (reg.onlyWhen !== undefined && !matchesFlagTest(reg.onlyWhen, resolved, route)) return false\n return true\n}\n\nconst EMPTY_FLAGS: RouteFlags = new Map() as RouteFlags\n\n/**\n * Execute a built {@link ContributorPipeline} against an {@link ExecutionContext}.\n *\n * Sequential — one contributor at a time, in the topo order chosen at\n * pipeline-build time. Per `architecture.md` §20.10, V1 does not run\n * contributors in parallel even within the same topo level; that is a\n * V2 follow-up gated on profiling.\n *\n * ## Error matrix (architecture.md §20.9)\n *\n * | resolve outcome | optional | onError defined | runner behaviour |\n * |-----------------|----------|-----------------|--------------------------------------------------|\n * | throws | true | — | skip; `ctx.get(key)` remains undefined |\n * | throws | false | yes | call hook; returned value (if any) stored |\n * | throws | false | no | propagate the original error |\n * | hook throws | — | yes | propagate the new error |\n * | hook returns | — | yes | undefined/void → skip; value → `ctx.set(key, v)` |\n * | resolves | — | — | `ctx.set(key, value)` |\n *\n * Container.resolve() throwing while building `deps` is treated as a\n * resolve-side throw and flows through the same matrix — adopters who\n * need different behaviour for missing-DI vs. data-lookup failures can\n * wrap the container access inside their own `resolve()`.\n */\nexport async function runContributors(options: RunContributorsOptions): Promise<void> {\n const { pipeline, ctx, container, flags } = options\n\n for (const reg of pipeline.contributors) {\n if (!shouldRun(reg, ctx, flags)) continue\n await runOne(reg, ctx, container)\n }\n}\n\nasync function runOne(\n reg: ContributorRegistration,\n ctx: ExecutionContext,\n container: Container,\n): Promise<void> {\n let value: unknown\n try {\n const deps = resolveDeps(reg, container)\n value = await reg.resolve(ctx, deps)\n } catch (err) {\n if (reg.optional) return\n if (reg.onError) {\n // Hook throws are unconditionally propagated — there is no\n // second-chance hook. Returning undefined/void from the hook\n // means \"skip\"; returning a value means \"use this instead\".\n const replacement = await reg.onError(err, ctx)\n if (replacement === undefined) return\n ctx.set(reg.key, replacement as never)\n return\n }\n throw err\n }\n ctx.set(reg.key, value as never)\n}\n\n/**\n * Resolve a contributor's declared dependencies through the container,\n * preserving the property names from the spec so `resolve(ctx, deps)`\n * receives exactly the shape the user wrote.\n *\n * Empty `deps` produces an empty object — `resolve()` always sees an\n * object, never `undefined`.\n */\nfunction resolveDeps(reg: ContributorRegistration, container: Container): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, token] of Object.entries(reg.deps)) {\n out[key] = container.resolve(token as never)\n }\n return out\n}\n"],"mappings":";;;;;;;;;;4DA8BA,SAAS,UACP,IACA,IACA,MACS,CACT,GAAI,IAAI,WAAa,IAAA,IAAa,IAAI,WAAa,IAAA,GAAW,MAAO,GACrE,IAAM,SAAW,OAAS,YACpB,MAAS,IAAiC,MAGhD,MADA,EADI,IAAI,WAAa,IAAA,IAAa,gBAAgB,IAAI,SAAU,SAAU,KAAK,GAC3E,IAAI,WAAa,IAAA,IAAa,CAAC,gBAAgB,IAAI,SAAU,SAAU,KAAK,EAElF,CAEA,MAAM,4BAA0B,IAAI,IA0BpC,eAAsB,gBAAgB,QAAgD,CACpF,GAAM,CAAE,SAAU,IAAK,UAAW,OAAU,QAE5C,IAAK,IAAM,OAAO,SAAS,aACpB,UAAU,IAAK,IAAK,KAAK,GAC9B,MAAM,OAAO,IAAK,IAAK,SAAS,CAEpC,CAEA,eAAe,OACb,IACA,IACA,UACe,CACf,IAAI,MACJ,GAAI,CACF,IAAM,KAAO,YAAY,IAAK,SAAS,EACvC,MAAQ,MAAM,IAAI,QAAQ,IAAK,IAAI,CACrC,OAAS,IAAK,CACZ,GAAI,IAAI,SAAU,OAClB,GAAI,IAAI,QAAS,CAIf,IAAM,YAAc,MAAM,IAAI,QAAQ,IAAK,GAAG,EAC9C,GAAI,cAAgB,IAAA,GAAW,OAC/B,IAAI,IAAI,IAAI,IAAK,WAAoB,EACrC,MACF,CACA,MAAM,GACR,CACA,IAAI,IAAI,IAAI,IAAK,KAAc,CACjC,CAUA,SAAS,YAAY,IAA8B,UAA+C,CAChG,IAAM,IAA+B,CAAC,EACtC,IAAK,GAAM,CAAC,IAAK,SAAU,OAAO,QAAQ,IAAI,IAAI,EAChD,IAAI,KAAO,UAAU,QAAQ,KAAc,EAE7C,OAAO,GACT"}
1
+ {"version":3,"file":"contributor-runner-DBpRR5dl.mjs","names":[],"sources":["../src/core/contributor-runner.ts"],"sourcesContent":["import type { Container } from './container'\nimport type { ContributorRegistration } from './context-decorator'\nimport type { ExecutionContext } from './execution-context'\nimport type { ContributorPipeline } from './contributor-pipeline'\nimport { matchesFlagTest, type RouteFlags } from './route-flag'\nimport type { MatchedRoute } from '../http/runtime'\n\nexport interface RunContributorsOptions {\n /** Pre-built, validated, topo-sorted pipeline. */\n pipeline: ContributorPipeline\n /** The execution context contributors read from and write into. */\n ctx: ExecutionContext\n /** DI container used to resolve declared `deps`. */\n container: Container\n /**\n * Route flags in force for the matched route, used to honour a\n * registration's `skipWhen` / `onlyWhen`. Omitted outside a route (the\n * pipeline then runs every contributor, as before).\n */\n flags?: RouteFlags\n}\n\n/**\n * Should this contributor run on a route carrying `flags`?\n *\n * `skipWhen` is what makes exemption composable: today a contributor can only\n * be opted out of by registering a permissive twin under the same key, which\n * requires owning that key. A flag is declared on the route, so an adopter can\n * exempt a contributor shipped by a plugin without forking it.\n */\nfunction shouldRun(\n reg: ContributorRegistration,\n ctx: ExecutionContext,\n flags?: RouteFlags,\n): boolean {\n if (reg.skipWhen === undefined && reg.onlyWhen === undefined) return true\n const resolved = flags ?? EMPTY_FLAGS\n const route = (ctx as { route?: MatchedRoute }).route\n if (reg.skipWhen !== undefined && matchesFlagTest(reg.skipWhen, resolved, route)) return false\n if (reg.onlyWhen !== undefined && !matchesFlagTest(reg.onlyWhen, resolved, route)) return false\n return true\n}\n\nconst EMPTY_FLAGS: RouteFlags = new Map() as RouteFlags\n\n/**\n * Execute a built {@link ContributorPipeline} against an {@link ExecutionContext}.\n *\n * Sequential — one contributor at a time, in the topo order chosen at\n * pipeline-build time. Per `architecture.md` §20.10, V1 does not run\n * contributors in parallel even within the same topo level; that is a\n * V2 follow-up gated on profiling.\n *\n * ## Error matrix (architecture.md §20.9)\n *\n * | resolve outcome | optional | onError defined | runner behaviour |\n * |-----------------|----------|-----------------|--------------------------------------------------|\n * | throws | true | — | skip; `ctx.get(key)` remains undefined |\n * | throws | false | yes | call hook; returned value (if any) stored |\n * | throws | false | no | propagate the original error |\n * | hook throws | — | yes | propagate the new error |\n * | hook returns | — | yes | undefined/void → skip; value → `ctx.set(key, v)` |\n * | resolves | — | — | `ctx.set(key, value)` |\n *\n * Container.resolve() throwing while building `deps` is treated as a\n * resolve-side throw and flows through the same matrix — adopters who\n * need different behaviour for missing-DI vs. data-lookup failures can\n * wrap the container access inside their own `resolve()`.\n */\nexport async function runContributors(options: RunContributorsOptions): Promise<void> {\n const { pipeline, ctx, container, flags } = options\n\n for (const reg of pipeline.contributors) {\n if (!shouldRun(reg, ctx, flags)) continue\n await runOne(reg, ctx, container)\n }\n}\n\nasync function runOne(\n reg: ContributorRegistration,\n ctx: ExecutionContext,\n container: Container,\n): Promise<void> {\n let value: unknown\n try {\n const deps = resolveDeps(reg, container)\n value = await reg.resolve(ctx, deps)\n } catch (err) {\n if (reg.optional) return\n if (reg.onError) {\n // Hook throws are unconditionally propagated — there is no\n // second-chance hook. Returning undefined/void from the hook\n // means \"skip\"; returning a value means \"use this instead\".\n const replacement = await reg.onError(err, ctx)\n if (replacement === undefined) return\n ctx.set(reg.key, replacement as never)\n return\n }\n throw err\n }\n ctx.set(reg.key, value as never)\n}\n\n/**\n * Resolve a contributor's declared dependencies through the container,\n * preserving the property names from the spec so `resolve(ctx, deps)`\n * receives exactly the shape the user wrote.\n *\n * Empty `deps` produces an empty object — `resolve()` always sees an\n * object, never `undefined`.\n */\nfunction resolveDeps(reg: ContributorRegistration, container: Container): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, token] of Object.entries(reg.deps)) {\n out[key] = container.resolve(token as never)\n }\n return out\n}\n"],"mappings":";;;;;;;;;;4DA8BA,SAAS,UACP,IACA,IACA,MACS,CACT,GAAI,IAAI,WAAa,IAAA,IAAa,IAAI,WAAa,IAAA,GAAW,MAAO,GACrE,IAAM,SAAW,OAAS,YACpB,MAAS,IAAiC,MAGhD,MADA,EADI,IAAI,WAAa,IAAA,IAAa,gBAAgB,IAAI,SAAU,SAAU,KAAK,GAC3E,IAAI,WAAa,IAAA,IAAa,CAAC,gBAAgB,IAAI,SAAU,SAAU,KAAK,EAElF,CAEA,MAAM,4BAA0B,IAAI,IA0BpC,eAAsB,gBAAgB,QAAgD,CACpF,GAAM,CAAE,SAAU,IAAK,UAAW,OAAU,QAE5C,IAAK,IAAM,OAAO,SAAS,aACpB,UAAU,IAAK,IAAK,KAAK,GAC9B,MAAM,OAAO,IAAK,IAAK,SAAS,CAEpC,CAEA,eAAe,OACb,IACA,IACA,UACe,CACf,IAAI,MACJ,GAAI,CACF,IAAM,KAAO,YAAY,IAAK,SAAS,EACvC,MAAQ,MAAM,IAAI,QAAQ,IAAK,IAAI,CACrC,OAAS,IAAK,CACZ,GAAI,IAAI,SAAU,OAClB,GAAI,IAAI,QAAS,CAIf,IAAM,YAAc,MAAM,IAAI,QAAQ,IAAK,GAAG,EAC9C,GAAI,cAAgB,IAAA,GAAW,OAC/B,IAAI,IAAI,IAAI,IAAK,WAAoB,EACrC,MACF,CACA,MAAM,GACR,CACA,IAAI,IAAI,IAAI,IAAK,KAAc,CACjC,CAUA,SAAS,YAAY,IAA8B,UAA+C,CAChG,IAAM,IAA+B,CAAC,EACtC,IAAK,GAAM,CAAC,IAAK,SAAU,OAAO,QAAQ,IAAI,IAAI,EAChD,IAAI,KAAO,UAAU,QAAQ,KAAc,EAE7C,OAAO,GACT"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { a as KickServer, i as AppAdapterClass, n as AdapterMiddleware, o as MiddlewarePath, r as AppAdapter, s as MiddlewarePhase, t as AdapterContext } from "../adapter-CvEm8Gpp.mjs";
2
+ import { a as KickServer, i as AppAdapterClass, n as AdapterMiddleware, o as MiddlewarePath, r as AppAdapter, s as MiddlewarePhase, t as AdapterContext } from "../adapter-ClEh-Oos.mjs";
3
3
  export { AdapterContext, AdapterMiddleware, AppAdapter, AppAdapterClass, KickServer, MiddlewarePath, MiddlewarePhase };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v10.0.0
2
+ * @forinda/kickjs v10.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v10.0.0
2
+ * @forinda/kickjs v10.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v10.0.0
2
+ * @forinda/kickjs v10.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v10.0.0
2
+ * @forinda/kickjs v10.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,4 +8,4 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- export{t as Container,n as tokenName}from"../container-CsCdem73.mjs";
11
+ export{t as Container,n as tokenName}from"../container-CS9v9UFU.mjs";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v10.0.0
2
+ * @forinda/kickjs v10.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{createLogger}from"./logger.mjs";import{c as pushClassMeta,t as getClassMeta}from"../metadata-DCWKG472.mjs";import{r as reportError}from"../observers-9FRE8VCB.mjs";const CRON_META=`kick/cron`,log=createLogger(`KickCron`),cronClasses=/* @__PURE__ */ new Map;function Cron(expression,options){return(target,propertyKey)=>{pushClassMeta(CRON_META,target.constructor,{...options,expression,handlerName:propertyKey}),cronClasses.set(target.constructor.name,target.constructor)}}function getCronJobs(target){return getClassMeta(CRON_META,target,[])}function listCronJobs(container){let jobs=[];for(let target of cronClasses.values())if(container.has(target))for(let meta of getCronJobs(target))jobs.push({...meta,name:meta.name??`${target.name}.${meta.handlerName}`,scheduleId:cronScheduleId(meta.expression),target});return jobs}function isCronJobEnabled(job){let{enabled}=job;return typeof enabled==`function`?enabled():enabled!==!1}const running=/* @__PURE__ */ new Set;async function runCronJob(job,container,trigger){if(!isCronJobEnabled(job))return!1;let key=`${job.target.name}.${job.handlerName}`;if(!job.overlap&&running.has(key))return log.warn(`${job.name} is still running — skipped this tick`),!1;running.add(key);try{return await container.resolve(job.target)[job.handlerName]({name:job.name,expression:job.expression,meta:job.meta??{},firedAt:/* @__PURE__ */ new Date,trigger}),!0}catch(err){throw log.error({err},`Cron job ${job.name} failed`),reportError(err,{source:`cron`,context:{job:job.name,expression:job.expression,trigger}}),err}finally{running.delete(key)}}async function runCronJobs(container,filter,trigger){let jobs=listCronJobs(container).filter(job=>(filter.name===void 0||job.name===filter.name)&&(filter.scheduleId===void 0||job.scheduleId===filter.scheduleId)&&(filter.expression===void 0||job.expression===filter.expression)),results=await Promise.allSettled(jobs.map(job=>runCronJob(job,container,trigger)));return{ran:results.filter(r=>r.status===`fulfilled`&&r.value).length,failed:results.filter(r=>r.status===`rejected`).length}}function cronScheduleId(expression){let hash=2166136261;for(let char of expression.trim().replace(/\s+/g,` `))hash^=char.codePointAt(0),hash=Math.imul(hash,16777619)>>>0;return hash.toString(36)}export{CRON_META,Cron,cronScheduleId,getCronJobs,isCronJobEnabled,listCronJobs,runCronJob,runCronJobs};
11
+ import{createLogger}from"./logger.mjs";import{c as pushClassMeta,t as getClassMeta}from"../metadata-zvd6AWkE.mjs";import{r as reportError}from"../observers-DZgs48UM.mjs";const CRON_META=`kick/cron`,log=createLogger(`KickCron`),cronClasses=/* @__PURE__ */ new Map;function Cron(expression,options){return(target,propertyKey)=>{pushClassMeta(CRON_META,target.constructor,{...options,expression,handlerName:propertyKey}),cronClasses.set(target.constructor.name,target.constructor)}}function getCronJobs(target){return getClassMeta(CRON_META,target,[])}function listCronJobs(container){let jobs=[];for(let target of cronClasses.values())if(container.has(target))for(let meta of getCronJobs(target))jobs.push({...meta,name:meta.name??`${target.name}.${meta.handlerName}`,scheduleId:cronScheduleId(meta.expression),target});return jobs}function isCronJobEnabled(job){let{enabled}=job;return typeof enabled==`function`?enabled():enabled!==!1}const running=/* @__PURE__ */ new Set;async function runCronJob(job,container,trigger){if(!isCronJobEnabled(job))return!1;let key=`${job.target.name}.${job.handlerName}`;if(!job.overlap&&running.has(key))return log.warn(`${job.name} is still running — skipped this tick`),!1;running.add(key);try{return await container.resolve(job.target)[job.handlerName]({name:job.name,expression:job.expression,meta:job.meta??{},firedAt:/* @__PURE__ */ new Date,trigger}),!0}catch(err){throw log.error({err},`Cron job ${job.name} failed`),reportError(err,{source:`cron`,context:{job:job.name,expression:job.expression,trigger}}),err}finally{running.delete(key)}}async function runCronJobs(container,filter,trigger){let jobs=listCronJobs(container).filter(job=>(filter.name===void 0||job.name===filter.name)&&(filter.scheduleId===void 0||job.scheduleId===filter.scheduleId)&&(filter.expression===void 0||job.expression===filter.expression)),results=await Promise.allSettled(jobs.map(job=>runCronJob(job,container,trigger)));return{ran:results.filter(r=>r.status===`fulfilled`&&r.value).length,failed:results.filter(r=>r.status===`rejected`).length}}function cronScheduleId(expression){let hash=2166136261;for(let char of expression.trim().replace(/\s+/g,` `))hash^=char.codePointAt(0),hash=Math.imul(hash,16777619)>>>0;return hash.toString(36)}export{CRON_META,Cron,cronScheduleId,getCronJobs,isCronJobEnabled,listCronJobs,runCronJob,runCronJobs};
12
12
  //# sourceMappingURL=cron.mjs.map