@forinda/kickjs 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/{adapter-diFqgxGR.d.mts → adapter-NstPNaFV.d.mts} +36 -3
  2. package/dist/{adapter-diFqgxGR.d.mts.map → adapter-NstPNaFV.d.mts.map} +1 -1
  3. package/dist/{application-Bflo42Wr.mjs → application-BDtFj66k.mjs} +3 -3
  4. package/dist/application-BDtFj66k.mjs.map +1 -0
  5. package/dist/{application-D_mReBi_.d.mts → application-CnJh2Bry.d.mts} +55 -15
  6. package/dist/application-CnJh2Bry.d.mts.map +1 -0
  7. package/dist/{bootstrap-DtlMl3-0.mjs → bootstrap-CJbP4DXN.mjs} +3 -3
  8. package/dist/{bootstrap-DtlMl3-0.mjs.map → bootstrap-CJbP4DXN.mjs.map} +1 -1
  9. package/dist/{bootstrap-CytCvAnr.d.mts → bootstrap-Iq_WR08c.d.mts} +2 -2
  10. package/dist/bootstrap-Iq_WR08c.d.mts.map +1 -0
  11. package/dist/client-ip-Di2MrjCQ.mjs +12 -0
  12. package/dist/client-ip-Di2MrjCQ.mjs.map +1 -0
  13. package/dist/config/index.mjs +2 -2
  14. package/dist/{config-Dx_Nbh79.mjs → config-BQgwhAlJ.mjs} +3 -3
  15. package/dist/{config-Dx_Nbh79.mjs.map → config-BQgwhAlJ.mjs.map} +1 -1
  16. package/dist/{container-BmZciE3k.mjs → container-BITnQkhU.mjs} +3 -3
  17. package/dist/{container-BmZciE3k.mjs.map → container-BITnQkhU.mjs.map} +1 -1
  18. package/dist/context-DYOcPcg8.mjs +12 -0
  19. package/dist/context-DYOcPcg8.mjs.map +1 -0
  20. package/dist/{context-errors-DDYA51Nf.mjs → context-errors-Bf2NqTS3.mjs} +2 -2
  21. package/dist/{context-errors-DDYA51Nf.mjs.map → context-errors-Bf2NqTS3.mjs.map} +1 -1
  22. package/dist/core/adapter.d.mts +1 -1
  23. package/dist/core/adapter.mjs +1 -1
  24. package/dist/core/app-module.mjs +1 -1
  25. package/dist/core/cache.mjs +1 -1
  26. package/dist/core/container.mjs +2 -2
  27. package/dist/core/cron.mjs +2 -2
  28. package/dist/core/decorators.mjs +2 -2
  29. package/dist/core/errors.mjs +1 -1
  30. package/dist/core/interfaces.mjs +1 -1
  31. package/dist/core/logger.mjs +1 -1
  32. package/dist/core/path.mjs +1 -1
  33. package/dist/core/plugin.d.mts +1 -1
  34. package/dist/core/plugin.mjs +1 -1
  35. package/dist/core/reactivity.mjs +1 -1
  36. package/dist/{core-kFuUx8FW.mjs → core-MI5Y1nGg.mjs} +3 -3
  37. package/dist/{core-kFuUx8FW.mjs.map → core-MI5Y1nGg.mjs.map} +1 -1
  38. package/dist/csrf-BAnc9Ww7.mjs +12 -0
  39. package/dist/csrf-BAnc9Ww7.mjs.map +1 -0
  40. package/dist/csrf-vR23CJas.d.mts.map +1 -1
  41. package/dist/{define-adapter-D3dAhxuu.mjs → define-adapter-Cz-bj4Xk.mjs} +2 -2
  42. package/dist/{define-adapter-D3dAhxuu.mjs.map → define-adapter-Cz-bj4Xk.mjs.map} +1 -1
  43. package/dist/{disposables-C4BbFv_2.mjs → disposables-0gJffTe_.mjs} +2 -2
  44. package/dist/{disposables-C4BbFv_2.mjs.map → disposables-0gJffTe_.mjs.map} +1 -1
  45. package/dist/{env-C5TMXJ7h.mjs → env-IfTg0Rv1.mjs} +3 -3
  46. package/dist/{env-C5TMXJ7h.mjs.map → env-IfTg0Rv1.mjs.map} +1 -1
  47. package/dist/{error-handler-Cr6Yt9as.d.mts → error-handler-p_T3hhiD.d.mts} +19 -4
  48. package/dist/error-handler-p_T3hhiD.d.mts.map +1 -0
  49. package/dist/{handler-BKqb8K_H.mjs → handler-mLCYXfTQ.mjs} +3 -3
  50. package/dist/{handler-BKqb8K_H.mjs.map → handler-mLCYXfTQ.mjs.map} +1 -1
  51. package/dist/http/application.d.mts +2 -2
  52. package/dist/http/application.mjs +2 -2
  53. package/dist/http/bootstrap.d.mts +1 -1
  54. package/dist/http/bootstrap.mjs +2 -2
  55. package/dist/http/context.d.mts +1 -1
  56. package/dist/http/context.mjs +2 -2
  57. package/dist/http/middleware/cors.mjs +1 -1
  58. package/dist/http/middleware/csrf.mjs +2 -2
  59. package/dist/http/middleware/error-handler.d.mts +1 -1
  60. package/dist/http/middleware/error-handler.mjs +2 -2
  61. package/dist/http/middleware/error-handler.mjs.map +1 -1
  62. package/dist/http/middleware/helmet.mjs +1 -1
  63. package/dist/http/middleware/rate-limit.mjs +2 -2
  64. package/dist/http/middleware/rate-limit.mjs.map +1 -1
  65. package/dist/http/middleware/request-id.mjs +1 -1
  66. package/dist/http/middleware/request-logger.d.mts +1 -1
  67. package/dist/http/middleware/request-logger.mjs +2 -2
  68. package/dist/http/middleware/request-logger.mjs.map +1 -1
  69. package/dist/http/middleware/session.mjs +2 -2
  70. package/dist/http/middleware/spa.d.mts +1 -1
  71. package/dist/http/middleware/spa.mjs +2 -2
  72. package/dist/http/middleware/upload.d.mts +1 -1
  73. package/dist/http/middleware/upload.mjs +2 -2
  74. package/dist/http/middleware/validate.mjs +1 -1
  75. package/dist/http/middleware/views.d.mts +1 -1
  76. package/dist/http/middleware/views.mjs +2 -2
  77. package/dist/http/query/index.mjs +2 -2
  78. package/dist/http/router-builder.d.mts +1 -1
  79. package/dist/http/router-builder.mjs +2 -2
  80. package/dist/http/runtimes/fastify.d.mts +1 -1
  81. package/dist/http/runtimes/fastify.mjs +2 -2
  82. package/dist/http/runtimes/h3-web.d.mts +1 -1
  83. package/dist/http/runtimes/h3-web.mjs +2 -2
  84. package/dist/http/runtimes/h3.d.mts +1 -1
  85. package/dist/http/runtimes/h3.mjs +2 -2
  86. package/dist/index.d.mts +9 -9
  87. package/dist/index.mjs +2 -2
  88. package/dist/kv-stores-DNeEYG-J.mjs +12 -0
  89. package/dist/kv-stores-DNeEYG-J.mjs.map +1 -0
  90. package/dist/{kv-stores-D2kSUAEP.d.mts → kv-stores-pWdw8RKf.d.mts} +3 -3
  91. package/dist/kv-stores-pWdw8RKf.d.mts.map +1 -0
  92. package/dist/{metadata-Cf6JPQax.mjs → metadata-CyyYO_qa.mjs} +2 -2
  93. package/dist/{metadata-Cf6JPQax.mjs.map → metadata-CyyYO_qa.mjs.map} +1 -1
  94. package/dist/{module-registry-Cpf01UdE.mjs → module-registry-BfOa58-E.mjs} +2 -2
  95. package/dist/{module-registry-Cpf01UdE.mjs.map → module-registry-BfOa58-E.mjs.map} +1 -1
  96. package/dist/{query-tx53xRSZ.mjs → query-BYUCKdu5.mjs} +2 -2
  97. package/dist/{query-tx53xRSZ.mjs.map → query-BYUCKdu5.mjs.map} +1 -1
  98. package/dist/rate-limit-Dme747JJ.d.mts.map +1 -1
  99. package/dist/{reply-dxkvXqNo.mjs → reply-Bo950wuK.mjs} +3 -3
  100. package/dist/{reply-dxkvXqNo.mjs.map → reply-Bo950wuK.mjs.map} +1 -1
  101. package/dist/{request-logger-BU4ulKLp.d.mts → request-logger-D_pf4xzE.d.mts} +21 -3
  102. package/dist/request-logger-D_pf4xzE.d.mts.map +1 -0
  103. package/dist/{router-builder-C6E11Grf.d.mts → router-builder-BYDUIiZW.d.mts} +2 -2
  104. package/dist/{router-builder-C6E11Grf.d.mts.map → router-builder-BYDUIiZW.d.mts.map} +1 -1
  105. package/dist/{router-builder-BjLsf6w0.mjs → router-builder-Bv175XoR.mjs} +3 -3
  106. package/dist/{router-builder-BjLsf6w0.mjs.map → router-builder-Bv175XoR.mjs.map} +1 -1
  107. package/dist/{spa-DJbVV_4F.d.mts → spa-CDQ74RGA.d.mts} +8 -9
  108. package/dist/spa-CDQ74RGA.d.mts.map +1 -0
  109. package/dist/{upload-Wck12jE5.d.mts → upload-CUtcjc-n.d.mts} +2 -2
  110. package/dist/{upload-Wck12jE5.d.mts.map → upload-CUtcjc-n.d.mts.map} +1 -1
  111. package/dist/{upload-config-BlmVJI4n.mjs → upload-config-C9V9tIR3.mjs} +2 -2
  112. package/dist/{upload-config-BlmVJI4n.mjs.map → upload-config-C9V9tIR3.mjs.map} +1 -1
  113. package/dist/web.d.mts +2 -2
  114. package/dist/web.mjs +2 -2
  115. package/package.json +5 -5
  116. package/dist/application-Bflo42Wr.mjs.map +0 -1
  117. package/dist/application-D_mReBi_.d.mts.map +0 -1
  118. package/dist/bootstrap-CytCvAnr.d.mts.map +0 -1
  119. package/dist/context-vG6HXwHB.mjs +0 -12
  120. package/dist/context-vG6HXwHB.mjs.map +0 -1
  121. package/dist/csrf-Dna2I7cx.mjs +0 -12
  122. package/dist/csrf-Dna2I7cx.mjs.map +0 -1
  123. package/dist/error-handler-Cr6Yt9as.d.mts.map +0 -1
  124. package/dist/kv-stores-D2kSUAEP.d.mts.map +0 -1
  125. package/dist/kv-stores-zApd2iLD.mjs +0 -12
  126. package/dist/kv-stores-zApd2iLD.mjs.map +0 -1
  127. package/dist/request-logger-BU4ulKLp.d.mts.map +0 -1
  128. package/dist/spa-DJbVV_4F.d.mts.map +0 -1
package/dist/web.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.0.0
2
+ * @forinda/kickjs v7.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{t as Container}from"./container-BmZciE3k.mjs";import{t as MutableModuleRegistry}from"./module-registry-Cpf01UdE.mjs";import{normalizePath}from"./core/path.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-BjLsf6w0.mjs";import{i as requestStore}from"./context-vG6HXwHB.mjs";import{n as isReply,r as reply}from"./reply-dxkvXqNo.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-zApd2iLD.mjs";import{n as WebRequestShim,r as WebResponseDriver,t as compileWebRoute}from"./handler-BKqb8K_H.mjs";function createWebApp(options){let h3mod=options.h3;if(typeof h3mod?.H3!=`function`)throw Error(`@forinda/kickjs/web: pass the h3 v2 module — \`createWebApp({ h3: await import("h3"), ... })\`. The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.`);let container=Container.getInstance();if(options.env){let env=options.env;Container._envResolver=key=>env[key]}Container._requestStoreProvider=()=>requestStore.getStore()??null;let registry=new MutableModuleRegistry;for(let m of options.modules)registry.mount(m);let modules=registry.entries.map(entry=>{let mod=typeof entry==`function`?new entry:entry;return mod.register?.(container),mod});container.bootstrap();let app=new h3mod.H3,apiPrefix=options.apiPrefix??`/api`,defaultVersion=options.defaultVersion??1,globalMiddleware=options.middleware??[],globalSources=(options.contributors??[]).map(registration=>({source:`global`,registration,label:`createWebApp`})),seenRoutes=new Map;for(let mod of modules){let declaredName=mod.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:mod.constructor?.name??`module`;_setExternalContributorSources([...(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel})),...globalSources]);try{let result=mod.routes();if(!result)continue;let routeSets=Array.isArray(result)?result:[result];for(let route of routeSets){if(!route.controller)throw Error(`@forinda/kickjs/web: module route at '${route.path}' has no controller. Express-style \`router\` mounts are not supported on the web entry.`);let mountPath=`${apiPrefix}/v${route.version??defaultVersion}${normalizePath(route.path)}`;for(let entry of buildRouteTable(route.controller)){let url=joinPath(mountPath,entry.path),owner=`${route.controller.name??`controller`}.${String(entry.meta.handlerName??`?`)}`;assertRouteUnique(seenRoutes,entry.method,url,owner),globalMiddleware.length>0&&(entry.middlewares=[...globalMiddleware,...entry.middlewares]);let run=compileWebRoute(entry);app.on(entry.method,url,event=>run({request:event.req,url:event.url,params:event.context.params??{}}))}}}finally{_setExternalContributorSources([])}}return{fetch:request=>app.fetch(request),h3:app}}function createFetchHandler(build){let app;return{fetch:(request,env={})=>(app??=createWebApp(build(env)),app.fetch(request))}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{KvRateLimitStore,KvSessionStore,WebRequestShim,WebResponseDriver,compileWebRoute,createFetchHandler,createWebApp,isReply,rateLimitGuard,reply};
11
+ import{t as Container}from"./container-BITnQkhU.mjs";import{t as MutableModuleRegistry}from"./module-registry-BfOa58-E.mjs";import{normalizePath}from"./core/path.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-Bv175XoR.mjs";import{i as requestStore}from"./context-DYOcPcg8.mjs";import{n as isReply,r as reply}from"./reply-Bo950wuK.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-DNeEYG-J.mjs";import{n as WebRequestShim,r as WebResponseDriver,t as compileWebRoute}from"./handler-mLCYXfTQ.mjs";function createWebApp(options){let h3mod=options.h3;if(typeof h3mod?.H3!=`function`)throw Error(`@forinda/kickjs/web: pass the h3 v2 module — \`createWebApp({ h3: await import("h3"), ... })\`. The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.`);let container=Container.getInstance();if(options.env){let env=options.env;Container._envResolver=key=>env[key]}Container._requestStoreProvider=()=>requestStore.getStore()??null;let registry=new MutableModuleRegistry;for(let m of options.modules)registry.mount(m);let modules=registry.entries.map(entry=>{let mod=typeof entry==`function`?new entry:entry;return mod.register?.(container),mod});container.bootstrap();let app=new h3mod.H3,apiPrefix=options.apiPrefix??`/api`,defaultVersion=options.defaultVersion??1,globalMiddleware=options.middleware??[],globalSources=(options.contributors??[]).map(registration=>({source:`global`,registration,label:`createWebApp`})),seenRoutes=new Map;for(let mod of modules){let declaredName=mod.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:mod.constructor?.name??`module`;_setExternalContributorSources([...(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel})),...globalSources]);try{let result=mod.routes();if(!result)continue;let routeSets=Array.isArray(result)?result:[result];for(let route of routeSets){if(!route.controller)throw Error(`@forinda/kickjs/web: module route at '${route.path}' has no controller. Express-style \`router\` mounts are not supported on the web entry.`);let mountPath=`${apiPrefix}/v${route.version??defaultVersion}${normalizePath(route.path)}`;for(let entry of buildRouteTable(route.controller)){let url=joinPath(mountPath,entry.path),owner=`${route.controller.name??`controller`}.${String(entry.meta.handlerName??`?`)}`;assertRouteUnique(seenRoutes,entry.method,url,owner),globalMiddleware.length>0&&(entry.middlewares=[...globalMiddleware,...entry.middlewares]);let run=compileWebRoute(entry);app.on(entry.method,url,event=>run({request:event.req,url:event.url,params:event.context.params??{}}))}}}finally{_setExternalContributorSources([])}}return{fetch:request=>app.fetch(request),h3:app}}function createFetchHandler(build){let app;return{fetch:(request,env={})=>(app??=createWebApp(build(env)),app.fetch(request))}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{KvRateLimitStore,KvSessionStore,WebRequestShim,WebResponseDriver,compileWebRoute,createFetchHandler,createWebApp,isReply,rateLimitGuard,reply};
12
12
  //# sourceMappingURL=web.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "A production-grade, decorator-driven Node.js framework for TypeScript — runs on Express, Fastify, or h3 (swap the engine in one line).",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -154,18 +154,18 @@
154
154
  "devDependencies": {
155
155
  "@fastify/middie": "^9.3.3",
156
156
  "@fastify/multipart": "^10.1.0",
157
- "@swc/core": "^1.15.46",
157
+ "@swc/core": "^1.15.47",
158
158
  "@types/express": "^5.0.6",
159
159
  "@types/multer": "^2.2.0",
160
- "@types/node": "^26.1.1",
160
+ "@types/node": "^26.1.2",
161
161
  "@types/serve-static": "^2.2.0",
162
- "fastify": "^5.10.0",
162
+ "fastify": "^5.11.2",
163
163
  "h3": "1.15.11",
164
164
  "h3-v2": "npm:h3@2.0.1-rc.22",
165
165
  "multer": "^2.2.0",
166
166
  "serve-static": "^2.2.0",
167
167
  "typescript": "^7.0.2",
168
- "vite": "^8.1.5",
168
+ "vite": "^8.2.0",
169
169
  "vitest": "^4.1.10",
170
170
  "zod": "^4.4.3"
171
171
  },
@@ -1 +0,0 @@
1
- {"version":3,"file":"application-Bflo42Wr.mjs","names":[],"sources":["../src/core/mount-sort.ts","../src/http/runtimes/express.ts","../src/http/application.ts"],"sourcesContent":["/**\n * Topological sort for plugins and adapters with `dependsOn` declarations.\n *\n * Mirrors the algorithm used by the Context Contributor pipeline\n * (`contributor-pipeline.ts:topoSort`) but operates on a name-keyed\n * `MountSortItem` shape rather than a `ContributorRegistration`. Kept\n * standalone instead of factored generic so that:\n *\n * 1. The contributor pipeline doesn't need refactoring (risk reduction).\n * 2. The error classes can carry plugin/adapter-specific diagnostic\n * fields (`kind`, `dependent`) without bending the contributor types.\n */\n\n/**\n * A plugin or adapter that participates in dependency-ordered mounting.\n * Both `KickPlugin` and `AppAdapter` satisfy this shape via their `name`\n * and `dependsOn` fields.\n */\nexport interface MountSortItem {\n /** Unique mount name within the plugin/adapter set being sorted. */\n name: string\n /**\n * Other plugin/adapter names that must mount before this one. Cycles\n * and missing names fail at boot — same fail-fast contract as the\n * Context Contributor pipeline (architecture.md §20.4).\n */\n dependsOn?: readonly string[]\n}\n\n/** What kind of mount set the sort is operating on — surfaces in error messages. */\nexport type MountKind = 'plugin' | 'adapter'\n\n/**\n * Two items in the same mount set declared the same `name`. Plugin/adapter\n * names must be unique so `dependsOn` references resolve unambiguously.\n */\nexport class DuplicateMountNameError extends Error {\n readonly kind: MountKind\n readonly mountName: string\n\n constructor(kind: MountKind, mountName: string) {\n super(`Duplicate ${kind} name '${mountName}' — every ${kind} must declare a unique name`)\n this.name = 'DuplicateMountNameError'\n this.kind = kind\n this.mountName = mountName\n }\n}\n\n/**\n * A `dependsOn` entry references a plugin/adapter that isn't in the\n * current set. The dependency might not be installed, might be misspelled,\n * or might live in a different mount set (plugin referencing an adapter,\n * or vice versa — they sort independently).\n */\nexport class MissingMountDepError extends Error {\n readonly kind: MountKind\n readonly missing: string\n readonly dependent: string\n\n constructor(kind: MountKind, missing: string, dependent: string) {\n super(\n `Missing ${kind} dependency '${missing}' required by '${dependent}' — ` +\n `is the ${kind} installed and named correctly?`,\n )\n this.name = 'MissingMountDepError'\n this.kind = kind\n this.missing = missing\n this.dependent = dependent\n }\n}\n\n/**\n * The `dependsOn` declarations form a cycle. Boot fails so the cycle\n * can't poison live traffic. The `cycle` field reports one path through\n * the offending nodes for diagnostics — formatted `A → B → C → A`.\n */\nexport class MountCycleError extends Error {\n readonly kind: MountKind\n readonly cycle: readonly string[]\n\n constructor(kind: MountKind, cycle: readonly string[]) {\n super(`Cycle in ${kind} dependsOn declarations: ${cycle.join(' → ')}`)\n this.name = 'MountCycleError'\n this.kind = kind\n this.cycle = Object.freeze([...cycle])\n }\n}\n\n/**\n * Topologically sort `items` so every item with `dependsOn: ['X']` mounts\n * after `X`. Items without `dependsOn` keep their input order via Kahn's\n * algorithm with a FIFO queue seeded in input order — preserves the\n * historical \"plugins/adapters mount in declaration order\" behaviour for\n * any set that doesn't use `dependsOn`.\n *\n * @param items - mountable items, e.g. plugins or adapters\n * @param kind - 'plugin' | 'adapter', used in error messages\n * @returns a new array in valid mount order\n * @throws {DuplicateMountNameError} two items share a name\n * @throws {MissingMountDepError} a `dependsOn` references an unknown name\n * @throws {MountCycleError} the dep graph contains a cycle\n */\nexport function mountSort<T extends MountSortItem>(items: readonly T[], kind: MountKind): T[] {\n const byName = new Map<string, T>()\n for (const item of items) {\n if (byName.has(item.name)) {\n throw new DuplicateMountNameError(kind, item.name)\n }\n byName.set(item.name, item)\n }\n\n // indegree[name] = number of unresolved deps for that item\n const indegree = new Map<string, number>()\n // dependents[depName] = items that depend on depName\n const dependents = new Map<string, T[]>()\n\n for (const item of items) {\n indegree.set(item.name, item.dependsOn?.length ?? 0)\n }\n for (const item of items) {\n for (const dep of item.dependsOn ?? []) {\n if (!byName.has(dep)) {\n throw new MissingMountDepError(kind, dep, item.name)\n }\n const list = dependents.get(dep) ?? []\n list.push(item)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: items with zero indegree, preserving input order.\n const queue: T[] = []\n for (const item of items) {\n if (indegree.get(item.name) === 0) queue.push(item)\n }\n\n const sorted: T[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.name) ?? []) {\n const next = (indegree.get(dependent.name) ?? 0) - 1\n indegree.set(dependent.name, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== items.length) {\n const remaining = items.filter((it) => (indegree.get(it.name) ?? 0) > 0)\n throw new MountCycleError(kind, reconstructCycle(remaining, byName))\n }\n\n return sorted\n}\n\n/**\n * Walk the residual graph to find one cycle path for diagnostics.\n * Starts from the first remaining node and follows the first dep that's\n * still in the residual set until we revisit a node. Returns a path\n * starting and ending with the same name so the message reads naturally\n * (`A → B → C → A`).\n */\nfunction reconstructCycle<T extends MountSortItem>(\n residual: readonly T[],\n byName: ReadonlyMap<string, T>,\n): string[] {\n const residualNames = new Set(residual.map((r) => r.name))\n const start = residual[0].name\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor = start\n\n while (true) {\n const item = byName.get(cursor)!\n const nextDep = item.dependsOn?.find((d) => residualNames.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n const cycleStart = path.indexOf(nextDep)\n return [...path.slice(cycleStart), nextDep]\n }\n path.push(nextDep)\n seen.add(nextDep)\n cursor = nextDep\n }\n\n return path\n}\n","// The default Express runtime — the M1 reference implementation of HttpRuntime.\n//\n// Its route materializer rebuilds the exact `(req, res, next)` handler chain\n// the old `buildRoutes()` produced, so swapping decorators → RouteTable →\n// runtime is behavior-neutral under Express. `buildRoutes()` (the public,\n// Router-returning API) now lives here as a thin shim over `buildRouteTable()`\n// + this materializer.\n\nimport express, {\n Router,\n type Express,\n type Request,\n type Response,\n type NextFunction,\n type RequestHandler,\n} from 'express'\n\nimport { buildRouteTable, type BuildRoutesOptions } from '../router-builder'\nimport { RequestContext } from '../context'\nimport { applyHandlerResult } from '../reply'\nimport { validate } from '../middleware/validate'\nimport { buildUploadMiddleware } from '../middleware/upload'\nimport type {\n ConnectMiddleware,\n HttpRuntime,\n RouteEntry,\n RouteTable,\n RuntimeAppOptions,\n UseConnectOptions,\n} from '../runtime'\n\n/**\n * Materialize a controller's {@link RouteEntry}[] onto a fresh `express.Router`.\n * Reproduces the legacy handler ordering precisely: validation → upload →\n * `(ctx, next)` middleware → contributor runner → terminal handler. Each step\n * constructs its own `RequestContext` over the same `req`/`res` — request-scoped\n * state lives in the AsyncLocalStorage store, not on the ctx instance, so the\n * per-step instances share state exactly as before.\n */\nexport function materializeRouter(entries: RouteEntry[]): Router {\n const router = Router()\n\n for (const entry of entries) {\n const method = entry.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch'\n const handlers: RequestHandler[] = []\n\n // Validation middleware (shared with the standalone validate() export).\n if (entry.meta.validation) {\n handlers.push(validate(entry.meta.validation))\n }\n\n // @FileUpload — auto-attach upload middleware from metadata.\n if (entry.meta.upload) {\n handlers.push(buildUploadMiddleware(entry.meta.upload))\n }\n\n // Class + method middleware — wrapped as Express middleware with error catching.\n for (const mw of entry.middlewares) {\n handlers.push((req: Request, res: Response, next: NextFunction) => {\n const ctx = new RequestContext(req, res, next)\n Promise.resolve(mw(ctx, next)).catch(next)\n })\n }\n\n // Context Contributor pipeline (#107) — runs, then advances the chain.\n if (entry.contributorRunner) {\n const run = entry.contributorRunner\n handlers.push(async (req: Request, res: Response, next: NextFunction) => {\n const ctx = new RequestContext(req, res, next)\n try {\n await run(ctx)\n next()\n } catch (err) {\n next(err)\n }\n })\n }\n\n // Terminal handler.\n handlers.push(async (req: Request, res: Response, next: NextFunction) => {\n const ctx = new RequestContext(req, res, next)\n try {\n const result = await entry.handler(ctx)\n // Return-value handlers (reply.ts): auto-send the returned payload\n // when the handler wrote nothing. `undefined` keeps prior behavior.\n if (!res.headersSent) applyHandlerResult(ctx, result)\n } catch (err) {\n next(err)\n }\n })\n ;(router as any)[method](entry.path, ...handlers)\n }\n\n return router\n}\n\n/**\n * Build an Express Router from a controller class decorated with @Get, @Post,\n * etc. The public, back-compatible entry point: a thin shim over\n * {@link buildRouteTable} + {@link materializeRouter}. Returns an\n * `express.Router` exactly as before — modules pass it to `app.use(path, router)`.\n *\n * @see buildRouteTable for the engine-neutral route-table form.\n */\nexport function buildRoutes(controllerClass: any, options: BuildRoutesOptions = {}): Router {\n return materializeRouter(buildRouteTable(controllerClass, options))\n}\n\n/**\n * The default HTTP runtime. Express stays a peer dependency of `@forinda/kickjs`;\n * apps that pass no `runtime` get this and behave exactly as before.\n */\nexport function expressRuntime(): HttpRuntime<Express> {\n return {\n name: 'express',\n\n createApp(options: RuntimeAppOptions = {}): Express {\n const app = express()\n // Engine hardening + config the Application used to apply directly.\n app.disable('x-powered-by')\n app.set('trust proxy', options.trustProxy ?? false)\n return app\n },\n\n nodeHandler(app) {\n return (req, res, next) => {\n if (next) {\n ;(app as any)(req, res, next)\n } else {\n ;(app as any)(req, res)\n }\n }\n },\n\n mountRoutes(app, table: RouteTable) {\n for (const { mountPath, routes } of table) {\n app.use(mountPath, materializeRouter(routes))\n }\n },\n\n useConnect(app, mw: ConnectMiddleware, opts: UseConnectOptions = {}) {\n if (opts.path !== undefined) {\n ;(app as any).use(opts.path, mw)\n } else {\n ;(app as any).use(mw)\n }\n },\n\n serveStatic(app, prefix, dir) {\n app.use(prefix, express.static(dir))\n },\n\n setNotFound(app, mw: ConnectMiddleware) {\n ;(app as any).use(mw)\n },\n\n setErrorHandler(app, mw: ConnectMiddleware) {\n ;(app as any).use(mw)\n },\n\n capabilities: {\n render: true,\n uploads: true,\n connectMiddleware: true,\n nativeBodyParsing: false,\n },\n }\n}\n","import http from 'node:http'\nimport express, { type Express, type RequestHandler } from 'express'\nimport {\n Container,\n createLogger,\n Logger,\n normalizePath,\n joinPaths,\n tokenName,\n METADATA,\n type AppModule,\n type AppModuleClass,\n type AppModuleEntry,\n type AppAdapter,\n type AdapterContext,\n type AdapterMiddleware,\n type ContributorRegistrations,\n type KickPlugin,\n type ModuleRegistry,\n type RouteDefinition,\n type SourcedRegistration,\n mountSort,\n MutableModuleRegistry,\n} from '../core'\nimport { moduleRouteMissingControllerError } from '../core/kick-errors'\nimport {\n disposeAll,\n drainDisposables,\n registerDisposable,\n runDisposables,\n} from '../core/disposables'\nimport { getClassMeta } from '../core/metadata'\nimport { requestId } from './middleware/request-id'\nimport { notFoundHandler, errorHandler } from './middleware/error-handler'\nimport { requestScopeMiddleware, isRequestScopeMiddleware } from './middleware/request-scope'\nimport {\n _setExternalContributorSources,\n assertRouteUnique,\n buildRouteTable,\n} from './router-builder'\nimport { expressRuntime } from './runtimes/express'\nimport type {\n ActiveRuntime,\n AdapterHttp,\n HttpRuntime,\n RouteEntry,\n RuntimeCapabilities,\n} from './runtime'\nimport { requestStore } from './request-store'\n\nconst log = createLogger('Application')\n\n/**\n * A middleware entry in the declarative pipeline.\n * Can be a bare handler or an object with path scoping.\n */\nexport type MiddlewareEntry = RequestHandler | { path: string; handler: RequestHandler }\n\nexport interface ApplicationOptions {\n /**\n * Feature modules to load. Accepts both class form\n * (`SomeModule extends AppModule`, instantiated via `new`) and\n * instance form (output of {@link defineModule}'s factory call,\n * e.g. `TasksModule({ scope: 'admin' })`) — the loader discriminates\n * and either `new`-s the class or uses the instance directly.\n *\n * Static path — for conditional / dynamic registration, use the\n * {@link ApplicationOptions.setup} callback instead.\n */\n modules: AppModuleEntry[]\n\n /**\n * Imperative module registration callback. Receives a\n * {@link ModuleRegistry} you call `.mount(module)` on to register\n * modules conditionally — based on env flags, runtime config, a\n * tenant list, etc. Runs after every plugin's `setup()` hook and\n * after the static `modules: [...]` array is collected.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules: [HelloModule()],\n * setup(registry) {\n * if (env.ENABLE_ADMIN) registry.mount(AdminModule())\n * },\n * })\n * ```\n */\n setup?(registry: ModuleRegistry): void\n /** Adapters that hook into the lifecycle (DB, Redis, Swagger, etc.) */\n adapters?: AppAdapter[]\n /** Server port (falls back to PORT env var, then 3000) */\n port?: number\n /** Global API prefix (default: '/api') */\n apiPrefix?: string\n /** Default API version (default: 1) — routes become /{prefix}/v{version}/{path} */\n defaultVersion?: number\n\n /**\n * Global middleware pipeline. Declared in order.\n * Replaces the hardcoded middleware stack — you control exactly what runs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * middlewares: [\n * helmet(),\n * cors(),\n * compression(),\n * morgan('dev'),\n * express.json({ limit: '1mb' }),\n * ],\n * })\n * ```\n *\n * If omitted, a sensible default is applied:\n * requestId(), express.json({ limit: '100kb' })\n */\n middlewares?: MiddlewareEntry[]\n /**\n * @deprecated Use {@link ApplicationOptions.middlewares} (plural).\n * Kept as an alias for back-compat; `middlewares` wins when both are\n * set.\n */\n middleware?: MiddlewareEntry[]\n\n /** Plugins that bundle modules, adapters, middleware, and DI bindings */\n plugins?: KickPlugin[]\n\n /**\n * Global Context Contributors (#107) that apply to every route in the\n * application. Merge into the per-route pipeline at the `'global'`\n * precedence level — they lose to module, adapter, class, and method\n * contributors with the same key, providing app-wide defaults that any\n * narrower scope can override.\n *\n * @example\n * ```ts\n * const StartedAt = defineContextDecorator({\n * key: 'requestStartedAt',\n * resolve: () => Date.now(),\n * })\n *\n * bootstrap({\n * modules,\n * contributors: [StartedAt.registration],\n * })\n * ```\n */\n contributors?: ContributorRegistrations\n\n /**\n * Backing store strategy for {@link RequestContext} `set/get` and the\n * Context Contributor pipeline (#107).\n *\n * - `'auto'` (default) — Application mounts {@link requestScopeMiddleware}\n * automatically before any user middleware. If the user-supplied\n * `middleware` list already includes `requestScopeMiddleware()`,\n * detection skips the auto-mount so adopters can control its position.\n * - `'manual'` — Application never mounts the wrapper. The Context\n * Contributor pipeline still runs on every route — the runner is\n * inserted by `router-builder` regardless of ALS state. What\n * degrades without an ALS frame is the *backing store*:\n * REQUEST-scoped DI throws (no `requestStore.getStore()` to read\n * from) and `RequestContext.set/get` throws. Use `'manual'` only when you\n * genuinely intend to wrap requests in your own ALS frame (rare —\n * multi-tenant adapters used to do this; post-Phase 3 they share\n * the framework's frame instead).\n */\n contextStore?: 'auto' | 'manual'\n\n /**\n * Controls whether KickJS registers process-level handlers at bootstrap.\n *\n * - `'auto'` (default) — register `uncaughtException` /\n * `unhandledRejection` loggers AND `SIGINT` / `SIGTERM` →\n * `app.shutdown()` → `process.exit(0)`.\n * - `'errors-only'` — register the error loggers; skip the signal\n * handlers. Use this when you have your own shutdown choreographer\n * (OpenTelemetry SDK, Sentry, custom orchestrator) that calls\n * `app.shutdown()` itself and you don't want two listeners racing\n * to call `process.exit`.\n * - `'manual'` — skip everything. Adopter is responsible for both\n * error logging and signal-driven shutdown.\n *\n * Background: many observability SDKs install their own SIGTERM\n * handler to flush spans/metrics before the process exits. Two\n * separate handlers calling `process.exit(0)` race each other and\n * can truncate the in-flight flush. Setting this to `'errors-only'`\n * or `'manual'` lets the SDK own the lifecycle while still letting\n * KickJS surface uncaught exceptions / rejections via the logger.\n *\n * The dev mode auto-detect (`__kickjs_httpServer` set by the Vite\n * plugin) suppresses signal registration regardless of this option,\n * since Vite owns the lifecycle in development.\n */\n processHooks?: 'auto' | 'errors-only' | 'manual'\n\n /**\n * The HTTP engine driver. Defaults to {@link expressRuntime} — Express stays\n * the zero-config default, so existing apps are unaffected.\n *\n * Any `HttpRuntime` is accepted — pass `fastifyRuntime()` (from\n * `@forinda/kickjs/fastify`) or a custom engine driver. Defaults to\n * {@link expressRuntime}. The engine-native escape hatches\n * (`getRuntimeApp()`, `AdapterContext.app`) follow the active runtime via the\n * registry (`ActiveRuntime`, spec §4.3b) — Express by default, or the augmented\n * engine once the `kick/runtime` typegen output is present.\n */\n runtime?: HttpRuntime\n\n /** Express `trust proxy` setting */\n trustProxy?: boolean | number | string | ((ip: string, hopIndex: number) => boolean)\n /** Maximum JSON body size (only used when middleware is not provided) */\n jsonLimit?: string | number\n /**\n * Print the route table on startup. Default: `false`. Set to `true`\n * to log a per-controller summary (method counts + mount paths) once\n * the app has mounted. Emitted at `info` level, so it is still subject\n * to `LOG_LEVEL` filtering (visible at the default `info`; hidden at\n * `warn`/`error`/`silent`).\n */\n logRouteTable?: boolean\n /**\n * @deprecated Use {@link ApplicationOptions.logRouteTable}. Kept as an\n * alias for back-compat; `logRouteTable` wins when both are set.\n */\n logRoutesTable?: boolean\n /**\n * Maximum time (ms) to wait for graceful shutdown before forcing exit.\n * Default: 30000 (30 seconds). Set to 0 to disable forced exit.\n */\n shutdownTimeout?: number\n\n /**\n * Enable cluster mode for multi-core utilization.\n *\n * When enabled, the primary process forks worker processes that share the\n * same port via Node's built-in `cluster` module (OS load balancing).\n *\n * - `true` — use all available CPU cores\n * - `{ workers: N }` — use exactly N workers\n *\n * Workers are auto-restarted on crash. SIGTERM/SIGINT on the primary is\n * forwarded to all workers.\n *\n * @example\n * ```ts\n * bootstrap({ modules, cluster: true })\n * bootstrap({ modules, cluster: { workers: 4 } })\n * ```\n */\n cluster?: boolean | { workers?: number }\n\n /**\n * Custom 404 handler for unmatched routes. Receives the raw Express\n * `(req, res, next)` args. When omitted, the built-in handler returns\n * `{ message: 'Not Found' }` with status 404.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * onNotFound: (req, res) => {\n * res.status(404).json({ error: 'Route not found', path: req.originalUrl })\n * },\n * })\n * ```\n */\n onNotFound?: (req: any, res: any, next: any) => void\n\n /**\n * Custom global error handler. Receives `(err, req, res, next)` — the\n * standard Express error-handling signature. When omitted, the built-in\n * handler formats ZodError, HttpException, and unexpected errors.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * onError: (err, req, res, next) => {\n * logger.error(err)\n * res.status(err.status ?? 500).json({ error: err.message })\n * },\n * })\n * ```\n */\n onError?: (err: any, req: any, res: any, next: any) => void\n\n /**\n * Security defaults applied automatically unless opted out.\n *\n * When not specified, secure defaults are applied:\n * - Helmet security headers are auto-injected\n * - JSON body limit defaults to 1MB\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * security: {\n * helmet: false, // disable auto-helmet\n * },\n * })\n * ```\n */\n security?: {\n /** Auto-inject helmet security headers. Default: true */\n helmet?: boolean\n }\n}\n\n/**\n * The main application class. Wires together Express, the DI container,\n * feature modules, adapters, and the middleware pipeline.\n */\nexport class Application {\n private app: Express\n /** The HTTP engine driver. Defaults to {@link expressRuntime}. */\n private readonly runtime: HttpRuntime\n private container: Container\n private httpServer: http.Server | null = null\n private readonly adapters: AppAdapter[]\n\n private readonly plugins: KickPlugin[]\n\n /**\n * Snapshot of every module-level Context Contributor registration\n * captured during `setup()`. The registrations themselves are\n * frozen by `defineContextDecorator`, so storing references is\n * safe — they outlive the module instances they came from.\n *\n * Used by {@link getContributors} so DevTools and adopter tooling\n * can render the module-level contributors alongside the adapter /\n * plugin / global ones. Populated once per `setup()` call; cleared\n * at the start of each invocation so a re-setup (test harnesses,\n * dev-server restarts) doesn't accumulate stale entries.\n */\n private _moduleContributors: Array<{\n registration: { key: string; dependsOn: readonly string[] }\n label: string\n }> = []\n\n /** Number of HTTP requests currently being processed */\n private _inFlightRequests = 0\n /** Whether the application is draining (shutting down gracefully) */\n private _draining = false\n /** Whether shutdown has already been initiated (prevents double-shutdown) */\n private _shutdownInitiated = false\n /** Resolvers waiting for in-flight requests to reach zero */\n private _drainResolvers: Array<() => void> = []\n\n constructor(private readonly options: ApplicationOptions) {\n this.runtime = options.runtime ?? expressRuntime()\n // `this.app` is the engine-native app; typed Express here (the default) for\n // the legacy escape hatches. Under another runtime it holds that engine's\n // app — `getRuntimeApp()` / `AdapterContext.app` expose it via ActiveRuntime.\n this.app = this.runtime.createApp({ trustProxy: options.trustProxy }) as Express\n this.container = Container.getInstance()\n\n // Sort plugins by `dependsOn` declarations BEFORE reading their adapters/etc.\n // Plugins without `dependsOn` keep their declaration order — this is a\n // pure refinement; no behaviour change for apps that don't use the field.\n this.plugins = mountSort(options.plugins ?? [], 'plugin')\n\n // Build adapter list from plugin adapters + user adapters, synthesize\n // a stable name for any anonymous adapter (so duplicate-name detection\n // and `dependsOn` resolution have something to key on), then sort by\n // adapter `dependsOn`. Plugin-shipped adapters keep their plugin's\n // relative order unless their own `dependsOn` says otherwise.\n const allAdapters = [\n ...this.plugins.flatMap((p) => p.adapters?.() ?? []),\n ...(options.adapters ?? []),\n ]\n let anonAdapterCount = 0\n const namedAdapters: Array<AppAdapter & { name: string }> = allAdapters.map((adapter) => {\n if (!adapter.name) {\n // `constructor.name === 'Object'` for plain object-literal adapters —\n // useless as an identity since two anonymous literals would collide.\n // Only borrow the constructor name when it's an actual class.\n const ctorName = adapter.constructor?.name\n const fallback =\n ctorName && ctorName !== 'Object' ? ctorName : `AnonymousAdapter#${anonAdapterCount++}`\n return Object.assign(adapter, { name: fallback })\n }\n return adapter as AppAdapter & { name: string }\n })\n this.adapters = mountSort(namedAdapters, 'adapter')\n // Wire the request store provider so Container can resolve REQUEST-scoped deps\n Container._requestStoreProvider = () => requestStore.getStore() ?? null\n }\n\n /** Whether the application is currently draining in-flight requests */\n get isDraining(): boolean {\n return this._draining\n }\n\n /** Number of HTTP requests currently being processed */\n get inFlightRequests(): number {\n return this._inFlightRequests\n }\n\n /** Get the DI container instance */\n getContainer(): Container {\n return this.container\n }\n\n /**\n * Express request handler — delegates to the internal Express app.\n *\n * Used by the Vite dev-server plugin to route requests through KickJS:\n * ```ts\n * const mod = await ssrLoadModule('virtual:kickjs/app')\n * mod.app.handle(req, res, next)\n * ```\n *\n * Also works as a standard Node.js request handler for production:\n * ```ts\n * http.createServer(app.handle.bind(app))\n * ```\n */\n handle(req: http.IncomingMessage, res: http.ServerResponse, next?: (err?: any) => void): void {\n const handler = this.runtime.nodeHandler(this.app)\n if (next) {\n handler(req, res, next)\n } else {\n handler(req, res)\n }\n }\n\n /**\n * Full setup pipeline:\n * 1. Adapter beforeMount hooks (early routes — docs, health)\n * 2. Adapter middleware (phase: beforeGlobal)\n * 3. Global middleware (user-declared or defaults)\n * 4. Adapter middleware (phase: afterGlobal)\n * 5. Module registration + DI bootstrap\n * 6. Adapter middleware (phase: beforeRoutes)\n * 7. Module route mounting\n * 8. Adapter middleware (phase: afterRoutes)\n * 9. Error handlers (notFound + global)\n * 10. Adapter beforeStart hooks\n */\n /** Build the adapter context object (shared across all hooks) */\n private adapterCtx(server?: any): AdapterContext {\n const env = process.env.NODE_ENV ?? 'development'\n return {\n http: this.adapterHttp(),\n app: this.app,\n container: this.container,\n server,\n env,\n isProduction: env === 'production',\n }\n }\n\n /**\n * The engine-agnostic HTTP surface handed to adapters as `ctx.http`. Each\n * call routes through the active runtime over the current `this.app`, so an\n * adapter written against this works under any runtime.\n */\n private adapterHttp(): AdapterHttp {\n return {\n route: (method, path, handler) => {\n const entry: RouteEntry = {\n method,\n path,\n middlewares: [],\n contributorRunner: null,\n handler,\n meta: {},\n }\n this.runtime.mountRoutes(this.app, [{ mountPath: '/', routes: [entry] }])\n },\n mount: (prefix, routes) => {\n this.runtime.mountRoutes(this.app, [{ mountPath: prefix, routes }])\n },\n serveStatic: (prefix, dir) => {\n this.runtime.serveStatic(this.app, prefix, dir)\n },\n use: (mw, opts) => {\n this.runtime.useConnect(this.app, mw, opts)\n },\n }\n }\n\n /** Call an adapter hook, awaiting async hooks and catching errors */\n private async callHook(\n hook: ((ctx: AdapterContext) => void | Promise<void>) | undefined,\n ctx: AdapterContext,\n ): Promise<void> {\n if (!hook) return\n try {\n const result = hook(ctx)\n if (result && typeof (result as Promise<void>).then === 'function') {\n await result\n }\n } catch (err) {\n log.error(err, 'Adapter hook failed')\n }\n }\n\n async setup(): Promise<void> {\n log.debug('Bootstrapping application...')\n\n // Reset module-contributor snapshot at the very top of each\n // setup pass — before anything that could throw — so a partial\n // failure doesn't leave stale entries that getContributors()\n // would surface on the next call. See _moduleContributors field\n // declaration for the use case.\n this._moduleContributors = []\n\n // Collect adapter middleware by phase\n const adapterMw = this.collectAdapterMiddleware()\n\n // Expose the Application instance on the Express app for adapter discovery\n ;(this.app as any).__kickApp = this\n\n const ctx = this.adapterCtx()\n\n // ── 1. Adapter beforeMount hooks ──────────────────────────────────\n for (const adapter of this.adapters) {\n await this.callHook(adapter.beforeMount?.bind(adapter), ctx)\n }\n\n // ── 2. Hardened defaults ──────────────────────────────────────────\n // x-powered-by disable + trust proxy now live in `runtime.createApp()`.\n\n // ── 2a. In-flight request tracking ──────────────────────────────\n this.runtime.useConnect(this.app, this.requestTrackingMiddleware())\n\n // ── 2b. Health check endpoints (before middleware, at root) ──────\n this.mountHealthEndpoints()\n\n // ── 2c. Request scope (AsyncLocalStorage) ────────────────────────\n // Auto-mounted unless the user opted out (`contextStore: 'manual'`)\n // or already included one in their middleware list.\n if (this.shouldAutoMountRequestScope()) {\n this.runtime.useConnect(this.app, requestScopeMiddleware())\n }\n\n // ── 3. Adapter middleware: beforeGlobal ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeGlobal)\n\n // ── 3b. Plugin registration ──────────────────────────────────────\n for (const plugin of this.plugins) {\n plugin.register?.(this.container)\n }\n\n // ── 3c. Plugin middleware ─────────────────────────────────────────\n for (const plugin of this.plugins) {\n try {\n const mw = plugin.middleware?.() ?? []\n for (const handler of mw) {\n this.runtime.useConnect(this.app, handler)\n }\n } catch (err) {\n log.error(err, `Plugin middleware failed: ${(plugin as any).name ?? 'unknown'}`)\n }\n }\n\n // ── 4. Global middleware ─────────────────────────────────────────\n // Auto-inject helmet unless opted out\n const autoHelmet = this.options.security?.helmet !== false\n if (autoHelmet) {\n try {\n const { helmet: helmetFn } = await import('./middleware/helmet')\n this.runtime.useConnect(this.app, helmetFn())\n } catch {\n // helmet middleware not available — skip silently\n }\n }\n\n const userMiddlewares = this.options.middlewares ?? this.options.middleware\n if (userMiddlewares) {\n // User-declared pipeline — full control\n for (const entry of userMiddlewares) {\n this.mountMiddlewareEntry(entry)\n }\n } else {\n // Sensible defaults when no middleware declared\n this.runtime.useConnect(this.app, requestId())\n // Skip express.json on engines that parse bodies natively (Fastify, h3) —\n // otherwise the body stream is read twice and the engine's parser hangs.\n if (!this.runtime.capabilities.nativeBodyParsing) {\n this.runtime.useConnect(this.app, express.json({ limit: this.options.jsonLimit ?? '1mb' }))\n }\n }\n\n // ── 5. Adapter middleware: afterGlobal ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterGlobal)\n\n // ── 6. Module registration + DI bootstrap ────────────────────────\n // Module collection — the static `modules: [...]` array AND the\n // imperative `setup(registry)` callback feed into the same\n // ordered list. Plugins go first so plugin modules / setup\n // calls run before user code (existing precedence).\n //\n // Order within the registry:\n // 1. plugin static modules (`plugin.modules?()`)\n // 2. plugin setup() calls (in plugin dependsOn-sorted order)\n // 3. user static modules (`options.modules`)\n // 4. user setup() callback\n const moduleRegistry = new MutableModuleRegistry()\n for (const plugin of this.plugins) {\n for (const m of plugin.modules?.() ?? []) moduleRegistry.mount(m)\n plugin.setup?.(moduleRegistry)\n }\n for (const m of this.options.modules) moduleRegistry.mount(m)\n this.options.setup?.(moduleRegistry)\n const allModuleEntries: AppModuleEntry[] = moduleRegistry.entries\n const modules = allModuleEntries.map((entry) => {\n // Discriminate class vs instance: classes are functions whose\n // `prototype` carries the AppModule shape; defineModule output is\n // a plain object with `routes`, `register`, `contributors`. Calling\n // `new` on a plain object throws, so we branch up-front rather\n // than try/catch.\n const mod: AppModule = typeof entry === 'function' ? new (entry as AppModuleClass)() : entry\n // `register()` is optional — modules whose classes are entirely\n // decorator-managed (@Service, @Controller, @Repository) don't need it.\n mod.register?.(this.container)\n return mod\n })\n this.container.bootstrap()\n\n // Register Logger as an injectable singleton so @Inject(Logger)\n // works in constructors. Without this, Logger is just a plain\n // class with static methods and no DI binding.\n if (!this.container.has(Logger)) {\n this.container.registerInstance(Logger, new Logger())\n }\n\n // ── 7. Adapter middleware: beforeRoutes ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeRoutes)\n\n // ── 8. Mount module routes with versioning ───────────────────────\n const apiPrefix = this.options.apiPrefix ?? '/api'\n const defaultVersion = this.options.defaultVersion ?? 1\n // Opt-in, default off. `logRouteTable` is the current name;\n // `logRoutesTable` is the deprecated alias.\n const shouldLogRoutes = this.options.logRouteTable ?? this.options.logRoutesTable ?? false\n\n // Collect route metadata during mounting (avoids calling mod.routes() twice)\n const mountedRoutes: Array<{ controller: any; mountPath: string }> = []\n\n // Context Contributors (#107) — collect adapter + plugin + global once;\n // per-module sources are computed inside the loop so module isolation is\n // preserved.\n //\n // Plugin contributors merge at the same `'adapter'` precedence as adapter\n // contributors. Plugins are conceptually \"bundles of adapters + extras\",\n // so a plugin that ships a typed contributor without standing up an\n // accompanying adapter behaves identically to one that does.\n const adapterSources: SourcedRegistration[] = []\n for (const adapter of this.adapters) {\n const adapterContribs = adapter.contributors?.() ?? []\n const adapterLabel = adapter.name ?? adapter.constructor.name ?? 'adapter'\n for (const registration of adapterContribs) {\n adapterSources.push({ source: 'adapter', registration, label: adapterLabel })\n }\n }\n for (const plugin of this.plugins) {\n const pluginContribs = plugin.contributors?.() ?? []\n const pluginLabel = plugin.name ?? plugin.constructor?.name ?? 'plugin'\n for (const registration of pluginContribs) {\n adapterSources.push({ source: 'adapter', registration, label: pluginLabel })\n }\n }\n const globalSources: SourcedRegistration[] = (this.options.contributors ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'global',\n registration,\n label: 'bootstrap',\n }),\n )\n\n // Reset module-contributor snapshot at the start of each setup\n // pass so re-running `setup()` (test harnesses, dev-server\n // restarts) doesn't accumulate stale entries.\n this._moduleContributors = []\n\n // Duplicate-route guard (KICK006): one registry across every module in\n // this setup pass, so cross-module collisions fail at boot instead of\n // silently losing the dispatch race.\n const seenRoutes = new Map<string, string>()\n\n for (const mod of modules) {\n // Prefer the declared `name` field (set by `defineModule({ name: 'X', ... })`)\n // over `constructor.name`. Factory-built modules are plain objects whose\n // constructor is `Object` — falling straight to `constructor.name` would\n // degrade every label in the DevTools Contributors table to \"Object\".\n const declaredName = (mod as { name?: unknown }).name\n const ctorName = mod.constructor?.name\n const moduleLabel =\n typeof declaredName === 'string' && declaredName.length > 0\n ? declaredName\n : ctorName && ctorName !== 'Object'\n ? ctorName\n : 'module'\n const moduleSources: SourcedRegistration[] = (mod.contributors?.() ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'module',\n registration,\n label: moduleLabel,\n }),\n )\n // Retain registrations for `getContributors()` — these would\n // otherwise be unreachable post-bootstrap (module instances\n // are not kept on the Application).\n for (const src of moduleSources) {\n this._moduleContributors.push({\n registration: src.registration as { key: string; dependsOn: readonly string[] },\n label: moduleLabel,\n })\n }\n\n // Thread per-module + adapter + global sources to buildRoutes via the\n // module-scoped slot. Module setup is sequential, so the slot is\n // race-free. The slot MUST stay populated across both `mod.routes()`\n // AND any framework-driven `buildRoutes(controller)` calls below —\n // when a module returns `{ path, controller }` (auto-derive shape),\n // buildRoutes runs here, not inside routes(). Clearing the slot in\n // the routes()-finally would drop module/adapter/global contributors\n // on the floor and surface as `MissingContributorError` for any\n // class/method-level dependsOn referencing a module-level key.\n _setExternalContributorSources([...moduleSources, ...adapterSources, ...globalSources])\n try {\n const result = mod.routes()\n if (!result) continue // Non-HTTP modules (queues, cron) may return null\n\n const routeSets = Array.isArray(result) ? result : [result]\n\n for (const route of routeSets) {\n const version = route.version ?? defaultVersion\n const mountPath = `${apiPrefix}/v${version}${normalizePath(route.path)}`\n // Common-case `{ path, controller }`: build the engine-neutral route\n // table and let the runtime materialize it natively (Express Router /\n // Fastify routes / …). Adopters who hand-build a `router` (composing\n // multiple controllers, third-party routers) pass it explicitly — that\n // is an Express-specific connect handler, mounted via `useConnect`.\n if (route.router) {\n this.runtime.useConnect(this.app, route.router, { path: mountPath })\n } else if (route.controller) {\n const routeTable = buildRouteTable(route.controller)\n const ctrl = route.controller.name ?? 'controller'\n for (const entry of routeTable) {\n // Per-handler owner so intra-controller duplicates name both methods.\n const owner = `${ctrl}.${String(entry.meta.handlerName ?? '?')}`\n assertRouteUnique(seenRoutes, entry.method, joinPaths(mountPath, entry.path), owner)\n }\n this.runtime.mountRoutes(this.app, [{ mountPath, routes: routeTable }])\n } else {\n throw moduleRouteMissingControllerError(mountPath)\n }\n\n // Notify adapters (e.g. SwaggerAdapter for OpenAPI spec generation)\n if (route.controller) {\n for (const adapter of this.adapters) {\n try {\n adapter.onRouteMount?.(route.controller, mountPath)\n } catch (err) {\n log.error(err, `adapter.onRouteMount() failed for ${mountPath}`)\n }\n }\n if (shouldLogRoutes) {\n mountedRoutes.push({ controller: route.controller, mountPath })\n }\n }\n }\n } finally {\n _setExternalContributorSources([])\n }\n }\n\n // ── 8b. Log route summary ─────────────────────────────────────────\n if (shouldLogRoutes && mountedRoutes.length > 0) {\n const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']\n const methodRank = (m: string) => {\n const i = methodOrder.indexOf(m)\n return i === -1 ? 99 : i\n }\n\n let totalRoutes = 0\n log.info('Routes:')\n\n for (const { controller, mountPath } of mountedRoutes) {\n const defs: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controller,\n [],\n )\n if (defs.length === 0) continue\n\n const counts: Record<string, number> = {}\n for (const def of defs) {\n const m = def.method.toUpperCase()\n counts[m] = (counts[m] || 0) + 1\n }\n totalRoutes += defs.length\n\n const methods = Object.entries(counts)\n .toSorted(([a], [b]) => methodRank(a) - methodRank(b))\n .map(([m, n]) => `${n} ${m}`)\n .join(', ')\n const name = controller.name || 'Controller'\n log.info(` ${name.padEnd(30)} ${mountPath.padEnd(25)} ${defs.length} routes (${methods})`)\n }\n\n log.info(` Total: ${totalRoutes} routes`)\n }\n\n // ── 9. Adapter middleware: afterRoutes ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterRoutes)\n\n // ── 10. Adapter beforeStart hooks ────────────────────────────────\n // Runs BEFORE error handlers so adapters can still mount Express\n // routes (e.g. `McpAdapter` registers `/_mcp/messages` here). If\n // error handlers were registered first, the notFoundHandler — a\n // catch-all that never calls next() — would pre-empt anything an\n // adapter adds afterwards and adapter-mounted endpoints would 404.\n for (const adapter of this.adapters) {\n await this.callHook(adapter.beforeStart?.bind(adapter), ctx)\n }\n\n // ── 11. Error handlers ───────────────────────────────────────────\n // Last in the chain so any adapter-mounted route (step 10) gets\n // a chance to match before the catch-all 404 fires.\n this.runtime.setNotFound(this.app, this.options.onNotFound ?? notFoundHandler())\n this.runtime.setErrorHandler(this.app, this.options.onError ?? errorHandler())\n }\n\n /** Register modules and DI without starting the HTTP server (used by kick tinker) */\n async registerOnly(): Promise<void> {\n await this.setup()\n }\n\n /**\n * Start the HTTP server.\n *\n * In **dev mode** (Vite plugin active): reuses `globalThis.__kickjs_httpServer`\n * created by Vite. Adapters (WsAdapter, Socket.IO, etc.) receive the real\n * `http.Server` through `afterStart({ server })` — zero adapter changes needed.\n *\n * In **production**: creates its own `http.Server` and binds to the port.\n */\n async start(): Promise<void> {\n await this.setup()\n\n const g = globalThis as any\n\n if (g.__kickjs_httpServer) {\n // ── DEV MODE: Vite owns the http.Server ──────────────────────\n // Don't create a new server or listen — Vite is already listening.\n // Just wire up adapters with the Vite-created server.\n this.httpServer = g.__kickjs_httpServer\n log.debug('Attached to Vite dev server')\n\n // Dev mode suppresses our own SIGINT/SIGTERM handlers (Vite owns\n // the process lifecycle), so a Ctrl+C would otherwise skip graceful\n // shutdown entirely — `adapter.shutdown()`, request draining, and\n // any shutdown logs never run. Expose `shutdown()` on globalThis so\n // the CLI dev server (`kick dev`) can drive it before tearing down\n // Vite. Same process, so the CLI's signal handler can reach this.\n g.__kickjs_app_shutdown = () => this.shutdown()\n\n for (const adapter of this.adapters) {\n const ctx = this.adapterCtx(this.httpServer!)\n await this.callHook(adapter.afterStart?.bind(adapter), ctx)\n }\n\n for (const plugin of this.plugins) {\n await plugin.onReady?.(this.container)\n }\n\n return\n }\n\n // ── PRODUCTION: Create and own the http.Server ─────────────────\n const port = this.options.port ?? parseInt(process.env.PORT || '3000', 10)\n this.httpServer = http.createServer(this.runtime.nodeHandler(this.app))\n\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n log.error(\n `Port ${port} is already in use. Kill the existing process or use a different port:\\n` +\n ` PORT=${port + 1} kick dev\\n` +\n ` lsof -i :${port} # find what's using it\\n` +\n ` kill <PID> # stop it`,\n )\n process.exit(1)\n }\n throw err\n })\n\n // Wrap listen in a Promise so afterStart/onReady errors propagate\n await new Promise<void>((resolve, reject) => {\n this.httpServer!.listen(port, async () => {\n try {\n log.info(`Server running on http://localhost:${port}`)\n\n for (const adapter of this.adapters) {\n const afterCtx = this.adapterCtx(this.httpServer!)\n await this.callHook(adapter.afterStart?.bind(adapter), afterCtx)\n }\n\n // Plugin onReady hooks\n for (const plugin of this.plugins) {\n await plugin.onReady?.(this.container)\n }\n\n resolve()\n } catch (err) {\n reject(err)\n }\n })\n })\n }\n\n /** HMR rebuild: swap Express handler without restarting the server */\n async rebuild(): Promise<void> {\n // Build the new app fully before swapping — if setup() throws,\n // the old app keeps running so the server stays responsive.\n const prevApp = this.app\n const prevContainer = this.container\n // Snapshot the outgoing app's disposables (session/rate-limit store\n // intervals) BEFORE setup() registers the incoming app's. On success the\n // stale set is disposed — without this, every HMR swap leaked one live\n // interval + Map per store. On failure it's restored, and whatever the\n // failed setup() half-registered is disposed instead.\n const staleDisposables = drainDisposables()\n\n try {\n Container.reset()\n this.container = Container.getInstance()\n this.app = this.runtime.createApp({ trustProxy: this.options.trustProxy }) as Express\n await this.setup()\n } catch (err) {\n log.error(err, 'HMR rebuild failed, keeping previous app')\n // Restore previous state so the server stays responsive\n await runDisposables(drainDisposables())\n for (const d of staleDisposables) registerDisposable(d)\n this.app = prevApp\n this.container = prevContainer\n return\n }\n\n await runDisposables(staleDisposables)\n\n if (this.httpServer) {\n this.httpServer.removeAllListeners('request')\n this.httpServer.on('request', this.runtime.nodeHandler(this.app))\n log.debug('HMR: app rebuilt and swapped')\n }\n }\n\n /**\n * Graceful shutdown with request draining.\n *\n * 1. Stops accepting new connections (server.close())\n * 2. Waits for in-flight requests to complete (up to shutdownTimeout)\n * 3. Calls adapter.shutdown() for all registered adapters\n * 4. Force-closes after timeout\n *\n * Safe to call multiple times — subsequent calls return the same promise.\n */\n async shutdown(): Promise<void> {\n // Prevent double-shutdown — return immediately if already initiated\n if (this._shutdownInitiated) {\n log.debug('Shutdown already in progress, skipping duplicate call')\n return\n }\n this._shutdownInitiated = true\n this._draining = true\n\n log.debug('Shutting down — draining in-flight requests...')\n\n const timeoutMs = this.options.shutdownTimeout ?? 30_000\n let timer: ReturnType<typeof setTimeout> | undefined\n\n // Start a force-exit timer if timeout is configured\n const forceExitPromise =\n timeoutMs > 0\n ? new Promise<'timeout'>((resolve) => {\n timer = setTimeout(() => resolve('timeout'), timeoutMs)\n timer.unref()\n })\n : new Promise<never>(() => {}) // never resolves — no forced exit\n\n try {\n // Step 1: Stop accepting new connections.\n // server.close() prevents new connections. Its callback fires only when\n // ALL existing connections are fully closed, so we do NOT await it here —\n // we track request draining separately via the tracking middleware.\n if (this.httpServer) {\n this.httpServer.close(() => {})\n }\n\n // Step 2: Wait for in-flight requests to drain (or timeout)\n if (this._inFlightRequests > 0) {\n log.debug(`Waiting for ${this._inFlightRequests} in-flight request(s) to complete...`)\n const drainPromise = new Promise<'drained'>((resolve) => {\n this._drainResolvers.push(() => resolve('drained'))\n })\n\n const result = await Promise.race([drainPromise, forceExitPromise])\n if (result === 'timeout') {\n log.warn(\n `Shutdown timeout (${timeoutMs}ms) reached with ${this._inFlightRequests} request(s) still in-flight, forcing shutdown`,\n )\n } else {\n log.debug('All in-flight requests completed')\n }\n }\n\n // Step 3: Run all plugin + adapter shutdowns concurrently\n const results = await Promise.allSettled([\n ...this.plugins.map((plugin) => Promise.resolve(plugin.shutdown?.())),\n ...this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.())),\n ])\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error({ err: result.reason }, 'Adapter shutdown failed')\n }\n }\n\n // Step 4: Release framework-owned resources started outside the\n // adapter lifecycle — in-memory session/rate-limit store intervals —\n // and flush any pending container change batch so its debounce timer\n // doesn't outlive the app.\n await disposeAll()\n this.container.flushChanges()\n } finally {\n if (timer) clearTimeout(timer)\n }\n }\n\n /**\n * The engine-native app instance, typed from the active runtime\n * ({@link ActiveRuntime} — Express by default). Prefer this over\n * {@link getExpressApp}; under a non-Express runtime the type follows the\n * augmented registry.\n */\n getRuntimeApp(): ActiveRuntime['app'] {\n return this.app\n }\n\n /**\n * The active HTTP runtime's identity + capabilities — `name` is `'express'`\n * (default), `'fastify'`, or `'h3'`; `capabilities` reports what the engine\n * supports (render / uploads / connectMiddleware / nativeBodyParsing).\n * Surfaced by DevTools and the VS Code extension so tooling can show which\n * engine the app is running on. Cheap, side-effect free.\n */\n getActiveRuntime(): { name: string; capabilities: RuntimeCapabilities } {\n return { name: this.runtime.name, capabilities: this.runtime.capabilities }\n }\n\n /** @deprecated Use {@link getRuntimeApp}. Returns the engine-native app. */\n getExpressApp(): Express {\n return this.app\n }\n\n /** Get registered adapters — used by DevToolsAdapter for peer discovery */\n getAdapters(): AppAdapter[] {\n return this.adapters\n }\n\n /**\n * Get registered plugins — used by DevToolsAdapter for topology\n * introspection (architecture.md §23). Read-only; returns the\n * already-sorted plugin list (post-`dependsOn` topo-sort).\n */\n getPlugins(): readonly KickPlugin[] {\n return this.plugins\n }\n\n /**\n * Snapshot of every Context Contributor reachable from this app —\n * walks **four** registration sites: adapters, plugins, modules,\n * and the bootstrap `contributors` option. The devtools Topology\n * tab consumes this for the Contributors panel; tests + adopters\n * can call it for diagnostics.\n *\n * Per-route (method/class-decorator) registrations are NOT included\n * here — they live on the route registry rather than on any of\n * the four collection sites this method walks. A future RPC will\n * surface them separately.\n *\n * Result shape stays minimal — `{ key, source, label, dependsOn }`\n * — so we don't leak `resolve` closures or other internal fields.\n * `source` matches the five-level precedence union from\n * `ContributorSource` (minus `'method'` / `'class'` which require\n * the per-route walk noted above).\n */\n getContributors(): ReadonlyArray<{\n key: string\n source: 'module' | 'adapter' | 'plugin' | 'global'\n label: string\n dependsOn: readonly string[]\n }> {\n const out: Array<{\n key: string\n source: 'module' | 'adapter' | 'plugin' | 'global'\n label: string\n dependsOn: readonly string[]\n }> = []\n const ingest = (\n list: ReadonlyArray<unknown> | null | undefined,\n source: 'module' | 'adapter' | 'plugin' | 'global',\n label: string,\n ): void => {\n if (!list) return\n for (const reg of list) {\n const r = reg as { key?: unknown; dependsOn?: unknown }\n if (r.key === undefined || r.key === null) continue\n // `key` is typically a string but createToken<T>() yields a\n // branded token object — normalise via tokenName so both\n // surfaces flow through.\n const key = typeof r.key === 'string' ? r.key : tokenName(r.key)\n out.push({\n key,\n source,\n label,\n dependsOn: Array.isArray(r.dependsOn) ? (r.dependsOn as string[]) : [],\n })\n }\n }\n for (const adapter of this.adapters) {\n ingest(adapter.contributors?.(), 'adapter', adapter.name ?? 'adapter')\n }\n for (const plugin of this.plugins) {\n ingest(plugin.contributors?.(), 'plugin', plugin.name ?? 'plugin')\n }\n // Module-level registrations — captured during `setup()` because\n // module instances are not retained on the Application. Walk the\n // snapshot rather than calling `mod.contributors()` here.\n for (const { registration, label } of this._moduleContributors) {\n ingest([registration], 'module', label)\n }\n ingest(this.options.contributors, 'global', 'bootstrap')\n return out\n }\n\n getHttpServer(): http.Server | null {\n return this.httpServer\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n /**\n * Decide whether {@link Application} should auto-mount the\n * {@link requestScopeMiddleware} ALS wrapper.\n *\n * Returns `false` when:\n * - `contextStore: 'manual'` was set (caller manages ALS frames), or\n * - the user-supplied `middleware` list already includes a\n * `requestScopeMiddleware()` (detected via the symbol marker stamped\n * in `middleware/request-scope.ts`).\n *\n * Otherwise `true` — preserves the historical default of \"always wrap\n * requests in an ALS frame\" so existing apps see no behavior change.\n */\n private shouldAutoMountRequestScope(): boolean {\n if (this.options.contextStore === 'manual') return false\n\n const userEntries = this.options.middlewares ?? this.options.middleware ?? []\n for (const entry of userEntries) {\n const handler = typeof entry === 'function' ? entry : entry.handler\n if (isRequestScopeMiddleware(handler)) return false\n }\n return true\n }\n\n private collectAdapterMiddleware() {\n const result = {\n beforeGlobal: [] as AdapterMiddleware[],\n afterGlobal: [] as AdapterMiddleware[],\n beforeRoutes: [] as AdapterMiddleware[],\n afterRoutes: [] as AdapterMiddleware[],\n }\n\n for (const adapter of this.adapters) {\n const entries = adapter.middleware?.() ?? []\n for (const entry of entries) {\n const phase = entry.phase ?? 'afterGlobal'\n result[phase].push(entry)\n }\n }\n\n return result\n }\n\n private mountMiddlewareList(entries: AdapterMiddleware[]): void {\n for (const entry of entries) {\n if (entry.path === undefined) {\n this.runtime.useConnect(this.app, entry.handler)\n continue\n }\n this.runtime.useConnect(this.app, entry.handler, { path: entry.path })\n }\n }\n\n private mountMiddlewareEntry(entry: MiddlewareEntry): void {\n if (typeof entry === 'function') {\n this.runtime.useConnect(this.app, entry)\n } else {\n this.runtime.useConnect(this.app, entry.handler, { path: entry.path })\n }\n }\n\n /** Middleware that tracks in-flight requests for graceful draining */\n private requestTrackingMiddleware(): RequestHandler {\n return (_req, res, next) => {\n this._inFlightRequests++\n const onFinish = () => {\n res.removeListener('finish', onFinish)\n res.removeListener('close', onFinish)\n this._inFlightRequests--\n // If draining and no more in-flight requests, resolve all waiters\n if (this._draining && this._inFlightRequests === 0) {\n for (const resolve of this._drainResolvers) {\n resolve()\n }\n this._drainResolvers = []\n }\n }\n res.on('finish', onFinish)\n res.on('close', onFinish)\n next()\n }\n }\n\n /** Mount /health/live and /health/ready endpoints at the root (no API prefix) */\n private mountHealthEndpoints(): void {\n const http = this.adapterHttp()\n\n http.route('GET', '/health/live', (ctx) => {\n if (this._draining) {\n ctx.res.status(503).json({ status: 'draining', uptime: process.uptime() })\n } else {\n ctx.json({ status: 'ok', uptime: process.uptime() })\n }\n })\n\n http.route('GET', '/health/ready', async (ctx) => {\n if (this._draining) {\n ctx.res.status(503).json({ status: 'draining', checks: [] })\n return\n }\n const adaptersWithHealth = this.adapters.filter((a) => a.onHealthCheck)\n const checks = await Promise.allSettled(adaptersWithHealth.map((a) => a.onHealthCheck!()))\n const results = checks.map((c, i) => {\n if (c.status === 'fulfilled') return c.value\n return { name: adaptersWithHealth[i].name ?? 'unknown', status: 'down' as const }\n })\n const healthy = results.every((r) => r.status === 'up')\n ctx.res.status(healthy ? 200 : 503).json({\n status: healthy ? 'ready' : 'degraded',\n checks: results,\n })\n })\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"application-D_mReBi_.d.mts","names":[],"sources":["../src/http/application.ts"],"mappings":";;;;;;;;;;;KAwDY,kBAAkB;EAAmB;EAAc,SAAS;;UAEvD;;;;;;;;;;;EAWf,SAAS;;;;;;;;;;;;;;;;;;EAmBT,OAAO,UAAU;;EAEjB,WAAW;;EAEX;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;;;EAuBA,cAAc;;;;;;EAMd,aAAa;;EAGb,UAAU;;;;;;;;;;;;;;;;;;;;;EAsBV,eAAe;;;;;;;;;;;;;;;;;;;EAoBf;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BA;;;;;;;;;;;;EAaA,UAAU;;EAGV,2CAA2C,YAAY;;EAEvD;;;;;;;;EAQA;;;;;EAKA;;;;;EAKA;;;;;;;;;;;;;;;;;;;EAoBA;IAAsB;;;;;;;;;;;;;;;;;EAiBtB,cAAc,UAAU,UAAU;;;;;;;;;;;;;;;;;EAkBlC,WAAW,UAAU,UAAU,UAAU;;;;;;;;;;;;;;;;;;EAmBzC;;IAEE;;;;;;;cAQS;mBAoCkB;UAnCrB;;mBAES;UACT;UACA;mBACS;mBAEA;;;;;;;;;;;;;UAcT;;UAMA;;UAEA;;UAEA;;UAEA;EAER,YAA6B,SAAS;;MAyClC;;MAKA;;EAKJ,gBAAgB;;;;;;;;;;;;;;;EAkBhB,OAAO,KAAK,KAAK,iBAAiB,KAAK,KAAK,gBAAgB,QAAQ;;;;;;;;;;;;;;;UAuB5D;;;;;;UAiBA;;UA0BM;EAeR,SAAS;;EA0UT,gBAAgB;;;;;;;;;;EAahB,SAAS;;EA0ET,WAAW;;;;;;;;;;;EA8CX,YAAY;;;;;;;EA6ElB,iBAAiB;;;;;;;;EAWjB;IAAsB;IAAc,cAAc;;;EAKlD,iBAAiB;;EAKjB,eAAe;;;;;;EASf,uBAAuB;;;;;;;;;;;;;;;;;;;EAsBvB,mBAAmB;IACjB;IACA;IACA;IACA;;EA6CF,iBAAiB,KAAK;;;;;;;;;;;;;;UAmBd;UAWA;UAmBA;UAUA;;UASA;;UAsBA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"bootstrap-CytCvAnr.d.mts","names":[],"sources":["../src/http/bootstrap.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;iBAqCsB,UAAU,SAAS,qBAAqB,QAAQ"}
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v7.0.0
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{normalizeProblem}from"./core/errors.mjs";import{r as MissingContextValueError}from"./context-errors-DDYA51Nf.mjs";import{a as parseQuery}from"./query-tx53xRSZ.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 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;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)}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`,`attachment; filename="${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)}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()}}}};export{requestStore as i,getRequestStore as n,getRequestValue as r,RequestContext as t};
12
- //# sourceMappingURL=context-vG6HXwHB.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"context-vG6HXwHB.mjs","names":[],"sources":["../src/http/request-store.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","/// <reference types=\"multer\" />\nimport type { Request, NextFunction } from 'express'\nimport type { ActiveRuntime, 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 {\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 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 /** Session data (requires session middleware) */\n get session(): any {\n return (this.req as any).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 `@forinda/kickjs-auth`\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 html(content: string, status = 200) {\n return this._response.status(status).type('text/html; charset=utf-8').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this._response.setHeader('Content-Disposition', `attachment; filename=\"${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 ) {\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 return this.json(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 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"],"mappings":"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"}
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v7.0.0
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
- function randomHex(bytes){let buf=crypto.getRandomValues(new Uint8Array(bytes));return Array.from(buf,b=>b.toString(16).padStart(2,`0`)).join(``)}function csrf(options={}){let cookieName=options.cookie??`_csrf`,headerName=options.header??`x-csrf-token`,protectedMethods=new Set((options.methods??[`POST`,`PUT`,`PATCH`,`DELETE`]).map(m=>m.toUpperCase())),ignorePaths=new Set(options.ignorePaths??[]),tokenLength=options.tokenLength??32,cookieOpts={httpOnly:!0,sameSite:`strict`,secure:process.env.NODE_ENV===`production`,path:`/`,...options.cookieOptions};return(req,res,next)=>{let token=(req.cookies||{})[cookieName];if(token||(token=randomHex(tokenLength),res.cookie(cookieName,token,cookieOpts)),!protectedMethods.has(req.method.toUpperCase())||ignorePaths.has(req.path))return next();let headerToken=req.headers[headerName];if(!headerToken||headerToken!==token)return res.status(403).json({message:`CSRF token mismatch`});next()}}export{randomHex as n,csrf as t};
12
- //# sourceMappingURL=csrf-Dna2I7cx.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"csrf-Dna2I7cx.mjs","names":[],"sources":["../src/core/web-crypto.ts","../src/http/middleware/csrf.ts"],"sourcesContent":["/**\n * Web Crypto helpers — portable across node/bun/deno/workers via\n * `globalThis.crypto`. The Web Crypto migration lives here so the request\n * path never imports `node:crypto` (edge-runtime portability, see\n * `web-standards-edge-design.md` P0).\n */\n\n/** Random hex string — replacement for `randomBytes(n).toString('hex')`. */\nexport function randomHex(bytes: number): string {\n const buf = crypto.getRandomValues(new Uint8Array(bytes))\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n","import type { Request, Response, NextFunction } from 'express'\n\nimport { randomHex } from '../../core/web-crypto'\n\nexport interface CsrfOptions {\n /** Cookie name for the CSRF token (default: '_csrf') */\n cookie?: string\n /** Header name to check for the token (default: 'x-csrf-token') */\n header?: string\n /** HTTP methods that require CSRF validation (default: POST, PUT, PATCH, DELETE) */\n methods?: string[]\n /** Paths to exclude from CSRF checks (e.g. webhooks) */\n ignorePaths?: string[]\n /** Token byte length before hex encoding (default: 32 = 64 hex chars) */\n tokenLength?: number\n /** Cookie options */\n cookieOptions?: {\n httpOnly?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n secure?: boolean\n path?: string\n }\n}\n\n/**\n * Double-submit cookie CSRF protection middleware.\n *\n * On every request, sets a CSRF token cookie. For state-changing methods\n * (POST, PUT, PATCH, DELETE), validates that the request header matches\n * the cookie value.\n *\n * @example\n * ```ts\n * import { csrf } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * cookieParser(),\n * csrf(),\n * // ... other middleware\n * ],\n * })\n * ```\n *\n * Client usage:\n * 1. Read the `_csrf` cookie value\n * 2. Send it in the `x-csrf-token` header on every mutating request\n */\nexport function csrf(options: CsrfOptions = {}) {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const cookieOpts = {\n httpOnly: true,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Generate or reuse CSRF token\n const cookies = (req as any).cookies || {}\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomHex(tokenLength)\n res.cookie(cookieName, token, cookieOpts)\n }\n\n // Skip validation for safe methods and ignored paths\n if (!protectedMethods.has(req.method.toUpperCase())) {\n return next()\n }\n\n if (ignorePaths.has(req.path)) {\n return next()\n }\n\n // Validate: header token must match cookie token\n const headerToken = req.headers[headerName] as string | undefined\n\n if (!headerToken || headerToken !== token) {\n return res.status(403).json({\n message: 'CSRF token mismatch',\n })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,UAAU,MAAuB,CAC/C,IAAM,IAAM,OAAO,gBAAgB,IAAI,WAAW,KAAK,CAAC,EACxD,OAAO,MAAM,KAAK,IAAM,GAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CACxE,CCsCA,SAAgB,KAAK,QAAuB,CAAC,EAAG,CAC9C,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,QAAQ,EAAA,CAAG,IAAK,GAAM,EAAE,YAAY,CAAC,CACpF,EACM,YAAc,IAAI,IAAI,QAAQ,aAAe,CAAC,CAAC,EAC/C,YAAc,QAAQ,aAAe,GACrC,WAAa,CACjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,aACb,EAEA,OAAQ,IAAc,IAAe,OAAuB,CAG1D,IAAI,OADa,IAAY,SAAW,CAAC,EAAA,CACrB,YAYpB,GAVK,QACH,MAAQ,UAAU,WAAW,EAC7B,IAAI,OAAO,WAAY,MAAO,UAAU,GAItC,CAAC,iBAAiB,IAAI,IAAI,OAAO,YAAY,CAAC,GAI9C,YAAY,IAAI,IAAI,IAAI,EAC1B,OAAO,KAAK,EAId,IAAM,YAAc,IAAI,QAAQ,YAEhC,GAAI,CAAC,aAAe,cAAgB,MAClC,OAAO,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,CAC1B,QAAS,qBACX,CAAC,EAGH,KAAK,CACP,CACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"error-handler-Cr6Yt9as.d.mts","names":[],"sources":["../src/http/middleware/error-handler.ts"],"mappings":";;;;iBAYgB,oBAAe,MACf,SAAO,KAAO,UAAQ,OAAS;;;;;;;;;;;;;;;;;;;;iBAwB/B,iBAAY,UAEV,KAAO,SAAO,KAAO,UAAQ,OAAS,iBAAY,cAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"kv-stores-D2kSUAEP.d.mts","names":[],"sources":["../src/http/reply.ts","../src/http/middleware/rate-limit-guard.ts","../src/http/middleware/kv-stores.ts"],"mappings":";;;;;;cAeM;;;;;;UAOW,MAAM,2BAA2B;YACtC;WACD,QAAQ;WACR,MAAM;;;;;;;;;;;;iBAaD,MAAM,kBAAkB,GAAG,QAAQ,GAAG,MAAM,IAAI,MAAM,GAAG;kBAAzD;MAKC,UAAA,GAAC,MAAQ,MAAI,WAAW;MAEvB,WAAA,GAAC,MAAQ,MAAI,WAAW;MAEpB,iBAAA;;;;;;;;;;;;;;;;KAgBV,qBAAqB,KAAK,eAAc,wBAAwB,MACvE,QAAQ,QAAQ,mDAEf,YAAY,QAAQ,QAAQ;KAG7B,YAAY,KAAK,UAAU,oBAAoB,KAAK,IAAI;iBAE7C,QAAQ,iBAAiB,SAAS;;;;;;;;;;;;iBAmBlC,mBAAmB,KAAK,gBAAgB;;;UCjFvC;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;EAMA,gBAAgB,KAAK;;EAErB;;;;;;EAMA,QAAQ;;EAER,QAAQ,KAAK;;;;;;;;;;;;;;;;;iBAkEC,eAAe,UAAS,wBAA6B;;;;;;;;;;;;;;;;;;;UC9EpD;EACf,IAAI,cAAc;EAClB,IAAI,aAAa,eAAe;IAAY;MAA2B;EACvE,OAAO,cAAc;;UAQN;;EAEf;;EAEA;;;;;;;;;;;;cAaW,4BAA4B;mBAKpB;mBAJF;mBACA;EAEjB,YACmB,IAAI,QACrB,SAAS;EAML,UAAU,cAAc;IAAU;IAAmB,WAAW;;EAahE,UAAU,cAAc;EAQxB,MAAM,cAAc;UAIZ;UAUA;;UAOC;;EAEf;;;;;;;;;;;;cAaW,0BAA0B;mBAIlB;mBAHF;EAEjB,YACmB,IAAI,QACrB,UAAS;EAKL,IAAI,cAAc,QAAQ;EAU1B,IAAI,aAAa,MAAM,aAAa,iBAAiB;EAMrD,QAAQ,cAAc;EAItB,MAAM,aAAa,iBAAiB"}
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v7.0.0
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
- var LazyMemoryStore=class{windowMs;hits=new Map;constructor(windowMs){this.windowMs=windowMs}async increment(key){let now=Date.now(),entry=this.hits.get(key);if(entry&&entry.reset>now)return entry.hits++,{totalHits:entry.hits,resetTime:new Date(entry.reset)};if(this.hits.size>1e4)for(let[k,e]of this.hits)e.reset<=now&&this.hits.delete(k);let fresh={hits:1,reset:now+this.windowMs};return this.hits.set(key,fresh),{totalHits:1,resetTime:new Date(fresh.reset)}}async decrement(key){let entry=this.hits.get(key);entry&&entry.hits>0&&entry.hits--}async reset(key){this.hits.delete(key)}};function defaultKey(ctx){let reqIp=ctx.req.ip;if(typeof reqIp==`string`&&reqIp.length>0)return reqIp;let h=ctx.headers,first=v=>typeof v==`string`&&v.length>0?v.split(`,`)[0].trim():void 0;return first(h[`cf-connecting-ip`])??first(h[`x-forwarded-for`])??first(h[`x-real-ip`])??`global`}function rateLimitGuard(options={}){let max=options.max??100,windowMs=options.windowMs??6e4,message=options.message??`Too Many Requests`,statusCode=options.statusCode??429,keyGenerator=options.keyGenerator??defaultKey,sendHeaders=options.headers??!0,store=options.store??new LazyMemoryStore(windowMs);return async(ctx,next)=>{if(options.skip?.(ctx))return next();let{totalHits,resetTime}=await store.increment(keyGenerator(ctx)),remaining=Math.max(0,max-totalHits);if(sendHeaders&&(ctx.setHeader(`X-RateLimit-Limit`,String(max)),ctx.setHeader(`X-RateLimit-Remaining`,String(remaining)),ctx.setHeader(`X-RateLimit-Reset`,String(Math.ceil(resetTime.getTime()/1e3)))),totalHits>max){let retryAfter=Math.max(1,Math.ceil((resetTime.getTime()-Date.now())/1e3));sendHeaders&&ctx.setHeader(`Retry-After`,String(retryAfter)),ctx.json({message},statusCode);return}next()}}function kvTtlSeconds(ms){return Math.max(60,Math.ceil(ms/1e3))}var KvRateLimitStore=class{kv;prefix;windowMs;constructor(kv,options){this.kv=kv,this.windowMs=options.windowMs,this.prefix=options.prefix??`rl:`}async increment(key){let now=Date.now(),entry=await this.read(key);if(entry&&entry.reset>now)return entry.hits++,await this.write(key,entry),{totalHits:entry.hits,resetTime:new Date(entry.reset)};let fresh={hits:1,reset:now+this.windowMs};return await this.write(key,fresh),{totalHits:1,resetTime:new Date(fresh.reset)}}async decrement(key){let entry=await this.read(key);entry&&entry.hits>0&&(entry.hits--,await this.write(key,entry))}async reset(key){await this.kv.delete(this.prefix+key)}async read(key){let raw=await this.kv.get(this.prefix+key);if(raw===null)return null;try{return JSON.parse(raw)}catch{return null}}async write(key,entry){await this.kv.put(this.prefix+key,JSON.stringify(entry),{expirationTtl:kvTtlSeconds(Math.max(entry.reset-Date.now(),1e3))})}},KvSessionStore=class{kv;prefix;constructor(kv,options={}){this.kv=kv,this.prefix=options.prefix??`sess:`}async get(sid){let raw=await this.kv.get(this.prefix+sid);if(raw===null)return null;try{return JSON.parse(raw)}catch{return null}}async set(sid,data,maxAge){await this.kv.put(this.prefix+sid,JSON.stringify(data),{expirationTtl:kvTtlSeconds(maxAge)})}async destroy(sid){await this.kv.delete(this.prefix+sid)}async touch(sid,maxAge){let data=await this.get(sid);data!==null&&await this.set(sid,data,maxAge)}};export{KvSessionStore as n,rateLimitGuard as r,KvRateLimitStore as t};
12
- //# sourceMappingURL=kv-stores-zApd2iLD.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"kv-stores-zApd2iLD.mjs","names":[],"sources":["../src/http/middleware/rate-limit-guard.ts","../src/http/middleware/kv-stores.ts"],"sourcesContent":["// Ctx-style rate limiter — runs on BOTH pipelines: the node runtimes (via\n// `@Middleware()` / class middleware) and the `@forinda/kickjs/web` fetch\n// entry (via `@Middleware()` or `createWebApp({ middleware })`), unlike the\n// connect-style `rateLimit()` which is node-only. Zero runtime imports —\n// part of the edge purity graph.\nimport type { MiddlewareHandler } from '../../core/decorators'\nimport type { RequestContext } from '../context'\nimport type { RateLimitStore } from './rate-limit'\n\nexport interface RateLimitGuardOptions {\n /** Maximum number of requests per window (default: 100). */\n max?: number\n /** Time window in milliseconds (default: 60_000). */\n windowMs?: number\n /** Response message when the limit is exceeded (default: 'Too Many Requests'). */\n message?: string\n /** HTTP status code when the limit is exceeded (default: 429). */\n statusCode?: number\n /**\n * Rate-limit key per request. Default: `cf-connecting-ip`, then the first\n * hop of `x-forwarded-for`, then `x-real-ip`, then `'global'`. Return a\n * user/tenant id here for authenticated quotas.\n */\n keyGenerator?: (ctx: RequestContext) => string\n /** Send `X-RateLimit-*` / `Retry-After` headers (default: true). */\n headers?: boolean\n /**\n * Counter backend. Defaults to a per-isolate in-memory map — fine for a\n * single node process, useless on edge where isolates recycle: pass a\n * `KvRateLimitStore` (or Redis-backed store) there.\n */\n store?: RateLimitStore\n /** Skip limiting for a request (health checks, allowlists). */\n skip?: (ctx: RequestContext) => boolean\n}\n\n/** Timer-free in-memory store: sweeps expired entries when the map grows. */\nclass LazyMemoryStore implements RateLimitStore {\n private readonly hits = new Map<string, { hits: number; reset: number }>()\n\n constructor(private readonly windowMs: number) {}\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n if (entry && entry.reset > now) {\n entry.hits++\n return { totalHits: entry.hits, resetTime: new Date(entry.reset) }\n }\n // ponytail: sweep-on-growth instead of a cleanup interval — no timer to\n // dispose, edge-safe. Bound is 10k live keys before a full sweep.\n if (this.hits.size > 10_000) {\n for (const [k, e] of this.hits) if (e.reset <= now) this.hits.delete(k)\n }\n const fresh = { hits: 1, reset: now + this.windowMs }\n this.hits.set(key, fresh)\n return { totalHits: 1, resetTime: new Date(fresh.reset) }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.hits > 0) entry.hits--\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n}\n\nfunction defaultKey(ctx: RequestContext): string {\n // Prefer the runtime-computed client IP (Express derives req.ip from the\n // app's `trust proxy` setting) — raw forwarded headers are client-spoofable\n // on node deployments that don't normalize them. Header fallbacks cover\n // runtimes without an `ip` field (the web/edge entry).\n const reqIp = (ctx.req as { ip?: unknown }).ip\n if (typeof reqIp === 'string' && reqIp.length > 0) return reqIp\n const h = ctx.headers\n const first = (v: unknown): string | undefined =>\n typeof v === 'string' && v.length > 0 ? v.split(',')[0].trim() : undefined\n return (\n first(h['cf-connecting-ip']) ?? first(h['x-forwarded-for']) ?? first(h['x-real-ip']) ?? 'global'\n )\n}\n\n/**\n * Rate limiting as a `(ctx, next)` middleware.\n *\n * ```ts\n * // Edge (Cloudflare Workers) — app-wide:\n * const app = createWebApp({\n * h3, modules,\n * middleware: [rateLimitGuard({ max: 60, windowMs: 60_000, store: new KvRateLimitStore(env.KV, { windowMs: 60_000 }) })],\n * })\n *\n * // Any runtime — per controller/route:\n * @Middleware(rateLimitGuard({ max: 10 }))\n * @Post('/login')\n * ```\n */\nexport function rateLimitGuard(options: RateLimitGuardOptions = {}): MiddlewareHandler {\n const max = options.max ?? 100\n const windowMs = options.windowMs ?? 60_000\n const message = options.message ?? 'Too Many Requests'\n const statusCode = options.statusCode ?? 429\n const keyGenerator = options.keyGenerator ?? defaultKey\n const sendHeaders = options.headers ?? true\n const store = options.store ?? new LazyMemoryStore(windowMs)\n\n return async (ctx: RequestContext, next: () => void): Promise<void> => {\n if (options.skip?.(ctx)) return next()\n\n const { totalHits, resetTime } = await store.increment(keyGenerator(ctx))\n const remaining = Math.max(0, max - totalHits)\n\n if (sendHeaders) {\n ctx.setHeader('X-RateLimit-Limit', String(max))\n ctx.setHeader('X-RateLimit-Remaining', String(remaining))\n ctx.setHeader('X-RateLimit-Reset', String(Math.ceil(resetTime.getTime() / 1000)))\n }\n\n if (totalHits > max) {\n const retryAfter = Math.max(1, Math.ceil((resetTime.getTime() - Date.now()) / 1000))\n if (sendHeaders) ctx.setHeader('Retry-After', String(retryAfter))\n ctx.json({ message }, statusCode)\n return\n }\n\n next()\n }\n}\n","// Edge-safe store adapters over a minimal KV surface. Zero runtime imports —\n// this file must stay importable from the `@forinda/kickjs/web` purity graph.\nimport type { RateLimitStore } from './rate-limit'\nimport type { SessionData, SessionStore } from './session'\n\n/**\n * The minimal async KV surface both stores need. Structurally compatible\n * with a Cloudflare Workers `KVNamespace` binding — pass it straight in:\n *\n * ```ts\n * export default {\n * fetch(req: Request, env: Env) {\n * const store = new KvRateLimitStore(env.MY_KV, { windowMs: 60_000 })\n * // ...\n * },\n * }\n * ```\n *\n * Anything with the same shape works too (Deno KV wrapper, Upstash Redis\n * REST client adapter, a Map-backed fake in tests).\n */\nexport interface KvLike {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/** Cloudflare KV rejects TTLs under 60 seconds — clamp up, never down. */\nfunction kvTtlSeconds(ms: number): number {\n return Math.max(60, Math.ceil(ms / 1000))\n}\n\nexport interface KvRateLimitStoreOptions {\n /** Window length in ms — must match the `windowMs` given to the limiter. */\n windowMs: number\n /** Key prefix in the KV namespace (default `'rl:'`). */\n prefix?: string\n}\n\n/**\n * {@link RateLimitStore} over a {@link KvLike} namespace, for edge deployments\n * where in-memory counters reset on every isolate.\n *\n * KV is eventually consistent and this is a read-modify-write counter, so\n * limiting is **approximate** under concurrency — bursts racing across\n * isolates can each read the same count. That is the accepted trade-off for\n * abuse throttling; use a Durable Object / Redis store when you need an\n * exact ceiling (billing quotas, strict per-key locks).\n */\nexport class KvRateLimitStore implements RateLimitStore {\n private readonly prefix: string\n private readonly windowMs: number\n\n constructor(\n private readonly kv: KvLike,\n options: KvRateLimitStoreOptions,\n ) {\n this.windowMs = options.windowMs\n this.prefix = options.prefix ?? 'rl:'\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = await this.read(key)\n if (entry && entry.reset > now) {\n entry.hits++\n await this.write(key, entry)\n return { totalHits: entry.hits, resetTime: new Date(entry.reset) }\n }\n const fresh = { hits: 1, reset: now + this.windowMs }\n await this.write(key, fresh)\n return { totalHits: 1, resetTime: new Date(fresh.reset) }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = await this.read(key)\n if (entry && entry.hits > 0) {\n entry.hits--\n await this.write(key, entry)\n }\n }\n\n async reset(key: string): Promise<void> {\n await this.kv.delete(this.prefix + key)\n }\n\n private async read(key: string): Promise<{ hits: number; reset: number } | null> {\n const raw = await this.kv.get(this.prefix + key)\n if (raw === null) return null\n try {\n return JSON.parse(raw) as { hits: number; reset: number }\n } catch {\n return null\n }\n }\n\n private async write(key: string, entry: { hits: number; reset: number }): Promise<void> {\n await this.kv.put(this.prefix + key, JSON.stringify(entry), {\n expirationTtl: kvTtlSeconds(Math.max(entry.reset - Date.now(), 1000)),\n })\n }\n}\n\nexport interface KvSessionStoreOptions {\n /** Key prefix in the KV namespace (default `'sess:'`). */\n prefix?: string\n}\n\n/**\n * {@link SessionStore} over a {@link KvLike} namespace. Plug into the node\n * `session()` middleware today (e.g. backed by Upstash/Cloudflare KV via\n * REST) so sessions survive restarts and horizontal scaling; TTL expiry is\n * delegated to the KV layer via `expirationTtl`.\n *\n * Cost note: `touch()` is a get + put round-trip (KvLike has no TTL-only\n * refresh), so `session({ rolling: true })` doubles KV operations per\n * request — per-operation-billed platforms may prefer `rolling: false`.\n */\nexport class KvSessionStore implements SessionStore {\n private readonly prefix: string\n\n constructor(\n private readonly kv: KvLike,\n options: KvSessionStoreOptions = {},\n ) {\n this.prefix = options.prefix ?? 'sess:'\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const raw = await this.kv.get(this.prefix + sid)\n if (raw === null) return null\n try {\n return JSON.parse(raw) as SessionData\n } catch {\n return null\n }\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n await this.kv.put(this.prefix + sid, JSON.stringify(data), {\n expirationTtl: kvTtlSeconds(maxAge),\n })\n }\n\n async destroy(sid: string): Promise<void> {\n await this.kv.delete(this.prefix + sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const data = await this.get(sid)\n if (data !== null) await this.set(sid, data, maxAge)\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"request-logger-BU4ulKLp.d.mts","names":[],"sources":["../src/http/middleware/request-logger.ts"],"mappings":";;;UAGiB;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;iBAmBc,cAAc,UAAS,wBAAyB,KAKjD,SAAO,KAAO,UAAQ,MAAQ"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"spa-DJbVV_4F.d.mts","names":[],"sources":["../src/core/disposables.ts","../src/core/kick-error.ts","../src/core/circuit-breaker.ts","../src/core/context-errors.ts","../src/core/mount-sort.ts","../src/core/asset-keys.ts","../src/http/cluster.ts","../src/http/define-http-context-decorator.ts","../src/http/runtimes/express.ts","../src/http/request-store.ts","../src/http/middleware/request-scope.ts","../src/http/middleware/trace-context.ts","../src/http/middleware/views.ts","../src/http/middleware/spa.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAaY,0BAA0B;;iBAKtB,mBAAmB,IAAI;;iBA6BjB,cAAc;;;;;;;;;;;;;;;;;;;;UC9BnB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,UAAU;;cAGC,kBAAkB;WACb;WACA;WACA;WACA;WACA;WACA,UAAU;EAE1B,YAAY,MAAM;;UAiDH;;;;;;EAMf;;;;;;;iBAQc,gBACd,KAAK,gBAAgB,WACrB,UAAS;;;;;;;;;;;;;;;;;;;;KCvFC;UAEK;;EAEf;;EAEA;;EAEA;;UAGe;EACf;EACA;EACA,OAAO;EACP,cAAc;;;;;cAMH,yBAAyB;WACR;EAA5B,YAA4B;;cAMjB;WAaO;UAZV;UACA;UACA;UACA;UACA;UACA;mBAES;mBACA;mBACA;EAEjB,YACkB,cAChB,SAAS;;;;;EAWL,QAAQ,GAAG,UAAU,QAAQ,KAAK,QAAQ;;EA0BhD,YAAY;;EAMZ,YAAY;;EAWZ;UASQ;UAWA;UAeA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cC1HG,gCAAgC;WAClC;WACA;WACA;WACA;EAET,YAAY,aAAa,mBAAmB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA0CjD,iCAAiC;WACnC;WACA;EAET,YAAY,aAAa;;;;;;;;;;;;cA6Cd,8BAA8B;WAChC;WACA;EAET,YAAY,0BAA0B;;;;;;;;;;;;;;;;;;;cA0B3B,kCAAkC;WACpC;WACA;EAET,YAAY,aAAa;;;;;;;;;;;;;;;;;;;;;UC5IV;;EAEf;;;;;;EAMA;;;KAIU;;;;;cAMC,gCAAgC;WAClC,MAAM;WACN;EAET,YAAY,MAAM,WAAW;;;;;;;;cAclB,6BAA6B;WAC/B,MAAM;WACN;WACA;EAET,YAAY,MAAM,WAAW,iBAAiB;;;;;;;cAiBnC,wBAAwB;WAC1B,MAAM;WACN;EAET,YAAY,MAAM,WAAW;;;;;;;;;;;;;;;;iBAsBf,UAAU,UAAU,eAAe,gBAAgB,KAAK,MAAM,YAAY;;;KC/E9E;UAEK;;EAEf;;EAEA;;UAGe;;;;;;;;EAQf,WAAW;;UAGI;EACf,OAAO;;;;;;;;EAQP;;;;;;;;;;;;iBAac,eACd,mBACA,0BACA,OAAM,wBACL;;iBA2Da,SAAS;;;UCzHR;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIc;;;;;;;;UCwDC,qCAAqC,UAAU;GAC7D,UAAU,gBAAgB,UAAU,eAAe,YAAY,uBAC9D,MAAM,qBAAqB,GAAG,GAAG,GAAG,kBACnC,iBAAiB,GAAG,GAAG,GAAG;;;;;;;UAQd;GAEb,UAAU,gBACV,UAAU,eAAe,YAAY,uBACrC,UAAU,0BAA0B,uBAEpC,MAAM,qBAAqB,GAAG,GAAG,GAAG,kBACnC,iBAAiB,GAAG,GAAG,GAAG;;;;;;;;;;;;;;;;EAiB7B,WAAW,UAAU,4BAA4B,qCAAqC;;cAW3E,4BAA4B;;;;;;;;;;;iBCpFzB,kBAAkB,SAAS,eAAe;;;;;;;;;iBAiE1C,YAAY,sBAAsB,UAAS,qBAA0B;;;;;iBAQrE,kBAAkB,YAAY;;;;UC5G7B;;EAEf;;;;;;EAMA,WAAW;;;;;;;;;;;;;;;;;;;;;EAqBX,QAAQ;;EAER;;;cAIW,cAAY,kBAAA;;;;;iBAMT,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoCnB,gBAAgB,UAAU,gBAAgB,KAAK,IAAI,UAAU;;;;;;;;;;;iBCe7D,2BAAsB,KACnB,SAAO,KAAO,UAAQ,MAAQ;;;;;;;cCxFpC;UAKI;;;;;EAKf;;;;;UAMe;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;iBAqBc,iBAAiB,iBAAiB;;;;;;;;;;;;;;;;;;;;;;iBA0ClC,aAAa,UAAS,uBAAwB,KAC/C,SAAO,KAAO,UAAQ,MAAQ;;;UC5F5B;;;;;;;;;;;;;;;EAef;;EAGA;;EAGA;;EAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAgCW,aAAW,eAAA;;;UCtDP;;;;;;;;;;;;;;EAcf;;;;;;;;;;;;;;;EAgBA;;;;;;;;;;EAWA;;;;;EAMA;;;;;EAMA;;;;;;;;;;;;;;EAeA;;;;;;;;;;;;;;;;;;;;;iBAoCc,iBACd,mBAKA,cACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAoJW,YAAU,eAAA"}