@forinda/kickjs 9.0.3 → 9.2.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 (144) hide show
  1. package/dist/{adapter-DhjoIYVI.d.mts → adapter-DjfRMob8.d.mts} +3 -13
  2. package/dist/{adapter-DhjoIYVI.d.mts.map → adapter-DjfRMob8.d.mts.map} +1 -1
  3. package/dist/application-BSJpaiSY.mjs +12 -0
  4. package/dist/{application-DPLzamTb.mjs.map → application-BSJpaiSY.mjs.map} +1 -1
  5. package/dist/{application-DqD58rhF.d.mts → application-CGxZCYl1.d.mts} +8 -8
  6. package/dist/{application-DqD58rhF.d.mts.map → application-CGxZCYl1.d.mts.map} +1 -1
  7. package/dist/{bootstrap-DUNvDJ1a.mjs → bootstrap-CbTF6xvv.mjs} +3 -3
  8. package/dist/{bootstrap-DUNvDJ1a.mjs.map → bootstrap-CbTF6xvv.mjs.map} +1 -1
  9. package/dist/{client-ip-CTr33Gtu.mjs → client-ip-pcINkkPr.mjs} +2 -2
  10. package/dist/{client-ip-CTr33Gtu.mjs.map → client-ip-pcINkkPr.mjs.map} +1 -1
  11. package/dist/config/index.d.mts +1 -1
  12. package/dist/config/index.mjs +2 -2
  13. package/dist/{config-C-sAHakM.mjs → config-DmGiAIuH.mjs} +3 -3
  14. package/dist/{config-C-sAHakM.mjs.map → config-DmGiAIuH.mjs.map} +1 -1
  15. package/dist/{container-ByH0uCpG.mjs → container-CAPN7pBG.mjs} +3 -3
  16. package/dist/{container-ByH0uCpG.mjs.map → container-CAPN7pBG.mjs.map} +1 -1
  17. package/dist/{context-errors-VjQBtxEc.mjs → context-errors-DYJjhalQ.mjs} +2 -2
  18. package/dist/{context-errors-VjQBtxEc.mjs.map → context-errors-DYJjhalQ.mjs.map} +1 -1
  19. package/dist/context-gpvcMlG5.mjs +12 -0
  20. package/dist/context-gpvcMlG5.mjs.map +1 -0
  21. package/dist/{contributor-runner-BEjgGaoG.mjs → contributor-runner-vk9XlOED.mjs} +3 -3
  22. package/dist/{contributor-runner-BEjgGaoG.mjs.map → contributor-runner-vk9XlOED.mjs.map} +1 -1
  23. package/dist/core/adapter.d.mts +1 -1
  24. package/dist/core/adapter.mjs +1 -1
  25. package/dist/core/app-module.mjs +1 -1
  26. package/dist/core/cache.mjs +1 -1
  27. package/dist/core/container.mjs +2 -2
  28. package/dist/core/cron.mjs +2 -2
  29. package/dist/core/decorators.d.mts +1 -1
  30. package/dist/core/decorators.mjs +2 -2
  31. package/dist/core/decorators.mjs.map +1 -1
  32. package/dist/core/errors.mjs +1 -1
  33. package/dist/core/interfaces.mjs +1 -1
  34. package/dist/core/logger.d.mts +2 -2
  35. package/dist/core/logger.mjs +2 -2
  36. package/dist/core/logger.mjs.map +1 -1
  37. package/dist/core/path.mjs +1 -1
  38. package/dist/core/plugin.d.mts +1 -1
  39. package/dist/core/plugin.mjs +1 -1
  40. package/dist/core/reactivity.mjs +1 -1
  41. package/dist/{core-DrjVUTxG.mjs → core-DKrKVLfj.mjs} +3 -3
  42. package/dist/{core-DrjVUTxG.mjs.map → core-DKrKVLfj.mjs.map} +1 -1
  43. package/dist/{csrf-COFjRrUz.d.mts → csrf-C73UkRgc.d.mts} +2 -2
  44. package/dist/{csrf-COFjRrUz.d.mts.map → csrf-C73UkRgc.d.mts.map} +1 -1
  45. package/dist/{csrf-CbFJiyKW.mjs → csrf-CxJItZVa.mjs} +3 -3
  46. package/dist/{csrf-CbFJiyKW.mjs.map → csrf-CxJItZVa.mjs.map} +1 -1
  47. package/dist/{decorate-hW_esogS.mjs → decorate-DfH1NIqV.mjs} +1 -1
  48. package/dist/{decorators-DOcWsXci.d.mts → decorators-BxNSz9Qt.d.mts} +4 -4
  49. package/dist/{decorators-DOcWsXci.d.mts.map → decorators-BxNSz9Qt.d.mts.map} +1 -1
  50. package/dist/{define-adapter-B4Iu_TcU.mjs → define-adapter-C6lFhCL-.mjs} +2 -2
  51. package/dist/{define-adapter-B4Iu_TcU.mjs.map → define-adapter-C6lFhCL-.mjs.map} +1 -1
  52. package/dist/{disposables-CgCLyCoP.mjs → disposables-NlW6Mibk.mjs} +2 -2
  53. package/dist/{disposables-CgCLyCoP.mjs.map → disposables-NlW6Mibk.mjs.map} +1 -1
  54. package/dist/{env-w27RDkCQ.mjs → env-__fzPsHD.mjs} +3 -3
  55. package/dist/{env-w27RDkCQ.mjs.map → env-__fzPsHD.mjs.map} +1 -1
  56. package/dist/{error-handler-DiueQHes.d.mts → error-handler-itG65lj0.d.mts} +2 -2
  57. package/dist/{error-handler-DiueQHes.d.mts.map → error-handler-itG65lj0.d.mts.map} +1 -1
  58. package/dist/handler-DnZbcleS.mjs +13 -0
  59. package/dist/handler-DnZbcleS.mjs.map +1 -0
  60. package/dist/http/application.d.mts +1 -1
  61. package/dist/http/application.mjs +2 -2
  62. package/dist/http/bootstrap.d.mts +1 -1
  63. package/dist/http/bootstrap.mjs +2 -2
  64. package/dist/http/context.d.mts +1 -1
  65. package/dist/http/context.mjs +2 -2
  66. package/dist/http/middleware/cors.mjs +1 -1
  67. package/dist/http/middleware/csrf.d.mts +1 -1
  68. package/dist/http/middleware/csrf.mjs +2 -2
  69. package/dist/http/middleware/error-handler.d.mts +1 -1
  70. package/dist/http/middleware/error-handler.mjs +2 -2
  71. package/dist/http/middleware/helmet.mjs +1 -1
  72. package/dist/http/middleware/rate-limit.mjs +2 -2
  73. package/dist/http/middleware/request-id.mjs +1 -1
  74. package/dist/http/middleware/request-logger.mjs +2 -2
  75. package/dist/http/middleware/request-logger.mjs.map +1 -1
  76. package/dist/http/middleware/session.mjs +2 -2
  77. package/dist/http/middleware/spa.d.mts +1 -1
  78. package/dist/http/middleware/spa.mjs +2 -2
  79. package/dist/http/middleware/upload.d.mts +1 -1
  80. package/dist/http/middleware/upload.mjs +2 -2
  81. package/dist/http/middleware/validate.mjs +1 -1
  82. package/dist/http/middleware/views.d.mts +1 -1
  83. package/dist/http/middleware/views.mjs +2 -2
  84. package/dist/http/query/index.mjs +2 -2
  85. package/dist/http/router-builder.d.mts +1 -1
  86. package/dist/http/router-builder.mjs +2 -2
  87. package/dist/http/runtimes/fastify.d.mts +3 -1
  88. package/dist/http/runtimes/fastify.d.mts.map +1 -1
  89. package/dist/http/runtimes/fastify.mjs +2 -2
  90. package/dist/http/runtimes/fastify.mjs.map +1 -1
  91. package/dist/http/runtimes/h3-web.d.mts +1 -1
  92. package/dist/http/runtimes/h3-web.mjs +2 -2
  93. package/dist/http/runtimes/h3.d.mts +1 -1
  94. package/dist/http/runtimes/h3.mjs +2 -2
  95. package/dist/http/runtimes/h3.mjs.map +1 -1
  96. package/dist/{index-CDn-p3tq.d.mts → index-CNHSbZTE.d.mts} +4 -4
  97. package/dist/{index-CDn-p3tq.d.mts.map → index-CNHSbZTE.d.mts.map} +1 -1
  98. package/dist/index.d.mts +10 -10
  99. package/dist/index.mjs +2 -2
  100. package/dist/index.mjs.map +1 -1
  101. package/dist/{jobs-CeP4aLYR.mjs → jobs-BmpeFvi0.mjs} +3 -3
  102. package/dist/{jobs-CeP4aLYR.mjs.map → jobs-BmpeFvi0.mjs.map} +1 -1
  103. package/dist/{kv-stores-CetFF9N_.d.mts → kv-stores-BZTe23Tw.d.mts} +3 -3
  104. package/dist/{kv-stores-CetFF9N_.d.mts.map → kv-stores-BZTe23Tw.d.mts.map} +1 -1
  105. package/dist/{kv-stores-5ocdxfdh.mjs → kv-stores-DJwxntyw.mjs} +3 -3
  106. package/dist/{kv-stores-5ocdxfdh.mjs.map → kv-stores-DJwxntyw.mjs.map} +1 -1
  107. package/dist/{logger-DA43DvNY.d.mts → logger-xE9qI_FS.d.mts} +45 -5
  108. package/dist/logger-xE9qI_FS.d.mts.map +1 -0
  109. package/dist/{metadata-CIXErBTj.mjs → metadata-CzzRUnAx.mjs} +2 -2
  110. package/dist/{metadata-CIXErBTj.mjs.map → metadata-CzzRUnAx.mjs.map} +1 -1
  111. package/dist/{module-registry-IO8RfVjX.mjs → module-registry-BnmRNy-3.mjs} +2 -2
  112. package/dist/{module-registry-IO8RfVjX.mjs.map → module-registry-BnmRNy-3.mjs.map} +1 -1
  113. package/dist/{observers-BtXkP5km.mjs → observers-COlQfinD.mjs} +2 -2
  114. package/dist/{observers-BtXkP5km.mjs.map → observers-COlQfinD.mjs.map} +1 -1
  115. package/dist/{query-CNa0mxSK.mjs → query-B5DTq_4W.mjs} +2 -2
  116. package/dist/{query-CNa0mxSK.mjs.map → query-B5DTq_4W.mjs.map} +1 -1
  117. package/dist/reply-CbTYIj_e.mjs +12 -0
  118. package/dist/reply-CbTYIj_e.mjs.map +1 -0
  119. package/dist/{respond-C4Vt5r46.mjs → respond-wbMSqHYo.mjs} +2 -2
  120. package/dist/{respond-C4Vt5r46.mjs.map → respond-wbMSqHYo.mjs.map} +1 -1
  121. package/dist/{route-flag-CpTa8y1n.mjs → route-flag-Bb7n9Mnp.mjs} +3 -3
  122. package/dist/{route-flag-CpTa8y1n.mjs.map → route-flag-Bb7n9Mnp.mjs.map} +1 -1
  123. package/dist/{route-policy-DPTLfer-.mjs → route-policy-C_Wsc-b1.mjs} +2 -2
  124. package/dist/{route-policy-DPTLfer-.mjs.map → route-policy-C_Wsc-b1.mjs.map} +1 -1
  125. package/dist/{router-builder-BSBSYgYW.d.mts → router-builder-CDkW88KQ.d.mts} +2 -2
  126. package/dist/{router-builder-BSBSYgYW.d.mts.map → router-builder-CDkW88KQ.d.mts.map} +1 -1
  127. package/dist/{router-builder-BxXKP8Wy.mjs → router-builder-DawnRzlp.mjs} +3 -3
  128. package/dist/{router-builder-BxXKP8Wy.mjs.map → router-builder-DawnRzlp.mjs.map} +1 -1
  129. package/dist/{upload-DjhnyHHm.d.mts → upload-Cy7qQy7M.d.mts} +3 -3
  130. package/dist/{upload-DjhnyHHm.d.mts.map → upload-Cy7qQy7M.d.mts.map} +1 -1
  131. package/dist/{upload-config-BmpmZZVK.mjs → upload-config-D01Ty40f.mjs} +2 -2
  132. package/dist/{upload-config-BmpmZZVK.mjs.map → upload-config-D01Ty40f.mjs.map} +1 -1
  133. package/dist/web.d.mts +3 -3
  134. package/dist/web.mjs +2 -2
  135. package/dist/web.mjs.map +1 -1
  136. package/package.json +2 -2
  137. package/dist/application-DPLzamTb.mjs +0 -12
  138. package/dist/context-fth87C3G.mjs +0 -12
  139. package/dist/context-fth87C3G.mjs.map +0 -1
  140. package/dist/handler-eguNTfCv.mjs +0 -13
  141. package/dist/handler-eguNTfCv.mjs.map +0 -1
  142. package/dist/logger-DA43DvNY.d.mts.map +0 -1
  143. package/dist/runtime-B4NQGN66.mjs +0 -12
  144. package/dist/runtime-B4NQGN66.mjs.map +0 -1
package/dist/web.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"web.mjs","names":[],"sources":["../src/web.ts"],"sourcesContent":["// `@forinda/kickjs/web` — the web-standard fetch entry for edge runtimes\n// (Cloudflare Workers), Bun, and Deno.\n//\n// PURITY CONTRACT: this entry's import graph must stay free of node-only\n// runtime imports — no `node:http`, `node:cluster`, `node:fs`, `node:module`,\n// no `express`, no `Application`/`bootstrap`. The single sanctioned node API\n// is `node:async_hooks` (AsyncLocalStorage — available on Workers with\n// `nodejs_compat`, Deno, and Bun). Enforced by the bundle-graph purity test.\n//\n// ```ts\n// // Cloudflare Workers (wrangler.toml: compatibility_flags = [\"nodejs_compat\"])\n// import { createWebApp } from '@forinda/kickjs/web'\n// import * as h3 from 'h3'\n// import { modules } from './modules'\n//\n// const app = createWebApp({ h3, modules })\n// export default { fetch: (req: Request) => app.fetch(req) }\n//\n// // Bun\n// Bun.serve({ fetch: (req) => app.fetch(req) })\n// ```\n\nimport { Container } from './core/container'\nimport { MutableModuleRegistry } from './core/module-registry'\nimport type { AppModule, AppModuleClass, AppModuleEntry } from './core/app-module'\nimport type { ContributorRegistrations } from './core/context-decorator'\nimport type { MiddlewareHandler } from './core/decorators'\nimport type { SourcedRegistration } from './core/contributor-pipeline'\nimport {\n _setExternalContributorSources,\n assertRouteUnique,\n buildRouteTable,\n} from './http/router-builder'\nimport { requestStore } from './http/request-store'\nimport { forwardBackgroundWork, type PlatformContext } from './http/background'\nimport { runCronJobs } from './core/cron'\nimport { runJob } from './core/jobs'\nimport { compileWebRoute } from './http/web/handler'\nimport { buildMountPath } from './core/path'\n\nexport { WebRequestShim, WebResponseDriver } from './http/web/driver'\nexport { compileWebRoute } from './http/web/handler'\n// Return-value handler helpers — edge consumers can't import the main barrel.\nexport { reply, isReply, type Reply, type InferHandlerResponse } from './http/reply'\nexport type { SseHandler } from './http/context'\nexport { waitUntil, type PlatformContext } from './http/background'\nexport { runCronJobs, type CronRun } from './core/cron'\nexport {\n runJob,\n registerJobContext,\n stampJobContext,\n JOB_CONTEXT_FIELD,\n type JobLike,\n type JobContextCarrier,\n} from './core/jobs'\n// Edge-safe stores + ctx-style rate limiter (zero runtime imports).\nexport {\n KvRateLimitStore,\n KvSessionStore,\n type KvLike,\n type KvRateLimitStoreOptions,\n type KvSessionStoreOptions,\n} from './http/middleware/kv-stores'\nexport { rateLimitGuard, type RateLimitGuardOptions } from './http/middleware/rate-limit-guard'\n\n// Minimal structural surface of h3 v2 used here (kept local so this entry\n// never imports the node-coupled h3-web runtime module).\ninterface H3AppLike {\n on(method: string, path: string, handler: (event: H3EventLike) => unknown): unknown\n all(path: string, handler: (event: H3EventLike) => unknown): unknown\n fetch(request: Request): Promise<Response>\n}\ninterface H3EventLike {\n req: Request\n url: URL\n context: { params?: Record<string, string> }\n}\n\nexport interface CreateWebAppOptions {\n /**\n * The h3 v2 module, statically imported by the caller:\n * `import * as h3 from 'h3'`. Passed in (rather than loaded here) because\n * edge bundlers have no `createRequire`, and a static `import 'h3'` inside\n * this entry would force the peer on every consumer of the subpath.\n */\n h3: unknown\n /** App modules — same shapes `bootstrap({ modules })` accepts. */\n modules: AppModuleEntry[]\n /** Route prefix (default '/api') — parity with bootstrap. */\n apiPrefix?: string\n /**\n * Default route version (default 1) — parity with bootstrap. `false` opts\n * out of URL versioning; a module's own `version` still overrides it.\n */\n defaultVersion?: number | false\n /** Global Context Contributors — parity with `bootstrap({ contributors })`. */\n contributors?: ContributorRegistrations\n /**\n * Env values for ConfigService/@Value resolution. On Workers, pass the\n * `env` binding from the fetch handler (see `createFetchHandler`); on\n * Bun/Deno, ambient `process.env` already works and this can be omitted.\n */\n env?: Record<string, string | undefined>\n /**\n * Global `(ctx, next)` middlewares, run before route middlewares on every\n * route — the web-entry counterpart of `bootstrap({ middlewares })`. Only\n * ctx-style handlers are accepted (no connect/express middleware here).\n */\n middlewares?: MiddlewareHandler[]\n}\n\nexport interface WebApp {\n /**\n * Web-standard entry: `Request` in, `Response` out. Pass the platform\n * context (Workers `ctx`, Netlify `context`) so work handed to\n * `ctx.waitUntil()` finishes after the response.\n */\n fetch: (request: Request, platform?: PlatformContext) => Promise<Response>\n /** The underlying h3 v2 app, for advanced composition. */\n h3: unknown\n}\n\n/**\n * Build a KickJS app as a web-standard fetch handler — no node http server,\n * no Application/bootstrap. Module registration, DI, and the contributor\n * pipeline behave exactly like `bootstrap()`; routes are compiled once here\n * and served through h3 v2's router.\n */\nexport function createWebApp(options: CreateWebAppOptions): WebApp {\n const h3mod = options.h3 as { H3?: new () => H3AppLike }\n if (typeof h3mod?.H3 !== 'function') {\n throw new Error(\n '@forinda/kickjs/web: pass the h3 v2 module — `createWebApp({ h3: await import(\"h3\"), ... })`. ' +\n \"The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.\",\n )\n }\n\n const container = Container.getInstance()\n\n // Wire ConfigService/@Value env resolution when the platform has no\n // ambient process.env (Workers). Latest-wins like loadEnv().\n if (options.env) {\n const env = options.env\n Container._envResolver = (key: string) => env[key]\n }\n\n // Request-scope provider — same wiring Application.setup() performs.\n Container._requestStoreProvider = () => requestStore.getStore() ?? null\n\n // Module mounting — mirrors Application.setup() steps 1:1 for the module\n // path (plugins/adapters are out of scope for the edge entry at launch).\n const registry = new MutableModuleRegistry()\n for (const m of options.modules) registry.mount(m)\n const modules = registry.entries.map((entry) => {\n const mod: AppModule = typeof entry === 'function' ? new (entry as AppModuleClass)() : entry\n mod.register?.(container)\n return mod\n })\n container.bootstrap()\n\n const app = new h3mod.H3()\n const apiPrefix = options.apiPrefix ?? '/api'\n const defaultVersion = options.defaultVersion ?? 1\n const globalMiddleware = options.middlewares ?? []\n\n const globalSources: SourcedRegistration[] = (options.contributors ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'global',\n registration: registration as SourcedRegistration['registration'],\n label: 'createWebApp',\n }),\n )\n\n // Duplicate-route guard (KICK006) — same boot-time check as the node path.\n const seenRoutes = new Map<string, string>()\n\n for (const mod of modules) {\n const declaredName = (mod as { name?: unknown }).name\n const moduleLabel =\n typeof declaredName === 'string' && declaredName.length > 0\n ? declaredName\n : (mod.constructor?.name ?? 'module')\n const moduleSources: SourcedRegistration[] = (mod.contributors?.() ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'module',\n registration,\n label: moduleLabel,\n }),\n )\n\n _setExternalContributorSources([...moduleSources, ...globalSources])\n try {\n const result = mod.routes()\n if (!result) continue\n const routeSets = Array.isArray(result) ? result : [result]\n for (const route of routeSets) {\n if (!route.controller) {\n throw new Error(\n `@forinda/kickjs/web: module route at '${route.path}' has no controller. ` +\n 'Express-style `router` mounts are not supported on the web entry.',\n )\n }\n const version = route.version ?? defaultVersion\n const mountPath = buildMountPath(apiPrefix, version, route.path)\n for (const entry of buildRouteTable(route.controller, { mountFlags: route.flags })) {\n const url = joinPath(mountPath, entry.path)\n // Per-handler owner so intra-controller duplicates name both methods.\n const owner = `${route.controller.name ?? 'controller'}.${String(entry.meta.handlerName ?? '?')}`\n assertRouteUnique(seenRoutes, entry.method, url, owner)\n if (globalMiddleware.length > 0) {\n entry.middlewares = [...globalMiddleware, ...entry.middlewares]\n }\n const run = compileWebRoute(entry)\n app.on(entry.method, url, (event: H3EventLike) =>\n run({\n request: event.req,\n url: event.url,\n params: event.context.params ?? {},\n }),\n )\n }\n }\n } finally {\n _setExternalContributorSources([])\n }\n }\n\n return {\n fetch: async (request: Request, platform?: PlatformContext) => {\n const response = await app.fetch(request)\n forwardBackgroundWork(platform)\n return response\n },\n h3: app,\n }\n}\n\n/**\n * Cloudflare Workers convenience: lazily build the app on the first request\n * so the Workers `env` binding can seed config before any module resolves.\n *\n * The Workers execution context (third argument) is forwarded, so work\n * handed to `ctx.waitUntil()` keeps running after the response.\n *\n * `scheduled` runs the `@Cron` jobs whose expression equals the trigger's\n * `event.cron` — list the same expressions under `[triggers] crons` in\n * wrangler.toml.\n *\n * `queue` consumes Cloudflare Queues: each message goes to the `@Job` class\n * for the batch's queue via `runJob`, and is acked when its handler\n * succeeds or retried when it throws. A message body of `{ name, data }`\n * picks the `@Process(name)` method; any other body goes to the queue's\n * catch-all `@Process()` as `data`.\n *\n * ```ts\n * export default createFetchHandler((env) => ({ h3, modules, env }))\n * ```\n */\nexport function createFetchHandler(\n build: (env: Record<string, string | undefined>) => CreateWebAppOptions,\n): {\n fetch: (\n request: Request,\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<Response>\n scheduled: (\n event: { cron: string },\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<void>\n queue: (\n batch: QueueBatch,\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<void>\n} {\n let app: WebApp | undefined\n return {\n fetch: (request, env = {}, ctx) => {\n app ??= createWebApp(build(env))\n return app.fetch(request, ctx)\n },\n scheduled: async (event, env = {}, ctx) => {\n app ??= createWebApp(build(env))\n const run = runCronJobs(Container.getInstance(), { expression: event.cron }, 'workers').then(\n () => {},\n )\n ctx?.waitUntil?.(run)\n await run\n },\n queue: async (batch, env = {}) => {\n app ??= createWebApp(build(env))\n const container = Container.getInstance()\n await Promise.all(\n batch.messages.map(async (message) => {\n const body = message.body as { name?: unknown; data?: unknown } | null\n const named = body !== null && typeof body === 'object' && typeof body.name === 'string'\n try {\n await runJob(container, batch.queue, {\n name: named ? (body!.name as string) : '',\n data: named ? body!.data : message.body,\n id: message.id,\n })\n message.ack()\n } catch {\n // Reported by runJob; Cloudflare redelivers per the queue's retry settings.\n message.retry()\n }\n }),\n )\n },\n }\n}\n\n/** The part of a Cloudflare Queues `MessageBatch` the consumer uses. */\nexport interface QueueBatch {\n readonly queue: string\n readonly messages: ReadonlyArray<{\n readonly id: string\n readonly body: unknown\n ack(): void\n retry(): void\n }>\n}\n\n/** Join a mount prefix and a route path into one URL, collapsing slashes. */\nfunction joinPath(mountPath: string, path: string): string {\n const a = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n if (path === '/' || path === '') return a === '' ? '/' : a\n const b = path.startsWith('/') ? path : `/${path}`\n return `${a}${b}`\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"web.mjs","names":[],"sources":["../src/web.ts"],"sourcesContent":["// `@forinda/kickjs/web` — the web-standard fetch entry for edge runtimes\n// (Cloudflare Workers), Bun, and Deno.\n//\n// PURITY CONTRACT: this entry's import graph must stay free of node-only\n// runtime imports — no `node:http`, `node:cluster`, `node:fs`, `node:module`,\n// no `express`, no `Application`/`bootstrap`. The single sanctioned node API\n// is `node:async_hooks` (AsyncLocalStorage — available on Workers with\n// `nodejs_compat`, Deno, and Bun). Enforced by the bundle-graph purity test.\n//\n// ```ts\n// // Cloudflare Workers (wrangler.toml: compatibility_flags = [\"nodejs_compat\"])\n// import { createWebApp } from '@forinda/kickjs/web'\n// import * as h3 from 'h3'\n// import { modules } from './modules'\n//\n// const app = createWebApp({ h3, modules })\n// export default { fetch: (req: Request) => app.fetch(req) }\n//\n// // Bun\n// Bun.serve({ fetch: (req) => app.fetch(req) })\n// ```\n\nimport { Container } from './core/container'\nimport { MutableModuleRegistry } from './core/module-registry'\nimport type { AppModule, AppModuleClass, AppModuleEntry } from './core/app-module'\nimport type { ContributorRegistrations } from './core/context-decorator'\nimport type { MiddlewareHandler } from './core/decorators'\nimport type { SourcedRegistration } from './core/contributor-pipeline'\nimport {\n _setExternalContributorSources,\n assertRouteUnique,\n buildRouteTable,\n} from './http/router-builder'\nimport { requestStore } from './http/request-store'\nimport { forwardBackgroundWork, type PlatformContext } from './http/background'\nimport { runCronJobs } from './core/cron'\nimport { runJob } from './core/jobs'\nimport { compileWebRoute } from './http/web/handler'\nimport { buildMountPath } from './core/path'\n\nexport { WebRequestShim, WebResponseDriver } from './http/web/driver'\nexport { compileWebRoute } from './http/web/handler'\n// Return-value handler helpers — edge consumers can't import the main barrel.\nexport { reply, isReply, type Reply, type InferHandlerResponse } from './http/reply'\nexport type { SseHandler } from './http/context'\nexport { waitUntil, type PlatformContext } from './http/background'\nexport { runCronJobs, type CronRun } from './core/cron'\nexport {\n runJob,\n registerJobContext,\n stampJobContext,\n JOB_CONTEXT_FIELD,\n type JobLike,\n type JobContextCarrier,\n} from './core/jobs'\n// Edge-safe stores + ctx-style rate limiter (zero runtime imports).\nexport {\n KvRateLimitStore,\n KvSessionStore,\n type KvLike,\n type KvRateLimitStoreOptions,\n type KvSessionStoreOptions,\n} from './http/middleware/kv-stores'\nexport { rateLimitGuard, type RateLimitGuardOptions } from './http/middleware/rate-limit-guard'\n\n// Minimal structural surface of h3 v2 used here (kept local so this entry\n// never imports the node-coupled h3-web runtime module).\ninterface H3AppLike {\n on(method: string, path: string, handler: (event: H3EventLike) => unknown): unknown\n all(path: string, handler: (event: H3EventLike) => unknown): unknown\n fetch(request: Request): Promise<Response>\n}\ninterface H3EventLike {\n req: Request\n url: URL\n context: { params?: Record<string, string> }\n}\n\nexport interface CreateWebAppOptions {\n /**\n * The h3 v2 module, statically imported by the caller:\n * `import * as h3 from 'h3'`. Passed in (rather than loaded here) because\n * edge bundlers have no `createRequire`, and a static `import 'h3'` inside\n * this entry would force the peer on every consumer of the subpath.\n */\n h3: unknown\n /** App modules — same shapes `bootstrap({ modules })` accepts. */\n modules: AppModuleEntry[]\n /** Route prefix (default '/api') — parity with bootstrap. */\n apiPrefix?: string\n /**\n * Default route version (default 1) — parity with bootstrap. `false` opts\n * out of URL versioning; a module's own `version` still overrides it.\n */\n defaultVersion?: number | false\n /** Global Context Contributors — parity with `bootstrap({ contributors })`. */\n contributors?: ContributorRegistrations\n /**\n * Env values for ConfigService/@Value resolution. On Workers, pass the\n * `env` binding from the fetch handler (see `createFetchHandler`); on\n * Bun/Deno, ambient `process.env` already works and this can be omitted.\n */\n env?: Record<string, string | undefined>\n /**\n * Global `(ctx, next)` middlewares, run before route middlewares on every\n * route — the web-entry counterpart of `bootstrap({ middlewares })`. Only\n * ctx-style handlers are accepted (no connect/express middleware here).\n */\n middlewares?: MiddlewareHandler[]\n}\n\nexport interface WebApp {\n /**\n * Web-standard entry: `Request` in, `Response` out. Pass the platform\n * context (Workers `ctx`, Netlify `context`) so work handed to\n * `ctx.waitUntil()` finishes after the response.\n */\n fetch: (request: Request, platform?: PlatformContext) => Promise<Response>\n /** The underlying h3 v2 app, for advanced composition. */\n h3: unknown\n}\n\n/**\n * Build a KickJS app as a web-standard fetch handler — no node http server,\n * no Application/bootstrap. Module registration, DI, and the contributor\n * pipeline behave exactly like `bootstrap()`; routes are compiled once here\n * and served through h3 v2's router.\n */\nexport function createWebApp(options: CreateWebAppOptions): WebApp {\n const h3mod = options.h3 as { H3?: new () => H3AppLike }\n if (typeof h3mod?.H3 !== 'function') {\n throw new Error(\n '@forinda/kickjs/web: pass the h3 v2 module — `createWebApp({ h3: await import(\"h3\"), ... })`. ' +\n \"The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.\",\n )\n }\n\n const container = Container.getInstance()\n\n // Wire ConfigService/@Value env resolution when the platform has no\n // ambient process.env (Workers). Latest-wins like loadEnv().\n if (options.env) {\n const env = options.env\n Container._envResolver = (key: string) => env[key]\n }\n\n // Request-scope provider — same wiring Application.setup() performs.\n Container._requestStoreProvider = () => requestStore.getStore() ?? null\n\n // Module mounting — mirrors Application.setup() steps 1:1 for the module\n // path (plugins/adapters are out of scope for the edge entry at launch).\n const registry = new MutableModuleRegistry()\n for (const m of options.modules) registry.mount(m)\n const modules = registry.entries.map((entry) => {\n const mod: AppModule = typeof entry === 'function' ? new (entry as AppModuleClass)() : entry\n mod.register?.(container)\n return mod\n })\n container.bootstrap()\n\n const app = new h3mod.H3()\n const apiPrefix = options.apiPrefix ?? '/api'\n const defaultVersion = options.defaultVersion ?? 1\n const globalMiddleware = options.middlewares ?? []\n\n const globalSources: SourcedRegistration[] = (options.contributors ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'global',\n registration: registration as SourcedRegistration['registration'],\n label: 'createWebApp',\n }),\n )\n\n // Duplicate-route guard (KICK006) — same boot-time check as the node path.\n const seenRoutes = new Map<string, string>()\n\n for (const mod of modules) {\n const declaredName = (mod as { name?: unknown }).name\n const moduleLabel =\n typeof declaredName === 'string' && declaredName.length > 0\n ? declaredName\n : (mod.constructor?.name ?? 'module')\n const moduleSources: SourcedRegistration[] = (mod.contributors?.() ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'module',\n registration,\n label: moduleLabel,\n }),\n )\n\n _setExternalContributorSources([...moduleSources, ...globalSources])\n try {\n const result = mod.routes()\n if (!result) continue\n const routeSets = Array.isArray(result) ? result : [result]\n for (const route of routeSets) {\n if (!route.controller) {\n throw new Error(\n `@forinda/kickjs/web: module route at '${route.path}' has no controller. ` +\n 'Express-style `router` mounts are not supported on the web entry.',\n )\n }\n const version = route.version ?? defaultVersion\n const mountPath = buildMountPath(apiPrefix, version, route.path)\n for (const entry of buildRouteTable(route.controller, { mountFlags: route.flags })) {\n const url = joinPath(mountPath, entry.path)\n // Per-handler owner so intra-controller duplicates name both methods.\n const owner = `${route.controller.name ?? 'controller'}.${String(entry.meta.handlerName ?? '?')}`\n assertRouteUnique(seenRoutes, entry.method, url, owner)\n if (globalMiddleware.length > 0) {\n entry.middlewares = [...globalMiddleware, ...entry.middlewares]\n }\n const run = compileWebRoute(entry)\n app.on(entry.method, url, (event: H3EventLike) =>\n run({\n request: event.req,\n url: event.url,\n params: event.context.params ?? {},\n }),\n )\n }\n }\n } finally {\n _setExternalContributorSources([])\n }\n }\n\n return {\n fetch: async (request: Request, platform?: PlatformContext) => {\n const response = await app.fetch(request)\n forwardBackgroundWork(platform)\n return response\n },\n h3: app,\n }\n}\n\n/**\n * Cloudflare Workers convenience: lazily build the app on the first request\n * so the Workers `env` binding can seed config before any module resolves.\n *\n * The Workers execution context (third argument) is forwarded, so work\n * handed to `ctx.waitUntil()` keeps running after the response.\n *\n * `scheduled` runs the `@Cron` jobs whose expression equals the trigger's\n * `event.cron` — list the same expressions under `[triggers] crons` in\n * wrangler.toml.\n *\n * `queue` consumes Cloudflare Queues: each message goes to the `@Job` class\n * for the batch's queue via `runJob`, and is acked when its handler\n * succeeds or retried when it throws. A message body of `{ name, data }`\n * picks the `@Process(name)` method; any other body goes to the queue's\n * catch-all `@Process()` as `data`.\n *\n * ```ts\n * export default createFetchHandler((env) => ({ h3, modules, env }))\n * ```\n */\nexport function createFetchHandler(\n build: (env: Record<string, string | undefined>) => CreateWebAppOptions,\n): {\n fetch: (\n request: Request,\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<Response>\n scheduled: (\n event: { cron: string },\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<void>\n queue: (\n batch: QueueBatch,\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<void>\n} {\n let app: WebApp | undefined\n return {\n fetch: (request, env = {}, ctx) => {\n app ??= createWebApp(build(env))\n return app.fetch(request, ctx)\n },\n scheduled: async (event, env = {}, ctx) => {\n app ??= createWebApp(build(env))\n const run = runCronJobs(Container.getInstance(), { expression: event.cron }, 'workers').then(\n () => {},\n )\n ctx?.waitUntil?.(run)\n await run\n },\n queue: async (batch, env = {}) => {\n app ??= createWebApp(build(env))\n const container = Container.getInstance()\n await Promise.all(\n batch.messages.map(async (message) => {\n const body = message.body as { name?: unknown; data?: unknown } | null\n const named = body !== null && typeof body === 'object' && typeof body.name === 'string'\n try {\n await runJob(container, batch.queue, {\n name: named ? (body!.name as string) : '',\n data: named ? body!.data : message.body,\n id: message.id,\n })\n message.ack()\n } catch {\n // Reported by runJob; Cloudflare redelivers per the queue's retry settings.\n message.retry()\n }\n }),\n )\n },\n }\n}\n\n/** The part of a Cloudflare Queues `MessageBatch` the consumer uses. */\nexport interface QueueBatch {\n readonly queue: string\n readonly messages: ReadonlyArray<{\n readonly id: string\n readonly body: unknown\n ack(): void\n retry(): void\n }>\n}\n\n/** Join a mount prefix and a route path into one URL, collapsing slashes. */\nfunction joinPath(mountPath: string, path: string): string {\n const a = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n if (path === '/' || path === '') return a === '' ? '/' : a\n const b = path.startsWith('/') ? path : `/${path}`\n return `${a}${b}`\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs",
3
- "version": "9.0.3",
3
+ "version": "9.2.0",
4
4
  "description": "Modern type-safe backend framework, powered by Vite. Built for developer experience, pluggable as you need — runs on Express, Fastify or h3.",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -116,7 +116,7 @@
116
116
  ],
117
117
  "dependencies": {
118
118
  "reflect-metadata": "^0.2.2",
119
- "@forinda/kickjs-schema": "^0.2.2"
119
+ "@forinda/kickjs-schema": "^0.2.4"
120
120
  },
121
121
  "peerDependencies": {
122
122
  "@fastify/middie": "^9.0.0",
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v9.0.3
3
- *
4
- * Copyright (c) Felix Orinda
5
- *
6
- * This source code is licensed under the MIT license found in the
7
- * LICENSE file in the root directory of this source tree.
8
- *
9
- * @license MIT
10
- */
11
- import{METADATA}from"./core/interfaces.mjs";import{c as http2UnsupportedRuntimeError,i as createToken,l as moduleRouteMissingControllerError,n as tokenName,s as http2RequiresTlsError,t as Container,u as tlsCredentialsError}from"./container-ByH0uCpG.mjs";import{Logger,createLogger}from"./core/logger.mjs";import{t as getClassMeta}from"./metadata-CIXErBTj.mjs";import{Controller,Get}from"./core/decorators.mjs";import{t as MutableModuleRegistry}from"./module-registry-IO8RfVjX.mjs";import{a as setObserverPublisher,i as reportResponse,n as releaseObservers,o as setObservers,r as reportError,t as hasResponseObservers}from"./observers-BtXkP5km.mjs";import{runCronJobs}from"./core/cron.mjs";import{i as runDisposables,n as drainDisposables,r as registerDisposable,t as disposeAll}from"./disposables-CgCLyCoP.mjs";import{HttpException}from"./core/errors.mjs";import{buildMountPath,joinPaths}from"./core/path.mjs";import{t as ROUTE_SLOT}from"./route-flag-CpTa8y1n.mjs";import"./core-DrjVUTxG.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-BxXKP8Wy.mjs";import{r as offerRoutePolicy,t as RoutePolicyTable}from"./route-policy-DPTLfer-.mjs";import{c as requestScopeMiddleware,d as rejectUnsupportedBody,i as reply,l as EXPRESS_JSON_TYPES,n as applyHandlerResult,s as isRequestScopeMiddleware,t as publishMatchedRoute}from"./runtime-B4NQGN66.mjs";import{requestId}from"./http/middleware/request-id.mjs";import{a as settleBackgroundWork,i as pendingBackgroundWork,l as requestStore,t as RequestContext}from"./context-fth87C3G.mjs";import{errorHandler,notFoundHandler}from"./http/middleware/error-handler.mjs";import{t as __decorate}from"./decorate-hW_esogS.mjs";import{validate}from"./http/middleware/validate.mjs";import{buildUploadMiddleware}from"./http/middleware/upload.mjs";import http from"node:http";import{randomBytes,timingSafeEqual}from"node:crypto";import https from"node:https";import http2 from"node:http2";import tls from"node:tls";import express,{Router}from"express";import diagnostics from"node:diagnostics_channel";const mergeConfig=(defaults={},overrides={})=>({...defaults,...overrides}),composeName=(base,scope)=>`${base}:${scope}`;function defineModule(options){if(typeof options!=`object`||!options)throw TypeError(`defineModule: options must be an object literal. See architecture.md §21 for the spec shape.`);if(typeof options.name!=`string`||options.name.length===0){let got=typeof options.name==`string`?`""`:typeof options.name;throw TypeError(`defineModule: options.name must be a non-empty string (got ${got}). Module names appear in diagnostics and \`.scoped()\` namespacing — anonymous modules make cycle / missing-dep errors hard to localize.`)}if(typeof options.build!=`function`)throw TypeError(`defineModule(${options.name}): options.build is required and must be a function (got ${typeof options.build}). The build function returns the AppModule shape — register() / routes() / contributors() — wired with the resolved config.`);let buildSync=(instanceName,scoped,overrides)=>{let config=mergeConfig(options.defaults,overrides),built=options.build(config,{name:instanceName,scoped});return Object.defineProperty(built,"__moduleName",{value:instanceName,enumerable:!1,writable:!1,configurable:!1}),built},factory=(config=>buildSync(options.name,!1,config));return factory.scoped=(scopeName,config)=>buildSync(composeName(options.name,scopeName),!0,config),Object.defineProperty(factory,"definition",{value:Object.freeze({...options}),writable:!1,enumerable:!0,configurable:!1}),factory}var DuplicateMountNameError=class extends Error{kind;mountName;constructor(kind,mountName){super(`Duplicate ${kind} name '${mountName}' — every ${kind} must declare a unique name`),this.name=`DuplicateMountNameError`,this.kind=kind,this.mountName=mountName}},MissingMountDepError=class extends Error{kind;missing;dependent;constructor(kind,missing,dependent){super(`Missing ${kind} dependency '${missing}' required by '${dependent}' — is the ${kind} installed and named correctly?`),this.name=`MissingMountDepError`,this.kind=kind,this.missing=missing,this.dependent=dependent}},MountCycleError=class extends Error{kind;cycle;constructor(kind,cycle){super(`Cycle in ${kind} dependsOn declarations: ${cycle.join(` → `)}`),this.name=`MountCycleError`,this.kind=kind,this.cycle=Object.freeze([...cycle])}};function mountSort(items,kind){let byName=new Map;for(let item of items){if(byName.has(item.name))throw new DuplicateMountNameError(kind,item.name);byName.set(item.name,item)}let indegree=new Map,dependents=new Map;for(let item of items)indegree.set(item.name,item.dependsOn?.length??0);for(let item of items)for(let dep of item.dependsOn??[]){if(!byName.has(dep))throw new MissingMountDepError(kind,dep,item.name);let list=dependents.get(dep)??[];list.push(item),dependents.set(dep,list)}let queue=[];for(let item of items)indegree.get(item.name)===0&&queue.push(item);let sorted=[];for(;queue.length>0;){let node=queue.shift();sorted.push(node);for(let dependent of dependents.get(node.name)??[]){let next=(indegree.get(dependent.name)??0)-1;indegree.set(dependent.name,next),next===0&&queue.push(dependent)}}if(sorted.length!==items.length)throw new MountCycleError(kind,reconstructCycle(items.filter(it=>(indegree.get(it.name)??0)>0),byName));return sorted}function reconstructCycle(residual,byName){let residualNames=new Set(residual.map(r=>r.name)),start=residual[0].name,path=[start],seen=new Set([start]),cursor=start;for(;;){let nextDep=byName.get(cursor).dependsOn?.find(d=>residualNames.has(d));if(nextDep===void 0)break;if(seen.has(nextDep)){let cycleStart=path.indexOf(nextDep);return[...path.slice(cycleStart),nextDep]}path.push(nextDep),seen.add(nextDep),cursor=nextDep}return path}const HANDLER_CHANNEL=`kickjs:handler`,ERROR_CHANNEL=`kickjs:error`,RESPONSE_CHANNEL=`kickjs:response`,handlerChannel=diagnostics.tracingChannel(HANDLER_CHANNEL),errorChannel=diagnostics.channel(ERROR_CHANNEL),responseChannel=diagnostics.channel(RESPONSE_CHANNEL);function installChannelPublisher(){setObserverPublisher({name:`diagnostics_channel`,onError(error,info){errorChannel.hasSubscribers&&errorChannel.publish({error,...info})},onResponse(info){responseChannel.hasSubscribers&&responseChannel.publish(info)}},()=>responseChannel.hasSubscribers)}function traceHandler(handler,meta){return ctx=>handlerChannel.hasSubscribers?handlerChannel.tracePromise(async()=>handler(ctx),{...meta,requestId:ctx.requestId,ctx}):handler(ctx)}function __decorateMetadata(k,v){if(typeof Reflect==`object`&&typeof Reflect.metadata==`function`)return Reflect.metadata(k,v)}const HEALTH_PROBE=createToken(`kick/Health/probe`);let HealthController=class{probe;constructor(probe){this.probe=probe}live(){return this.probe.isDraining()?reply(503,{status:`draining`,uptime:process.uptime()}):reply(200,{status:`ok`,uptime:process.uptime()})}async ready(){if(this.probe.isDraining())return reply(503,{status:`draining`,checks:[]});let checks=await this.probe.checks(),healthy=checks.every(c=>c.status===`up`);return reply(healthy?200:503,{status:healthy?`ready`:`degraded`,checks})}};__decorate([Get(`/live`),__decorateMetadata(`design:type`,Function),__decorateMetadata(`design:paramtypes`,[]),__decorateMetadata(`design:returntype`,void 0)],HealthController.prototype,`live`,null),__decorate([Get(`/ready`),__decorateMetadata(`design:type`,Function),__decorateMetadata(`design:paramtypes`,[]),__decorateMetadata(`design:returntype`,Promise)],HealthController.prototype,`ready`,null),HealthController=__decorate([Controller(),__decorateMetadata(`design:paramtypes`,[Object])],HealthController);const healthModule=defineModule({name:`HealthModule`,build:config=>({register(container){container.registerFactory(HealthController,()=>{let probe=container.resolve(HEALTH_PROBE);return new HealthController(probe)})},routes(){return{path:`/health`,controller:HealthController,version:!1,prefix:!1,flags:config.flags}}})});function materializeRouter(entries){let router=Router();for(let entry of entries){let method=entry.method.toLowerCase(),handlers=[];handlers.push((req,_res,next)=>{publishMatchedRoute(req,entry),next()});let contributorStep=run=>async(req,res,next)=>{let ctx=new RequestContext(req,res,next);try{await run(ctx),res.headersSent||next()}catch(err){next(err)}};entry.earlyContributorRunner&&handlers.push(contributorStep(entry.earlyContributorRunner)),entry.meta.validation&&handlers.push(validate(entry.meta.validation)),entry.meta.upload&&handlers.push(buildUploadMiddleware(entry.meta.upload));for(let mw of entry.middlewares)handlers.push((req,res,next)=>{let ctx=new RequestContext(req,res,next);Promise.resolve(mw(ctx,next)).catch(next)});entry.contributorRunner&&handlers.push(contributorStep(entry.contributorRunner)),handlers.push(async(req,res,next)=>{let ctx=new RequestContext(req,res,next);try{let result=await entry.handler(ctx);res.headersSent||applyHandlerResult(ctx,result)}catch(err){next(err)}}),router[method](entry.path,...handlers)}return router}function buildRoutes(controllerClass,options={}){return materializeRouter(buildRouteTable(controllerClass,options))}function expressRuntime(){return{name:`express`,createApp(options={}){let app=express();return app.disable(`x-powered-by`),app.set(`trust proxy`,options.trustProxy??!1),app},nodeHandler(app){return(req,res,next)=>{next?app(req,res,next):app(req,res)}},mountRoutes(app,table){for(let{mountPath,routes}of table)app.use(mountPath,materializeRouter(routes))},useConnect(app,mw,opts={}){opts.path===void 0?app.use(mw):app.use(opts.path,mw)},serveStatic(app,prefix,dir){app.use(prefix,express.static(dir))},setNotFound(app,mw){app.use(mw)},setErrorHandler(app,mw){app.use(mw)},capabilities:{render:!0,uploads:!0,connectMiddleware:!0,nativeBodyParsing:!1,http2:!1}}}const HOP_BY_HOP_REQUEST=[`connection`,`keep-alive`,`proxy-connection`,`transfer-encoding`,`upgrade`,`te`,`trailer`,`host`,`content-length`],HOP_BY_HOP_RESPONSE=[`connection`,`keep-alive`,`proxy-connection`,`transfer-encoding`,`upgrade`,`trailer`,`content-encoding`,`content-length`],LOOPBACK_HOST=`x-kick-loopback-host`,LOOPBACK_SECRET=`x-kick-loopback-secret`;async function streamedBody(request){if(!request.body||request.method===`GET`||request.method===`HEAD`)return;let reader=request.body.getReader(),first=await reader.read();if(!first.done)return new ReadableStream({start(controller){controller.enqueue(first.value)},async pull(controller){let{done,value}=await reader.read();done?controller.close():controller.enqueue(value)},cancel(reason){return reader.cancel(reason)}})}function createLoopback(handle){let started,secret=randomBytes(24).toString(`hex`),server=()=>(started??=new Promise((resolve,reject)=>{let instance=http.createServer((req,res)=>{let host=req.headers[LOOPBACK_HOST];req.headers[LOOPBACK_SECRET]===secret&&typeof host==`string`&&(req.headers.host=host),delete req.headers[LOOPBACK_HOST],delete req.headers[LOOPBACK_SECRET],handle(req,res)});instance.once(`error`,reject),instance.listen(0,`127.0.0.1`,()=>{instance.unref(),resolve({origin:`http://127.0.0.1:${instance.address().port}`,server:instance})})}).catch(err=>{throw started=void 0,err}),started);return{async fetch(request){let{origin}=await server(),url=new URL(request.url),headers=new Headers(request.headers);for(let name of HOP_BY_HOP_REQUEST)headers.delete(name);headers.set(`x-forwarded-host`,url.host),headers.set(`x-forwarded-proto`,url.protocol.slice(0,-1)),headers.set(LOOPBACK_HOST,url.host),headers.set(LOOPBACK_SECRET,secret);let body=await streamedBody(request),upstream=await fetch(origin+url.pathname+url.search,{method:request.method,headers,body,...body?{duplex:`half`}:{},redirect:`manual`,signal:request.signal}),responseHeaders=new Headers(upstream.headers),named=responseHeaders.get(`connection`)?.split(`,`)??[];for(let name of[...HOP_BY_HOP_RESPONSE,...named])responseHeaders.delete(name.trim());return new Response(upstream.body,{status:upstream.status,statusText:upstream.statusText,headers:responseHeaders})},async close(){let running=await started?.catch(()=>void 0);if(started=void 0,!running)return;let closed=new Promise(resolve=>running.server.close(()=>resolve()));running.server.closeAllConnections(),await closed}}}const log=createLogger(`Application`);function assertUsableTls(options){let{key,cert,pfx,SNICallback,pskCallback}=options;if(!pfx&&!SNICallback&&!pskCallback){if(!key&&!cert)throw tlsCredentialsError(`no key/cert, pfx, SNICallback, or pskCallback`);if(!key)throw tlsCredentialsError(`cert is set but key is missing`);if(!cert)throw tlsCredentialsError(`key is set but cert is missing`)}try{tls.createSecureContext(options)}catch(err){throw tlsCredentialsError(err.message)}}function isModuleFactory(entry){return typeof entry==`function`&&`definition`in entry&&typeof entry.scoped==`function`}function isConfigurable(factory){return factory.definition?.defaults!=null}function toAppModule(entry){if(typeof entry!=`function`)return entry;if(isModuleFactory(entry)){if(isConfigurable(entry)){let name=entry.definition?.name||entry.name||`<anonymous>`;throw TypeError(`bootstrap: module \`${name}\` takes configuration, so it must be invoked.\n Write \`${name}()\` for its defaults, or \`${name}({ … })\` to configure it.\n Passing it bare would have silently selected the defaults.`)}return entry()}let name=entry.name||`<anonymous>`;if(!isConstructible(entry))throw TypeError(`bootstrap: module entry \`${name}\` is a function, but not a module class and not a defineModule() factory (no \`definition\`).\n A defineModule() module is passed as \`${name}\` or \`${name}()\`; a class must implement AppModule.`);let mod=new entry;if(typeof mod?.routes!=`function`)throw TypeError(`bootstrap: module entry \`${name}\` constructed, but the result does not implement AppModule — \`routes()\` is missing.\n A module class needs \`routes()\`; a defineModule() module is passed as \`${name}\` or \`${name}()\`.`);return mod}function isConstructible(fn){try{return Reflect.construct(String,[],fn),!0}catch{return!1}}var Application=class{options;app;runtime;container;httpServer=null;handlerMode=!1;observers=[];http2Sessions=new Set;loopback;adapters;routePolicy=new RoutePolicyTable;plugins;_moduleContributors=[];_inFlightRequests=0;_draining=!1;_shutdownInitiated=!1;_drainResolvers=[];constructor(options){this.options=options,this.runtime=options.runtime??expressRuntime(),this.app=this.runtime.createApp({trustProxy:options.trustProxy}),this.container=Container.getInstance(),this.plugins=mountSort(options.plugins??[],`plugin`);let allAdapters=[...this.plugins.flatMap(p=>p.adapters?.()??[]),...options.adapters??[]],anonAdapterCount=0,namedAdapters=allAdapters.map(adapter=>{if(!adapter.name){let ctorName=adapter.constructor?.name,fallback=ctorName&&ctorName!==`Object`?ctorName:`AnonymousAdapter#${anonAdapterCount++}`;return Object.assign(adapter,{name:fallback})}return adapter});this.adapters=mountSort(namedAdapters,`adapter`),this.observers=setObservers([...this.adapters,...this.plugins]),installChannelPublisher(),Container._requestStoreProvider=()=>requestStore.getStore()??null}get isDraining(){return this._draining}get inFlightRequests(){return this._inFlightRequests}getContainer(){return this.container}fetch(request){let runtimeApp=this.app;return typeof runtimeApp?.fetch==`function`?Promise.resolve(runtimeApp.fetch(request)):(this.loopback??=createLoopback((req,res)=>this.handle(req,res)),this.loopback.fetch(request))}handle(req,res,next){let handler=this.runtime.nodeHandler(this.app);next?handler(req,res,next):handler(req,res)}adapterCtx(server){let env=process.env.NODE_ENV??`development`;return{http:this.adapterHttp(),app:this.app,fetch:request=>this.fetch(request),container:this.container,server,env,isProduction:env===`production`}}adapterHttp(){return{route:(method,path,handler)=>{let entry={method,path,middlewares:[],contributorRunner:null,handler,meta:{}};this.runtime.mountRoutes(this.app,[{mountPath:`/`,routes:[entry]}])},mount:(prefix,routes)=>{this.runtime.mountRoutes(this.app,[{mountPath:prefix,routes}])},serveStatic:(prefix,dir)=>{this.runtime.serveStatic(this.app,prefix,dir)},use:(mw,opts)=>{this.runtime.useConnect(this.app,mw,opts)}}}async callHook(hook,ctx,adapterName=`adapter`,phase=`setup`){if(hook)try{await hook(ctx)}catch(err){if(phase===`setup`)throw err;log.error(err,`${adapterName} afterStart hook failed`)}}async setup(){log.debug(`Bootstrapping application...`),this._moduleContributors=[];let adapterMw=this.collectAdapterMiddleware();this.app.__kickApp=this;let ctx=this.adapterCtx();for(let adapter of this.adapters)await this.callHook(adapter.beforeMount?.bind(adapter),ctx,adapter.name);this.handlerMode&&this.mountCronTrigger(),this.runtime.useConnect(this.app,this.requestTrackingMiddleware()),this.shouldAutoMountRequestScope()&&this.runtime.useConnect(this.app,requestScopeMiddleware()),this.mountMiddlewareList(adapterMw.beforeGlobal);for(let plugin of this.plugins)plugin.register?.(this.container);for(let plugin of this.plugins)try{let mw=plugin.middleware?.()??[];for(let handler of mw)this.runtime.useConnect(this.app,handler)}catch(err){log.error(err,`Plugin middleware failed: ${plugin.name??`unknown`}`)}let declaredHelmet=(this.options.middlewares??[]).some(entry=>typeof entry==`function`&&Symbol.for(`kick/http/helmet`)in entry);if(this.options.security?.helmet!==!1&&!declaredHelmet)try{let{helmet:helmetFn}=await import(`./http/middleware/helmet.mjs`);this.runtime.useConnect(this.app,helmetFn())}catch{}let userMiddlewares=this.options.middlewares;if(userMiddlewares)for(let entry of userMiddlewares)this.mountMiddlewareEntry(entry);else if(this.runtime.useConnect(this.app,requestId()),!this.runtime.capabilities.nativeBodyParsing){let limit=this.options.jsonLimit??`1mb`;this.runtime.useConnect(this.app,express.json({limit,type:[...EXPRESS_JSON_TYPES]})),this.runtime.useConnect(this.app,express.urlencoded({extended:!0,limit})),this.runtime.useConnect(this.app,express.text({limit,type:`text/*`})),this.runtime.useConnect(this.app,rejectUnsupportedBody())}this.mountMiddlewareList(adapterMw.afterGlobal);let moduleRegistry=new MutableModuleRegistry;for(let plugin of this.plugins){for(let m of plugin.modules?.()??[])moduleRegistry.mount(m);plugin.setup?.(moduleRegistry)}for(let m of this.options.modules)moduleRegistry.mount(m);this.options.setup?.(moduleRegistry),this.options.health!==!1&&(this.container.registerInstance(HEALTH_PROBE,{isDraining:()=>this._draining,checks:async()=>{let withCheck=this.adapters.filter(a=>a.onHealthCheck);return(await Promise.allSettled(withCheck.map(a=>a.onHealthCheck()))).map((c,i)=>c.status===`fulfilled`?c.value:{name:withCheck[i].name??`unknown`,status:`down`})}}),moduleRegistry.mount(healthModule(this.options.health===!0?{}:this.options.health)));let modules=moduleRegistry.entries.map(entry=>{let mod=toAppModule(entry);return mod.register?.(this.container),mod});this.container.bootstrap(),this.container.has(Logger)||this.container.registerInstance(Logger,new Logger),this.mountMiddlewareList(adapterMw.beforeRoutes);let apiPrefix=this.options.apiPrefix??`/api`,defaultVersion=this.options.defaultVersion??1,shouldLogRoutes=this.options.logRouteTable??this.options.logRoutesTable??!1,mountedRoutes=[],adapterSources=[];for(let adapter of this.adapters){let adapterContribs=adapter.contributors?.()??[],adapterLabel=adapter.name??adapter.constructor.name??`adapter`;for(let registration of adapterContribs)adapterSources.push({source:`adapter`,registration,label:adapterLabel})}for(let plugin of this.plugins){let pluginContribs=plugin.contributors?.()??[],pluginLabel=plugin.name??plugin.constructor?.name??`plugin`;for(let registration of pluginContribs)adapterSources.push({source:`adapter`,registration,label:pluginLabel})}let globalSources=(this.options.contributors??[]).map(registration=>({source:`global`,registration,label:`bootstrap`}));this._moduleContributors=[];let seenRoutes=new Map,mountedPaths=[];for(let mod of modules){let declaredName=mod.name,ctorName=mod.constructor?.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:ctorName&&ctorName!==`Object`?ctorName:`module`,moduleSources=(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel}));for(let src of moduleSources)this._moduleContributors.push({registration:src.registration,label:moduleLabel});_setExternalContributorSources([...moduleSources,...adapterSources,...globalSources]);try{let result=mod.routes();if(!result)continue;let routeSets=Array.isArray(result)?result:[result];for(let route of routeSets){let version=route.version??defaultVersion,mountPath=buildMountPath(route.prefix===!1?``:apiPrefix,version,route.path);if(route.router)this.runtime.useConnect(this.app,route.router,{path:mountPath});else if(route.controller){let routeTable=buildRouteTable(route.controller,{mountFlags:route.flags}),ctrl=route.controller.name??`controller`;for(let entry of routeTable){let owner=`${ctrl}.${String(entry.meta.handlerName??`?`)}`,fullPath=joinPaths(mountPath,entry.path);assertRouteUnique(seenRoutes,entry.method,fullPath,owner),mountedPaths.push({method:entry.method,path:fullPath}),this.routePolicy.add(entry.method,fullPath,entry.meta.flags)}let traced=routeTable.map(entry=>{let pattern=joinPaths(mountPath,entry.path);return{...entry,handler:traceHandler(entry.handler,{method:entry.method,route:pattern,controller:route.controller?.name,handler:entry.meta.handlerName}),meta:{...entry.meta,pattern}}});this.runtime.mountRoutes(this.app,[{mountPath,routes:traced}])}else throw moduleRouteMissingControllerError(mountPath);if(route.controller){for(let adapter of this.adapters)try{adapter.onRouteMount?.(route.controller,mountPath)}catch(err){log.error(err,`adapter.onRouteMount() failed for ${mountPath}`)}shouldLogRoutes&&mountedRoutes.push({controller:route.controller,mountPath})}}}finally{_setExternalContributorSources([])}}if(shouldLogRoutes&&mountedRoutes.length>0){let methodOrder=[`GET`,`POST`,`PUT`,`PATCH`,`DELETE`,`HEAD`,`OPTIONS`],methodRank=m=>{let i=methodOrder.indexOf(m);return i===-1?99:i},totalRoutes=0;log.info(`Routes:`);for(let{controller,mountPath}of mountedRoutes){let defs=getClassMeta(METADATA.ROUTES,controller,[]);if(defs.length===0)continue;let counts={};for(let def of defs){let m=def.method.toUpperCase();counts[m]=(counts[m]||0)+1}totalRoutes+=defs.length;let methods=Object.entries(counts).toSorted(([a],[b])=>methodRank(a)-methodRank(b)).map(([m,n])=>`${n} ${m}`).join(`, `),name=controller.name||`Controller`;log.info(` ${name.padEnd(30)} ${mountPath.padEnd(25)} ${defs.length} routes (${methods})`)}log.info(` Total: ${totalRoutes} routes`)}this.mountMiddlewareList(adapterMw.afterRoutes);for(let adapter of this.adapters)await this.callHook(adapter.beforeStart?.bind(adapter),ctx,adapter.name);for(let entry of this.options.middlewares??[])offerRoutePolicy(typeof entry==`function`?entry:entry.handler,this.routePolicy);for(let adapter of this.adapters)for(let mw of adapter.middleware?.()??[])offerRoutePolicy(mw.handler,this.routePolicy);this.runtime.setNotFound(this.app,this.options.onNotFound??notFoundHandler(mountedPaths)),this.runtime.setErrorHandler(this.app,observeErrors(this.options.onError??errorHandler()))}async registerOnly(){await this.setup()}async startWithoutServer(){this.handlerMode=!0,await this.setup();let needServer=this.adapters.filter(adapter=>adapter.afterStart);needServer.length>0&&log.warn(`No HTTP server in handler mode, so afterStart does not run for: ${needServer.map(adapter=>adapter.name).join(`, `)}. Anything they attach to the server (WebSockets, upgrade handlers) is unavailable.`);for(let plugin of this.plugins)await plugin.onReady?.(this.container)}async start(){let g=globalThis,serverOptions=this.options.server;if(serverOptions&&g.__kickjs_httpServer)log.warn(`server options (tls / http2) are ignored in dev mode — Vite owns the server`);else if(serverOptions){if(serverOptions.http2&&!serverOptions.tls)throw http2RequiresTlsError();if(serverOptions.http2&&!this.runtime.capabilities.http2)throw http2UnsupportedRuntimeError(this.runtime.name);serverOptions.tls&&assertUsableTls(serverOptions.tls)}if(await this.setup(),g.__kickjs_httpServer){this.httpServer=g.__kickjs_httpServer,log.debug(`Attached to Vite dev server`),g.__kickjs_app_shutdown=()=>this.shutdown();for(let adapter of this.adapters){let ctx=this.adapterCtx(this.httpServer);await this.callHook(adapter.afterStart?.bind(adapter),ctx,adapter.name,`afterStart`)}for(let plugin of this.plugins)await plugin.onReady?.(this.container);return}let port=this.options.port??parseInt(process.env.PORT||`3000`,10);this.httpServer=this.createNodeServer(),this.httpServer.on(`error`,err=>{throw err.code===`EADDRINUSE`&&(log.error(`Port ${port} is already in use. Kill the existing process or use a different port:\n PORT=${port+1} kick dev\n lsof -i :${port} # find what's using it\n kill <PID> # stop it`),process.exit(1)),err}),await new Promise((resolve,reject)=>{this.httpServer.listen(port,async()=>{try{let scheme=serverOptions?.tls?`https`:`http`,protocol=serverOptions?.http2?` (HTTP/2)`:``;log.info(`Server running on ${scheme}://localhost:${port}${protocol}`);for(let adapter of this.adapters){let afterCtx=this.adapterCtx(this.httpServer);await this.callHook(adapter.afterStart?.bind(adapter),afterCtx,adapter.name,`afterStart`)}for(let plugin of this.plugins)await plugin.onReady?.(this.container);resolve()}catch(err){reject(err)}})})}createNodeServer(){let handler=this.runtime.nodeHandler(this.app),{tls,http2:useHttp2}=this.options.server??{};if(useHttp2){let server=http2.createSecureServer({...tls,allowHTTP1:!0},handler);return server.on(`session`,session=>{this.http2Sessions.add(session),session.once(`close`,()=>this.http2Sessions.delete(session))}),server}return tls?https.createServer(tls,handler):http.createServer(handler)}async rebuild(){let prevApp=this.app,prevContainer=this.container,staleDisposables=drainDisposables();try{Container.reset(),this.container=Container.getInstance(),this.app=this.runtime.createApp({trustProxy:this.options.trustProxy}),await this.setup()}catch(err){log.error(err,`HMR rebuild failed, keeping previous app`),await runDisposables(drainDisposables());for(let d of staleDisposables)registerDisposable(d);this.app=prevApp,this.container=prevContainer;return}await runDisposables(staleDisposables),this.httpServer&&(this.httpServer.removeAllListeners(`request`),this.httpServer.on(`request`,this.runtime.nodeHandler(this.app)),log.debug(`HMR: app rebuilt and swapped`))}async shutdown(options={}){let closeServer=options.closeServer??!0;if(this._shutdownInitiated){log.debug(`Shutdown already in progress, skipping duplicate call`);return}this._shutdownInitiated=!0,this._draining=!0,log.debug(`Shutting down — draining in-flight requests...`);let timeoutMs=this.options.shutdownTimeout??3e4,timer,forceExitPromise=timeoutMs>0?new Promise(resolve=>{timer=setTimeout(()=>resolve(`timeout`),timeoutMs),timer.unref()}):new Promise(()=>{});try{if(closeServer&&this.httpServer){this.httpServer.close(()=>{});for(let session of this.http2Sessions)session.close()}if(closeServer&&this._inFlightRequests>0){log.debug(`Waiting for ${this._inFlightRequests} in-flight request(s) to complete...`);let drainPromise=new Promise(resolve=>{this._drainResolvers.push(()=>resolve(`drained`))});if(await Promise.race([drainPromise,forceExitPromise])===`timeout`){log.warn(`Shutdown timeout (${timeoutMs}ms) reached with ${this._inFlightRequests} request(s) still in-flight, forcing shutdown`);for(let session of this.http2Sessions)session.destroy()}else log.debug(`All in-flight requests completed`)}closeServer&&pendingBackgroundWork()>0&&(log.debug(`Waiting for ${pendingBackgroundWork()} waitUntil() task(s) to settle...`),await Promise.race([settleBackgroundWork().then(()=>`settled`),forceExitPromise])===`timeout`&&log.warn(`Shutdown timeout (${timeoutMs}ms) reached with ${pendingBackgroundWork()} waitUntil() task(s) still running`));let hookTimeoutMs=closeServer?timeoutMs:Math.min(timeoutMs,5e3),timeBoxed=(label,run)=>{let hook=(async()=>run())();return hookTimeoutMs<=0?hook:new Promise((resolve,reject)=>{let t=setTimeout(()=>{log.warn(`${label} did not finish shutting down within ${hookTimeoutMs}ms — continuing without it. Its resources may still be held.`),resolve(void 0)},hookTimeoutMs);t.unref(),hook.then(value=>{clearTimeout(t),resolve(value)},err=>{clearTimeout(t),reject(err)})})},wasListening=this.httpServer?.listening===!0,results=await Promise.allSettled([...this.plugins.map(plugin=>timeBoxed(`Plugin '${plugin.name}'`,()=>plugin.shutdown?.())),...this.adapters.map(adapter=>timeBoxed(`Adapter '${adapter.name}'`,()=>adapter.shutdown?.()))]);!closeServer&&wasListening&&this.httpServer?.listening===!1&&log.error("An adapter or plugin closed the shared HTTP server during an HMR reload. The dev server is now unreachable and will not rebind until you restart it. Close only what you own: socket.io's `io.close()` closes the HTTP server passed to its constructor — on a reload, disconnect the sockets instead.");for(let result of results)result.status===`rejected`&&log.error({err:result.reason},`Adapter shutdown failed`);await disposeAll(),this.container.flushChanges(),await this.loopback?.close(),this.loopback=void 0,closeServer&&releaseObservers(this.observers)}finally{timer&&clearTimeout(timer)}}getRuntimeApp(){return this.app}getActiveRuntime(){return{name:this.runtime.name,capabilities:this.runtime.capabilities}}getExpressApp(){return this.app}getAdapters(){return this.adapters}getPlugins(){return this.plugins}getContributors(){let out=[],ingest=(list,source,label)=>{if(list)for(let reg of list){let r=reg;if(r.key===void 0||r.key===null)continue;let key=typeof r.key==`string`?r.key:tokenName(r.key);out.push({key,source,label,dependsOn:Array.isArray(r.dependsOn)?r.dependsOn:[]})}};for(let adapter of this.adapters)ingest(adapter.contributors?.(),`adapter`,adapter.name??`adapter`);for(let plugin of this.plugins)ingest(plugin.contributors?.(),`plugin`,plugin.name??`plugin`);for(let{registration,label}of this._moduleContributors)ingest([registration],`module`,label);return ingest(this.options.contributors,`global`,`bootstrap`),out}getHttpServer(){return this.httpServer}shouldAutoMountRequestScope(){if(this.options.contextStore===`manual`)return!1;let userEntries=this.options.middlewares??[];for(let entry of userEntries){let handler=typeof entry==`function`?entry:entry.handler;if(isRequestScopeMiddleware(handler))return!1}return!0}collectAdapterMiddleware(){let result={beforeGlobal:[],afterGlobal:[],beforeRoutes:[],afterRoutes:[]};for(let adapter of this.adapters){let entries=adapter.middleware?.()??[];for(let entry of entries)result[entry.phase??`afterGlobal`].push(entry)}return result}mountMiddlewareList(entries){for(let entry of entries){if(entry.path===void 0){this.runtime.useConnect(this.app,entry.handler);continue}this.runtime.useConnect(this.app,entry.handler,{path:entry.path})}}mountMiddlewareEntry(entry){typeof entry==`function`?this.runtime.useConnect(this.app,entry):this.runtime.useConnect(this.app,entry.handler,{path:entry.path})}mountCronTrigger(){let secret=process.env.CRON_SECRET;secret&&this.adapterHttp().route(`GET`,`/_kick/cron/:scheduleId`,async ctx=>{if(!matchesBearer(ctx.headers.authorization,secret))throw new HttpException(401,`Missing or invalid CRON_SECRET bearer token`);let result=await runCronJobs(this.container,{scheduleId:ctx.params.scheduleId},`http`);ctx.json(result,result.failed>0?500:200)})}requestTrackingMiddleware(){return(req,res,next)=>{this._inFlightRequests++;let started=hasResponseObservers()?performance.now():void 0,onFinish=()=>settle(!0),onClose=()=>settle(!1),settle=finished=>{if(res.removeListener(`finish`,onFinish),res.removeListener(`close`,onClose),finished&&started!==void 0){let matched=req[ROUTE_SLOT];reportResponse({method:req.method,path:requestPath(req),route:matched?.pattern,status:res.statusCode,durationMs:performance.now()-started,requestId:requestIdOf(req)})}if(this._inFlightRequests--,this._draining&&this._inFlightRequests===0){for(let resolve of this._drainResolvers)resolve();this._drainResolvers=[]}};res.on(`finish`,onFinish),res.on(`close`,onClose),next()}}};function requestPath(req){return(req.originalUrl??req.url??``).split(`?`)[0]}function requestIdOf(req){let header=req.headers?.[`x-request-id`];return req.requestId??(typeof header==`string`?header:void 0)}function errorStatus(err){return typeof err?.problem?.status==`number`?err.problem.status:typeof err?.status==`number`?err.status:typeof err?.statusCode==`number`?err.statusCode:err?.name===`ZodError`?422:500}function observeErrors(handler){return((err,req,res,next)=>{let matched=req?.[ROUTE_SLOT];return reportError(err,{source:`request`,requestId:requestIdOf(req??{}),method:req?.method,path:requestPath(req??{}),route:matched?.pattern,status:errorStatus(err)}),handler(err,req,res,next)})}function matchesBearer(header,secret){if(typeof header!=`string`)return!1;let given=Buffer.from(header),expected=Buffer.from(`Bearer ${secret}`);return given.length===expected.length&&timingSafeEqual(given,expected)}export{HEALTH_PROBE as a,ERROR_CHANNEL as c,DuplicateMountNameError as d,MissingMountDepError as f,defineModule as h,materializeRouter as i,HANDLER_CHANNEL as l,mountSort as m,buildRoutes as n,HealthController as o,MountCycleError as p,expressRuntime as r,healthModule as s,Application as t,RESPONSE_CHANNEL as u};
12
- //# sourceMappingURL=application-DPLzamTb.mjs.map
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v9.0.3
3
- *
4
- * Copyright (c) Felix Orinda
5
- *
6
- * This source code is licensed under the MIT license found in the
7
- * LICENSE file in the root directory of this source tree.
8
- *
9
- * @license MIT
10
- */
11
- import{createLogger}from"./core/logger.mjs";import{r as reportError}from"./observers-BtXkP5km.mjs";import{normalizeProblem}from"./core/errors.mjs";import{t as ROUTE_SLOT}from"./route-flag-CpTa8y1n.mjs";import{r as MissingContextValueError}from"./context-errors-VjQBtxEc.mjs";import{t as resolveClientIp}from"./client-ip-CTr33Gtu.mjs";import{a as parseQuery}from"./query-CNa0mxSK.mjs";import{AsyncLocalStorage}from"node:async_hooks";const requestStore=new AsyncLocalStorage;function getRequestStore(){let store=requestStore.getStore();if(!store)throw Error(`No active request context. REQUEST-scoped services can only be resolved during an HTTP request.`);return store}function getRequestValue(key){return requestStore.getStore()?.values.get(key)}const log=createLogger(`waitUntil`),pending=new Set;function waitUntil(promise){let requestId=requestStore.getStore()?.requestId,tracked=Promise.resolve(promise).then(()=>{},err=>{log.error({err,requestId},`Background work passed to waitUntil() failed`),reportError(err,{source:`background`,requestId})});pending.add(tracked),tracked.finally(()=>pending.delete(tracked))}async function settleBackgroundWork(){for(;pending.size>0;)await Promise.all(pending)}function pendingBackgroundWork(){return pending.size}function forwardBackgroundWork(platform){platform?.waitUntil?.(settleBackgroundWork())}const signalControllerKey=Symbol.for(`kickjs.requestContext.signalController`),readOnlyProxyCacheKey=Symbol.for(`kickjs.requestContext.readOnlyProxyCache`);function isPlainObjectOrArray(v){if(typeof v!=`object`||!v)return!1;if(Array.isArray(v))return!0;let proto=Object.getPrototypeOf(v);return proto===Object.prototype||proto===null}function makeReadOnlyProxy(target,req,label){if(process.env.NODE_ENV===`production`||!isPlainObjectOrArray(target))return target;let reqObj=req,cache=reqObj[readOnlyProxyCacheKey];cache||(cache=new WeakMap,reqObj[readOnlyProxyCacheKey]=cache);let targetObj=target,cached=cache.get(targetObj);if(cached)return cached;let warnWrite=(verb,prop)=>{console.warn(`[kickjs] Attempted to ${verb} \`ctx.${label}.${String(prop)}\` — request data is read-only. Stash computed values via \`ctx.set('key', value)\` or a Context Contributor, or reach \`ctx.req.${label}\` if you genuinely need the raw mutable handle.`)},proxy=new Proxy(targetObj,{get(t,prop,receiver){let value=Reflect.get(t,prop,receiver);return isPlainObjectOrArray(value)?makeReadOnlyProxy(value,req,`${label}.${String(prop)}`):value},set(_t,prop){return warnWrite(`assign`,prop),!0},deleteProperty(_t,prop){return warnWrite(`delete`,prop),!0},defineProperty(_t,prop){return warnWrite(`Object.defineProperty on`,prop),!0}});return cache.set(targetObj,proxy),proxy}var RequestContext=class{req;res;next;_response;get route(){return this.req?.[ROUTE_SLOT]}constructor(req,res,next,responseDriver){this.req=req,this.res=res,this.next=next,this._response=responseDriver??res}_qsMemo;_problem;metadataReadOnly(){return requestStore.getStore()?.values??null}metadataForWrite(){let store=requestStore.getStore();if(store)return store.values;throw Error(`RequestContext.set called outside an AsyncLocalStorage frame. Mount requestScopeMiddleware() before constructing RequestContext, or wrap test setup in requestStore.run({ requestId, instances: new Map(), values: new Map() }, () => ...).`)}get body(){return makeReadOnlyProxy(this.req.body,this.req,`body`)}get params(){return makeReadOnlyProxy(this.req.params,this.req,`params`)}get query(){return makeReadOnlyProxy(this.req.query,this.req,`query`)}get headers(){return makeReadOnlyProxy(this.req.headers,this.req,`headers`)}get requestId(){return this.req.requestId??this.req.headers[`x-request-id`]}get signal(){let req=this.req,cached=req[signalControllerKey];if(cached)return cached.signal;let ctrl=new AbortController;req[signalControllerKey]=ctrl;let abort=()=>ctrl.abort(`request closed`);return this.req.once(`close`,abort),this._response.once(`close`,abort),ctrl.signal}get session(){return this.req.session}get user(){return this.metadataReadOnly()?.get(`user`)??this.req.user}get tenantId(){return this.user?.tenantId}get roles(){let u=this.user;return u?.tenantRoles??u?.roles??[]}qs(fieldConfig,options){let memo=this._qsMemo;if(memo&&memo.config===fieldConfig&&memo.options===options)return memo.result;let effective={...options,onReject:options?.onReject??(rejection=>{let id=this.requestId?`[${this.requestId}] `:``;console.warn(`${id}query ${rejection.kind} '${rejection.field}' rejected (${rejection.reason})`)})},result=parseQuery(this.req.query,fieldConfig,effective);return this._qsMemo={config:fieldConfig,options,result},result}get file(){let f=this.req.file;return f?makeReadOnlyProxy(f,this.req,`file`):void 0}get files(){let f=this.req.files;if(Array.isArray(f))return makeReadOnlyProxy(f,this.req,`files`)}get(key){return this.metadataReadOnly()?.get(key)}require(key){let value=this.metadataReadOnly()?.get(key);if(value===void 0)throw new MissingContextValueError(key,this.routeLabel());return value}routeLabel(){let req=this.req,method=typeof req?.method==`string`?req.method:void 0,url=typeof req?.url==`string`?req.url:void 0;return method&&url?`${method} ${url}`:method??url}set(key,value){this.metadataForWrite().set(key,value)}setHeader(name,value){return this._response.setHeader(name,value),this}json(data,status=200){return this._response.status(status).json(data)}created(data){return this._response.status(201).json(data)}noContent(){return this._response.status(204).end()}notFound(message=`Not Found`){return this._response.status(404).json({message})}badRequest(message){return this._response.status(400).json({message})}get problem(){if(this._problem)return this._problem;let send=input=>this.sendProblem(input);return send.badRequest=(input={})=>this.sendProblem({status:400,...input}),send.unauthorized=(input={})=>this.sendProblem({status:401,...input}),send.forbidden=(input={})=>this.sendProblem({status:403,...input}),send.notFound=(input={})=>this.sendProblem({status:404,...input}),send.conflict=(input={})=>this.sendProblem({status:409,...input}),send.unprocessable=(input={})=>this.sendProblem({status:422,...input}),send.tooManyRequests=(input={})=>this.sendProblem({status:429,...input}),send.internal=(input={})=>this.sendProblem({status:500,...input}),send.validation=(issues,input={})=>{let errors=(issues||[]).map(issue=>({field:issue.path?.join?.(`.`)??``,message:issue.message??``,code:issue.code}));return this.sendProblem({status:422,detail:issues?.[0]?.message,...input,errors})},this._problem=send,this._problem}sendProblem(input){let body=normalizeProblem(input);return this._response.setHeader(`Content-Type`,`application/problem+json`),this._response.status(body.status).json(body)}get ip(){return resolveClientIp(this.req)}redirect(url,status=302){return this._response.status(status).setHeader(`location`,url),this._response.end()}html(content,status=200){return this._response.status(status).type(`text/html; charset=utf-8`).send(content)}download(buffer,filename,contentType=`application/octet-stream`){return this._response.setHeader(`Content-Disposition`,contentDisposition(filename)),this._response.setHeader(`Content-Type`,contentType),this._response.send(buffer)}render(template,data={}){return this._response.render(template,data)}async paginate(fetcher,fieldConfig){let parsed=this.qs(fieldConfig),{data,total}=await fetcher(parsed),{page,limit}=parsed.pagination,totalPages=Math.ceil(total/limit)||1,response={data,meta:{page,limit,total,totalPages,hasNext:page<totalPages,hasPrev:page>1}};return this.json(response),response}async sendResponse(response){let headers={};response.headers.forEach((value,name)=>{name!==`set-cookie`&&(headers[name]=value)});let cookies=response.headers.getSetCookie();if(cookies.length>0&&(headers[`set-cookie`]=cookies),!response.body){this._response.status(response.status);for(let[name,value]of Object.entries(headers))this._response.setHeader(name,value);this._response.send(``);return}this._response.writeHead(response.status,headers),this._response.flushHeaders();let reader=response.body.getReader(),stop=()=>{reader.cancel().catch(()=>{})};this.signal.addEventListener(`abort`,stop,{once:!0});try{for(;;){let{done,value}=await reader.read();if(done)break;this._response.write(Buffer.from(value))===!1&&!this.signal.aborted&&await new Promise(resolve=>{let resume=()=>{this.signal.removeEventListener(`abort`,resume),resolve()};this._response.once(`drain`,resume),this.signal.addEventListener(`abort`,resume,{once:!0})})}}finally{this.signal.removeEventListener(`abort`,stop),this._response.end()}}waitUntil(promise){waitUntil(promise)}sse(){this._response.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`,"X-Accel-Buffering":`no`}),this._response.flushHeaders();let closeCallbacks=[];return this.req.on(`close`,()=>{for(let cb of closeCallbacks)cb()}),{send:(data,event,id)=>{id&&this._response.write(`id: ${id}\n`),event&&this._response.write(`event: ${event}\n`),this._response.write(`data: ${JSON.stringify(data)}\n\n`)},comment:text=>{this._response.write(`: ${text}\n\n`)},onClose:fn=>{closeCallbacks.push(fn)},close:()=>{this._response.end()}}}};function contentDisposition(filename){return`attachment; filename="${filename.replace(/[^\x20-\x7e]|["\\]/g,`_`)}"; filename*=UTF-8''${encodeURIComponent(filename).replace(/['()*]/g,c=>`%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`}export{settleBackgroundWork as a,getRequestValue as c,pendingBackgroundWork as i,requestStore as l,contentDisposition as n,waitUntil as o,forwardBackgroundWork as r,getRequestStore as s,RequestContext as t};
12
- //# sourceMappingURL=context-fth87C3G.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"context-fth87C3G.mjs","names":[],"sources":["../src/http/request-store.ts","../src/http/background.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","/// <reference types=\"multer\" />\nimport type { Request, NextFunction } from 'express'\nimport { ROUTE_SLOT } from '../core/route-flag'\nimport type { ActiveRuntime, MatchedRoute, RuntimeResponse } 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._response.status(status).json(data)\n }\n\n created(data: any) {\n return this._response.status(201).json(data)\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 `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,13 +0,0 @@
1
- /**
2
- * @forinda/kickjs v9.0.3
3
- *
4
- * Copyright (c) Felix Orinda
5
- *
6
- * This source code is licensed under the MIT license found in the
7
- * LICENSE file in the root directory of this source tree.
8
- *
9
- * @license MIT
10
- */
11
- import{createLogger,describeError}from"./core/logger.mjs";import{HttpException}from"./core/errors.mjs";import{a as createRequestStore,f as unsupportedMediaTypeError,n as applyHandlerResult,o as disposeRequestStore,t as publishMatchedRoute,u as classifyMediaType}from"./runtime-B4NQGN66.mjs";import{l as requestStore,t as RequestContext}from"./context-fth87C3G.mjs";import{validate}from"./http/middleware/validate.mjs";import{t as applyUploadConfig}from"./upload-config-BmpmZZVK.mjs";var WebRequestShim=class{method;url;headers;body;params={};query={};requestId;file;files;signal;constructor(request,url){this.method=request.method,this.url=url.pathname+url.search,this.signal=request.signal;let headers={};request.headers.forEach((value,key)=>{headers[key]=value}),this.headers=headers;let query={};for(let key of url.searchParams.keys()){let all=url.searchParams.getAll(key);query[key]=all.length>1?all:all[0]}this.query=query}once(event,listener){return event===`close`&&this.signal.addEventListener(`abort`,listener,{once:!0}),this}on(event,listener){return this.once(event,listener)}};const encoder=new TextEncoder;function toBytes(chunk){return chunk instanceof Uint8Array?chunk:encoder.encode(String(chunk))}var WebResponseDriver=class{signal;ready;settled=!1;_status=200;headers=new Headers;_headersSent=!1;writer;resolveReady;constructor(signal){this.signal=signal,this.ready=new Promise(resolve=>{this.resolveReady=resolve})}status(code){return this._status=code,this}json(data){return this.headers.has(`content-type`)||this.headers.set(`content-type`,`application/json; charset=utf-8`),this.finish(JSON.stringify(data))}send(data){return typeof data==`object`&&data&&!(data instanceof Uint8Array)?this.json(data):this.finish(data)}type(contentType){return this.headers.set(`content-type`,contentType),this}setHeader(name,value){if(Array.isArray(value)){this.headers.delete(name);for(let v of value)this.headers.append(name,String(v))}else this.headers.set(name,String(value));return this}render(){throw Error(`ctx.render() is not supported on the web runtime (no view engine).`)}writeHead(statusCode,headers){if(this._status=statusCode,headers)for(let[name,value]of Object.entries(headers))this.setHeader(name,value);return this.startStream(),this}flushHeaders(){return this.startStream(),this}write(chunk){return this.startStream(),this.writer.write(toBytes(chunk)).catch(()=>{}),!0}end(data){return this.writer?(data!==void 0&&this.write(data),this.writer.close().catch(()=>{}),this.writer=void 0,this):this.finish(data)}once(event,listener){return event===`close`&&this.signal&&this.signal.addEventListener(`abort`,listener,{once:!0}),this}get headersSent(){return this._headersSent}finish(body){if(this.settled)return this;this.settled=!0,this._headersSent=!0;let bodyAllowed=this._status!==204&&this._status!==304;return this.resolveReady(new Response(bodyAllowed?body??null:null,{status:this._status,headers:this.headers})),this}startStream(){if(this.writer||this.settled)return;let{readable,writable}=new TransformStream;this.writer=writable.getWriter(),this.settled=!0,this._headersSent=!0,this.signal?.addEventListener(`abort`,()=>{this.writer?.abort().catch(()=>{}),this.writer=void 0},{once:!0}),this.resolveReady(new Response(readable,{status:this._status,headers:this.headers}))}};const log=createLogger(`WebPipeline`),NOOP_NEXT=()=>{},BODY_METHODS=new Set([`POST`,`PUT`,`PATCH`,`DELETE`]);function defaultErrorResponse(err,driver){let status=typeof err?.status==`number`?err.status:typeof err?.statusCode==`number`?err.statusCode:500;if(status>=500){log.error(err,`Unhandled error in web pipeline — ${describeError(err)}`);let stack=err?.stack;driver.status(status).json({error:`Internal Server Error`,...isProduction()?{}:{message:describeError(err),...typeof stack==`string`?{stack:stack.split(`
12
- `)}:{}}});return}driver.status(status).json({error:err?.message??`Request failed`})}function isProduction(){return typeof process<`u`&&process.env?.NODE_ENV===`production`}function compileWebRoute(entry,hooks){let validator=entry.meta.validation?validate(entry.meta.validation):void 0,upload=entry.meta.upload&&entry.meta.upload.mode!==`none`?entry.meta.upload:null;return async({request,url,params})=>{let req=new WebRequestShim(request,url);req.params=params;let bodyError;if(upload){let form=await request.formData().catch(()=>void 0);if(form){let rawParts=[],fields={};for(let[name,value]of form.entries())typeof value==`string`?fields[name]=value:rawParts.push({fieldname:name,filename:value.name??``,mimetype:value.type||`application/octet-stream`,buffer:new Uint8Array(await value.arrayBuffer())});let{file,files}=applyUploadConfig(rawParts,entry.meta.upload);req.file=file,req.files=files,req.body=fields}}else if(BODY_METHODS.has(request.method)){let raw=request.body===null?``:await request.text().catch(()=>``),kind=classifyMediaType(req.headers[`content-type`]);if(raw!==``&&kind===`unsupported`)bodyError=unsupportedMediaTypeError(req.headers[`content-type`]);else if(raw!==``&&kind!==`multipart`){if(kind===`json`)try{req.body=JSON.parse(raw)}catch(err){bodyError=HttpException.badRequest(err instanceof Error?err.message:`Request body could not be parsed`)}else req.body=kind===`urlencoded`?Object.fromEntries(new URLSearchParams(raw)):raw}}let store=createRequestStore(req.headers[`x-request-id`]);req.requestId=store.requestId;let driver=new WebResponseDriver(request.signal);driver.setHeader(`x-request-id`,store.requestId),publishMatchedRoute(req,entry);let pipeline=requestStore.run(store,async()=>{if(bodyError)throw bodyError;let ctx=new RequestContext(req,driver,NOOP_NEXT,driver);if(entry.earlyContributorRunner&&(await entry.earlyContributorRunner(ctx),driver.settled))return;validator&&await new Promise((resolve,reject)=>{validator(req,void 0,err=>err?reject(err):resolve())});for(let mw of entry.middlewares){let advanced=!1;if(await new Promise((resolve,reject)=>{Promise.resolve(mw(ctx,err=>{advanced=!0,err?reject(err):resolve()})).catch(reject)}),!advanced||driver.settled)return}if(entry.contributorRunner&&await entry.contributorRunner(ctx),driver.settled)return;let result=await entry.handler(ctx);driver.settled||applyHandlerResult(ctx,result)});try{try{await Promise.race([pipeline,driver.ready])}catch(err){driver.settled||(hooks?.onError&&await hooks.onError(err,req,driver),driver.settled||defaultErrorResponse(err,driver))}return driver.settled||driver.status(404).json({error:`Not Found`}),await driver.ready}finally{Promise.resolve(pipeline).catch(()=>{}).then(()=>disposeRequestStore(store))}}}export{WebRequestShim as n,WebResponseDriver as r,compileWebRoute as t};
13
- //# sourceMappingURL=handler-eguNTfCv.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"handler-eguNTfCv.mjs","names":[],"sources":["../src/http/web/driver.ts","../src/http/web/handler.ts"],"sourcesContent":["// Web-standard driver pair — the request/response bridge between web\n// primitives (WHATWG Request/Response) and the KickJS RequestContext surface.\n//\n// Deliberately imports NOTHING from node: this module is the edge-safe core\n// shared by the h3 v2 runtime (`runtimes/h3-web.ts`, node bootstrap) and the\n// `@forinda/kickjs/web` fetch entry (edge/Bun/Deno).\n\nimport type { RuntimeResponse } from '../runtime'\n\n/**\n * Presents a web `Request` as the express-shaped `req` object that\n * `RequestContext` (and route validation) reads: plain-object `headers`,\n * assignable `body`/`params`/`query`, and `on/once('close')` bridged from\n * the request's `AbortSignal` — the only EventEmitter surface the context\n * layer uses (`ctx.signal`, SSE close).\n */\nexport class WebRequestShim {\n readonly method: string\n readonly url: string\n /** Lower-cased plain header object (RequestContext indexes it directly). */\n readonly headers: Record<string, string>\n body: unknown\n params: Record<string, string> = {}\n query: Record<string, unknown> = {}\n requestId?: string\n file?: unknown\n files?: unknown;\n // Ad-hoc props middleware may set (session, user, ...) — index signature\n // keeps assignment open like a node req object.\n [key: string]: unknown\n\n private readonly signal: AbortSignal\n\n constructor(request: Request, url: URL) {\n this.method = request.method\n // Express-shaped `req.url` is path + search, not the absolute URL.\n this.url = url.pathname + url.search\n this.signal = request.signal\n const headers: Record<string, string> = {}\n request.headers.forEach((value, key) => {\n headers[key] = value\n })\n this.headers = headers\n const query: Record<string, unknown> = {}\n for (const key of url.searchParams.keys()) {\n const all = url.searchParams.getAll(key)\n query[key] = all.length > 1 ? all : all[0]\n }\n this.query = query\n }\n\n /** `req.once('close')` — abort of the underlying request signal. */\n once(event: string, listener: () => void): this {\n if (event === 'close') this.signal.addEventListener('abort', listener, { once: true })\n return this\n }\n\n /** `req.on('close')` — same bridge; signals only abort once anyway. */\n on(event: string, listener: () => void): this {\n return this.once(event, listener)\n }\n}\n\nconst encoder = new TextEncoder()\n\nfunction toBytes(chunk: unknown): Uint8Array {\n if (chunk instanceof Uint8Array) return chunk\n return encoder.encode(String(chunk))\n}\n\n/**\n * `RuntimeResponse` over a web `Response`.\n *\n * Two modes:\n * - **Buffered** (default): `status/json/send/type/setHeader` record state;\n * the terminal call resolves {@link ready} with a complete `Response`.\n * - **Streaming**: the first `writeHead`/`flushHeaders`/`write` call switches\n * to a `TransformStream` and resolves {@link ready} IMMEDIATELY with a\n * streamed `Response` — SSE works on edge this way; `write` feeds the\n * writer, `end` closes it.\n *\n * A class so the method table lives on a shared prototype (one allocation\n * per request — same rationale as the Fastify/h3 driver classes).\n */\nexport class WebResponseDriver implements RuntimeResponse {\n /** Resolves with the final (or streamed) Response. */\n readonly ready: Promise<Response>\n /** True once a terminal/streaming action produced the Response. */\n settled = false\n\n private _status = 200\n private readonly headers = new Headers()\n private _headersSent = false\n private writer?: WritableStreamDefaultWriter<Uint8Array>\n private resolveReady!: (response: Response) => void\n\n constructor(private readonly signal?: AbortSignal) {\n this.ready = new Promise<Response>((resolve) => {\n this.resolveReady = resolve\n })\n }\n\n status(code: number): this {\n this._status = code\n return this\n }\n\n json(data: unknown): this {\n if (!this.headers.has('content-type'))\n this.headers.set('content-type', 'application/json; charset=utf-8')\n return this.finish(JSON.stringify(data))\n }\n\n send(data: unknown): this {\n if (\n data !== undefined &&\n data !== null &&\n typeof data === 'object' &&\n !(data instanceof Uint8Array)\n ) {\n return this.json(data)\n }\n return this.finish(data as string | Uint8Array | null | undefined)\n }\n\n type(contentType: string): this {\n this.headers.set('content-type', contentType)\n return this\n }\n\n setHeader(name: string, value: unknown): this {\n if (Array.isArray(value)) {\n this.headers.delete(name)\n for (const v of value) this.headers.append(name, String(v))\n } else {\n this.headers.set(name, String(value))\n }\n return this\n }\n\n render(): never {\n throw new Error('ctx.render() is not supported on the web runtime (no view engine).')\n }\n\n writeHead(statusCode: number, headers?: Record<string, string | number | string[]>): this {\n this._status = statusCode\n if (headers) {\n for (const [name, value] of Object.entries(headers)) this.setHeader(name, value)\n }\n this.startStream()\n return this\n }\n\n flushHeaders(): this {\n this.startStream()\n return this\n }\n\n write(chunk: unknown): boolean {\n this.startStream()\n // Fire-and-forget: backpressure is absorbed by the stream's queue. Write\n // failures (client gone) surface via the abort listener, not here.\n void this.writer!.write(toBytes(chunk)).catch(() => {})\n return true\n }\n\n end(data?: unknown): this {\n if (this.writer) {\n if (data !== undefined) this.write(data)\n void this.writer.close().catch(() => {})\n this.writer = undefined\n return this\n }\n return this.finish(data as string | Uint8Array | null | undefined)\n }\n\n once(event: string, listener: (...args: unknown[]) => void): this {\n if (event === 'close' && this.signal)\n this.signal.addEventListener('abort', listener as () => void, { once: true })\n return this\n }\n\n get headersSent(): boolean {\n return this._headersSent\n }\n\n /** Buffered terminal: build the final Response exactly once. */\n private finish(body?: string | Uint8Array | null): this {\n if (this.settled) return this\n this.settled = true\n this._headersSent = true\n // 204/304 must not carry a body per the Response constructor contract.\n const bodyAllowed = this._status !== 204 && this._status !== 304\n this.resolveReady(\n new Response(bodyAllowed ? (body ?? null) : null, {\n status: this._status,\n headers: this.headers,\n }),\n )\n return this\n }\n\n /** Switch to streaming mode and resolve `ready` with the streamed Response. */\n private startStream(): void {\n if (this.writer || this.settled) return\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()\n this.writer = writable.getWriter()\n this.settled = true\n this._headersSent = true\n // Abort from the client tears the stream down so the writer stops queueing.\n this.signal?.addEventListener(\n 'abort',\n () => {\n void this.writer?.abort().catch(() => {})\n this.writer = undefined\n },\n { once: true },\n )\n this.resolveReady(new Response(readable, { status: this._status, headers: this.headers }))\n }\n}\n","// Engine-neutral web pipeline: run one KickJS RouteEntry against a WHATWG\n// Request and produce a WHATWG Response. Shared by the h3 v2 runtime\n// (node bootstrap) and the `@forinda/kickjs/web` fetch entry (edge/Bun/Deno).\n// Edge-safe: no node imports beyond the sanctioned ALS request store.\n\nimport { HttpException } from '../../core/errors'\nimport { classifyMediaType, unsupportedMediaTypeError } from '../body-policy'\nimport { RequestContext } from '../context'\nimport { publishMatchedRoute } from '../runtime'\nimport { applyHandlerResult } from '../reply'\nimport { requestStore } from '../request-store'\nimport { createRequestStore, disposeRequestStore } from '../middleware/request-scope'\nimport { validate } from '../middleware/validate'\nimport { applyUploadConfig, type RawUploadPart } from '../middleware/upload-config'\nimport type { RouteEntry, RuntimeResponse } from '../runtime'\nimport { WebRequestShim, WebResponseDriver } from './driver'\nimport { createLogger, describeError } from '../../core/logger'\n\nconst log = createLogger('WebPipeline')\nconst NOOP_NEXT = (): void => {}\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nexport interface WebRouteInvocation {\n request: Request\n url: URL\n params: Record<string, string>\n}\n\nexport interface WebRouteHooks {\n /**\n * Error bridge — receives pipeline rejections (validation failures,\n * handler throws) with the shim + driver so an engine can dispatch the\n * framework's connect-style error handler. When absent (or when it\n * doesn't settle the driver), a minimal JSON error is produced from\n * `err.status`/`err.statusCode` (500 default).\n */\n onError?: (err: unknown, req: WebRequestShim, res: WebResponseDriver) => void | Promise<void>\n}\n\n/**\n * Fallback error shape when no engine error handler settles the response.\n *\n * This path used to be completely silent: a bare\n * `{ error: 'Internal Server Error' }` with no log line anywhere, so a\n * failure that reached here left no trace on either side. It is a\n * last-resort branch, which is exactly when diagnostics matter most.\n */\nfunction defaultErrorResponse(err: unknown, driver: WebResponseDriver): void {\n const status =\n typeof (err as { status?: number })?.status === 'number'\n ? (err as { status: number }).status\n : typeof (err as { statusCode?: number })?.statusCode === 'number'\n ? (err as { statusCode: number }).statusCode\n : 500\n\n if (status >= 500) {\n log.error(err, `Unhandled error in web pipeline — ${describeError(err)}`)\n const stack = (err as Error)?.stack\n driver.status(status).json({\n error: 'Internal Server Error',\n ...(isProduction()\n ? {}\n : {\n message: describeError(err),\n ...(typeof stack === 'string' ? { stack: stack.split('\\n') } : {}),\n }),\n })\n return\n }\n\n driver.status(status).json({ error: (err as Error)?.message ?? 'Request failed' })\n}\n\n/**\n * Read per-call, not at module scope: the web entry is imported once and\n * reused across invocations, and edge runtimes may not expose `process`.\n */\nfunction isProduction(): boolean {\n return typeof process !== 'undefined' && process.env?.NODE_ENV === 'production'\n}\n\n/**\n * Compile a RouteEntry into a `(invocation) => Promise<Response>` function.\n * Everything derivable at route-build time (validator, upload config) is\n * hoisted here — per-request work is shim + store + pipeline only.\n */\nexport function compileWebRoute(\n entry: RouteEntry,\n hooks?: WebRouteHooks,\n): (invocation: WebRouteInvocation) => Promise<Response> {\n const validator = entry.meta.validation ? validate(entry.meta.validation) : undefined\n const upload = entry.meta.upload && entry.meta.upload.mode !== 'none' ? entry.meta.upload : null\n\n return async ({ request, url, params }) => {\n const req = new WebRequestShim(request, url)\n req.params = params\n let bodyError: unknown\n\n // Body: uploads consume the stream as FormData; everything else parses\n // by content-type.\n if (upload) {\n const form = await request.formData().catch(() => undefined)\n if (form) {\n const rawParts: RawUploadPart[] = []\n const fields: Record<string, unknown> = {}\n for (const [name, value] of form.entries()) {\n if (typeof value === 'string') {\n fields[name] = value\n } else {\n rawParts.push({\n fieldname: name,\n filename: value.name ?? '',\n mimetype: value.type || 'application/octet-stream',\n buffer: new Uint8Array(await value.arrayBuffer()) as unknown as Buffer,\n })\n }\n }\n const { file, files } = applyUploadConfig(rawParts, entry.meta.upload!)\n req.file = file\n req.files = files\n req.body = fields\n }\n } else if (BODY_METHODS.has(request.method)) {\n // Read once — a Request body is a stream and cannot be consumed twice.\n // A read that fails (client aborted mid-flight) is treated as an absent\n // body: there is nobody left to answer, and it is not a malformed one.\n const raw = request.body === null ? '' : await request.text().catch(() => '')\n\n // An empty body is ABSENT, not unparseable — the same call the node h3\n // runtime makes on `content-length: 0`. Distinguishing the two is the\n // whole point: `.catch(() => undefined)` around the parse could not,\n // so a malformed payload answered 200 with the handler running against\n // `undefined` (#605), the defect #586 removed from the node runtime.\n const kind = classifyMediaType(req.headers['content-type'])\n if (raw !== '' && kind === 'unsupported') {\n // Held for the same reason as a parse failure: no driver yet.\n bodyError = unsupportedMediaTypeError(req.headers['content-type'])\n } else if (raw !== '' && kind !== 'multipart') {\n if (kind === 'json') {\n try {\n req.body = JSON.parse(raw)\n } catch (err) {\n // Held, not thrown: the driver that turns an error into a response\n // does not exist yet, so throwing here escapes the route entirely\n // and surfaces as a 500. Rethrown as the pipeline's first act.\n bodyError = HttpException.badRequest(\n err instanceof Error ? err.message : 'Request body could not be parsed',\n )\n }\n } else if (kind === 'urlencoded') {\n req.body = Object.fromEntries(new URLSearchParams(raw))\n } else {\n req.body = raw\n }\n }\n }\n\n const store = createRequestStore(req.headers['x-request-id'])\n req.requestId = store.requestId\n\n const driver = new WebResponseDriver(request.signal)\n driver.setHeader('x-request-id', store.requestId)\n\n publishMatchedRoute(req, entry)\n\n const pipeline = requestStore.run(store, async () => {\n if (bodyError) throw bodyError\n\n const ctx = new RequestContext(\n req as never,\n driver as never,\n NOOP_NEXT,\n driver as RuntimeResponse,\n )\n\n // `beforeValidation` contributors (#677) — ahead of validation and middleware.\n if (entry.earlyContributorRunner) {\n await entry.earlyContributorRunner(ctx)\n if (driver.settled) return\n }\n\n if (validator) {\n await new Promise<void>((resolve, reject) => {\n validator(req as never, undefined as never, (err?: unknown) =>\n err ? reject(err) : resolve(),\n )\n })\n }\n\n for (const mw of entry.middlewares) {\n let advanced = false\n await new Promise<void>((resolve, reject) => {\n const next = (err?: unknown): void => {\n advanced = true\n if (err) reject(err)\n else resolve()\n }\n Promise.resolve(mw(ctx, next)).catch(reject)\n })\n if (!advanced || driver.settled) return\n }\n\n if (entry.contributorRunner) await entry.contributorRunner(ctx)\n if (driver.settled) return\n const result = await entry.handler(ctx)\n // Return-value handlers (reply.ts): auto-send when nothing was written.\n // `undefined` falls through to the canonical 404 below.\n if (!driver.settled) applyHandlerResult(ctx, result)\n })\n\n try {\n // Streaming responses resolve `ready` mid-pipeline (SSE); buffered ones\n // resolve at the terminal ctx call. Race so both shapes return promptly.\n try {\n await Promise.race([pipeline, driver.ready])\n } catch (err) {\n if (!driver.settled) {\n if (hooks?.onError) await hooks.onError(err, req, driver)\n if (!driver.settled) defaultErrorResponse(err, driver)\n }\n }\n if (!driver.settled) {\n // Pipeline finished without responding — canonical 404 shape.\n driver.status(404).json({ error: 'Not Found' })\n }\n return await driver.ready\n } finally {\n // @PreDestroy teardown. For buffered responses this runs post-response;\n // for streams it runs when the pipeline (not the stream) completes —\n // stream lifetime is governed by the client abort signal.\n void Promise.resolve(pipeline)\n .catch(() => {})\n .then(() => disposeRequestStore(store))\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"logger-DA43DvNY.d.mts","names":[],"sources":["../src/core/logger.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA+BiB;EACf,KAAK,gBAAgB;EACrB,KAAK,gBAAgB;EACrB,MAAM,gBAAgB;EACtB,MAAM,gBAAgB;EACtB,OAAO,gBAAgB;EACvB,OAAO,gBAAgB;;EAEvB,MAAM;IAAY;MAAsB;;;;;;;;;;;;;;;cAgD7B,iCAAiC;UACpC;UACA;EAER,YAAY;UAMJ;;UASA;EAIR,KAAK,gBAAgB;EAGrB,KAAK,gBAAgB;EAGrB,MAAM,gBAAgB;EAGtB,MAAM,gBAAgB;EAGtB,MAAM,gBAAgB;EAGtB,MAAM,gBAAgB;EAItB,MAAM;IAAY;MAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;cAoC7B;UACH;UACA;UACA;;iBAGO;EAEf,YAAY;;cAOA;;;;;;;;;;;SAkBL,YAAY,UAAU;;SAOtB,eAAe;;;;;SAQf;;SAOA,IAAI,eAAe;;EAU1B,MAAM,eAAe;EAIrB,KAAK,gBAAgB;EAIrB,KAAK,gBAAgB;;EAKrB,MAAM,cAAc,gBAAgB;;EAEpC,MAAM,gBAAgB;;EAEtB,MAAM;EA8BN,MAAM,gBAAgB;EAItB,MAAM,gBAAgB;EAKtB,MAAM,gBAAgB;;;;;;;;;;;iBAeR,cAAc,cAAc;;iBA+B5B,aAAa,eAAe"}
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v9.0.3
3
- *
4
- * Copyright (c) Felix Orinda
5
- *
6
- * This source code is licensed under the MIT license found in the
7
- * LICENSE file in the root directory of this source tree.
8
- *
9
- * @license MIT
10
- */
11
- import{METADATA}from"./core/interfaces.mjs";import{createLogger}from"./core/logger.mjs";import{n as getClassMetaOrUndefined}from"./metadata-CIXErBTj.mjs";import{HttpException}from"./core/errors.mjs";import{t as ROUTE_SLOT}from"./route-flag-CpTa8y1n.mjs";import{l as requestStore}from"./context-fth87C3G.mjs";function normalizeMediaType(contentType){return((Array.isArray(contentType)?contentType[0]:contentType)??``).split(`;`)[0].trim().toLowerCase()}function isJsonMediaType(mediaType){return mediaType===`application/json`||/^application\/[\w.+-]+\+json$/.test(mediaType)}function classifyMediaType(contentType){let mediaType=normalizeMediaType(contentType);return mediaType?isJsonMediaType(mediaType)?`json`:mediaType===`application/x-www-form-urlencoded`?`urlencoded`:mediaType.startsWith(`multipart/`)?`multipart`:mediaType.startsWith(`text/`)?`text`:`unsupported`:`unsupported`}const EXPRESS_JSON_TYPES=[`application/json`,`application/*+json`],SUPPORTED_BODY_TYPES=[`application/json`,`application/*+json`,`application/x-www-form-urlencoded`,`text/*`,`multipart/form-data`];function unsupportedMediaTypeError(contentType){let mediaType=normalizeMediaType(contentType);return new HttpException(415,mediaType?`Unsupported Media Type: ${mediaType}`:`Unsupported Media Type: request has a body but no Content-Type`,void 0,{Accept:SUPPORTED_BODY_TYPES.join(`, `)})}const BODY_METHODS=new Set([`POST`,`PUT`,`PATCH`,`DELETE`]);function rejectUnsupportedBody(){return(req,_res,next)=>{if(!BODY_METHODS.has((req.method??`GET`).toUpperCase())||classifyMediaType(req.headers?.[`content-type`])!==`unsupported`)return next();let reject=()=>next(unsupportedMediaTypeError(req.headers?.[`content-type`])),length=req.headers?.[`content-length`];if(length!==void 0)return length===`0`?next():reject();if(req.headers?.[`transfer-encoding`]===void 0)return next();let decided=!1,settle=fn=>()=>{decided||(decided=!0,req.off?.(`data`,onData),req.off?.(`end`,onEnd),fn())},onData=settle(reject),onEnd=settle(()=>next());req.on?.(`data`,onData),req.on?.(`end`,onEnd)}}const log=createLogger(`RequestScope`),preDestroyCache=new Map;function preDestroyMethod(ctor){let method=preDestroyCache.get(ctor);return method===void 0&&(method=getClassMetaOrUndefined(METADATA.PRE_DESTROY,ctor.prototype)??null,preDestroyCache.set(ctor,method)),method}function disposeRequestStore(store){if(!store.disposed&&(store.disposed=!0,store.instances.size!==0)){for(let instance of store.instances.values()){if(typeof instance!=`object`||!instance)continue;let method=preDestroyMethod(instance.constructor);if(method&&typeof instance[method]==`function`)try{let result=instance[method]();result&&typeof result.then==`function`&&result.catch(err=>log.error(err,`@PreDestroy on ${instance.constructor.name} rejected`))}catch(err){log.error(err,`@PreDestroy on ${instance.constructor.name} threw`)}}store.instances.clear()}}const REQUEST_SCOPE_MIDDLEWARE_MARKER=Symbol.for(`@kickjs/requestScopeMiddleware`);function createRequestStore(requestId){return{requestId:requestId||crypto.randomUUID(),instances:new Map,values:new Map}}function requestScopeMiddleware(){let mw=(req,res,next)=>{let requestIdHeader=req.headers[`x-request-id`],requestId=(Array.isArray(requestIdHeader)?requestIdHeader[0]:requestIdHeader)||crypto.randomUUID(),store=createRequestStore(requestId);req.requestId=requestId,res.once(`close`,()=>disposeRequestStore(store)),requestStore.run(store,next)};return mw[REQUEST_SCOPE_MIDDLEWARE_MARKER]=!0,mw}function isRequestScopeMiddleware(value){return typeof value==`function`&&value[REQUEST_SCOPE_MIDDLEWARE_MARKER]===!0}const REPLY_BRAND=Symbol.for(`kickjs.reply`);function reply(status,body){return{[REPLY_BRAND]:!0,status,body}}reply.ok=body=>reply(200,body),reply.created=body=>reply(201,body),reply.accepted=body=>reply(202,body),reply.noContent=()=>reply(204,void 0);function isReply(value){return typeof value==`object`&&!!value&&value[REPLY_BRAND]===!0}function applyHandlerResult(ctx,result){return result===void 0?!1:isReply(result)?result.status===204?(ctx.noContent(),!0):(ctx.json(result.body,result.status),!0):(ctx.json(result),!0)}function publishMatchedRoute(req,entry){req[ROUTE_SLOT]={method:entry.method,path:entry.path,controller:entry.meta.controller,handlerName:entry.meta.handlerName,flags:entry.meta.flags??EMPTY_FLAGS,pattern:entry.meta.pattern??entry.path}}const EMPTY_FLAGS=new Map;export{createRequestStore as a,requestScopeMiddleware as c,rejectUnsupportedBody as d,unsupportedMediaTypeError as f,reply as i,EXPRESS_JSON_TYPES as l,applyHandlerResult as n,disposeRequestStore as o,isReply as r,isRequestScopeMiddleware as s,publishMatchedRoute as t,classifyMediaType as u};
12
- //# sourceMappingURL=runtime-B4NQGN66.mjs.map