@forinda/kickjs 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/{adapter-DhjoIYVI.d.mts → adapter-DjfRMob8.d.mts} +3 -13
  2. package/dist/{adapter-DhjoIYVI.d.mts.map → adapter-DjfRMob8.d.mts.map} +1 -1
  3. package/dist/application-BSJpaiSY.mjs +12 -0
  4. package/dist/{application-nDj-76EX.mjs.map → application-BSJpaiSY.mjs.map} +1 -1
  5. package/dist/{application-DqD58rhF.d.mts → application-CGxZCYl1.d.mts} +8 -8
  6. package/dist/{application-DqD58rhF.d.mts.map → application-CGxZCYl1.d.mts.map} +1 -1
  7. package/dist/{bootstrap-BqcT1h9H.mjs → bootstrap-CbTF6xvv.mjs} +3 -3
  8. package/dist/{bootstrap-BqcT1h9H.mjs.map → bootstrap-CbTF6xvv.mjs.map} +1 -1
  9. package/dist/{client-ip-DMKc-Trg.mjs → client-ip-pcINkkPr.mjs} +2 -2
  10. package/dist/{client-ip-DMKc-Trg.mjs.map → client-ip-pcINkkPr.mjs.map} +1 -1
  11. package/dist/config/index.d.mts +1 -1
  12. package/dist/config/index.mjs +2 -2
  13. package/dist/{config-DrgIbebd.mjs → config-DmGiAIuH.mjs} +3 -3
  14. package/dist/{config-DrgIbebd.mjs.map → config-DmGiAIuH.mjs.map} +1 -1
  15. package/dist/{container-ZnfelY_a.mjs → container-CAPN7pBG.mjs} +3 -3
  16. package/dist/{container-ZnfelY_a.mjs.map → container-CAPN7pBG.mjs.map} +1 -1
  17. package/dist/{context-errors-Cgtrrqfs.mjs → context-errors-DYJjhalQ.mjs} +2 -2
  18. package/dist/{context-errors-Cgtrrqfs.mjs.map → context-errors-DYJjhalQ.mjs.map} +1 -1
  19. package/dist/context-gpvcMlG5.mjs +12 -0
  20. package/dist/context-gpvcMlG5.mjs.map +1 -0
  21. package/dist/{contributor-runner-BSpshfPN.mjs → contributor-runner-vk9XlOED.mjs} +3 -3
  22. package/dist/{contributor-runner-BSpshfPN.mjs.map → contributor-runner-vk9XlOED.mjs.map} +1 -1
  23. package/dist/core/adapter.d.mts +1 -1
  24. package/dist/core/adapter.mjs +1 -1
  25. package/dist/core/app-module.mjs +1 -1
  26. package/dist/core/cache.mjs +1 -1
  27. package/dist/core/container.mjs +2 -2
  28. package/dist/core/cron.mjs +2 -2
  29. package/dist/core/decorators.d.mts +1 -1
  30. package/dist/core/decorators.mjs +2 -2
  31. package/dist/core/decorators.mjs.map +1 -1
  32. package/dist/core/errors.mjs +1 -1
  33. package/dist/core/interfaces.mjs +1 -1
  34. package/dist/core/logger.mjs +1 -1
  35. package/dist/core/path.mjs +1 -1
  36. package/dist/core/plugin.d.mts +1 -1
  37. package/dist/core/plugin.mjs +1 -1
  38. package/dist/core/reactivity.mjs +1 -1
  39. package/dist/{core-_WNyghl_.mjs → core-DKrKVLfj.mjs} +3 -3
  40. package/dist/{core-_WNyghl_.mjs.map → core-DKrKVLfj.mjs.map} +1 -1
  41. package/dist/{csrf-COFjRrUz.d.mts → csrf-C73UkRgc.d.mts} +2 -2
  42. package/dist/{csrf-COFjRrUz.d.mts.map → csrf-C73UkRgc.d.mts.map} +1 -1
  43. package/dist/{csrf-BcxqzCZW.mjs → csrf-CxJItZVa.mjs} +3 -3
  44. package/dist/{csrf-BcxqzCZW.mjs.map → csrf-CxJItZVa.mjs.map} +1 -1
  45. package/dist/{decorate-BBwu7--P.mjs → decorate-DfH1NIqV.mjs} +1 -1
  46. package/dist/{decorators-DOcWsXci.d.mts → decorators-BxNSz9Qt.d.mts} +4 -4
  47. package/dist/{decorators-DOcWsXci.d.mts.map → decorators-BxNSz9Qt.d.mts.map} +1 -1
  48. package/dist/{define-adapter-CajLeMzM.mjs → define-adapter-C6lFhCL-.mjs} +2 -2
  49. package/dist/{define-adapter-CajLeMzM.mjs.map → define-adapter-C6lFhCL-.mjs.map} +1 -1
  50. package/dist/{disposables-DIXt8ze-.mjs → disposables-NlW6Mibk.mjs} +2 -2
  51. package/dist/{disposables-DIXt8ze-.mjs.map → disposables-NlW6Mibk.mjs.map} +1 -1
  52. package/dist/{env-DtJ-6gyL.mjs → env-__fzPsHD.mjs} +3 -3
  53. package/dist/{env-DtJ-6gyL.mjs.map → env-__fzPsHD.mjs.map} +1 -1
  54. package/dist/{error-handler-DiueQHes.d.mts → error-handler-itG65lj0.d.mts} +2 -2
  55. package/dist/{error-handler-DiueQHes.d.mts.map → error-handler-itG65lj0.d.mts.map} +1 -1
  56. package/dist/handler-DnZbcleS.mjs +13 -0
  57. package/dist/handler-DnZbcleS.mjs.map +1 -0
  58. package/dist/http/application.d.mts +1 -1
  59. package/dist/http/application.mjs +2 -2
  60. package/dist/http/bootstrap.d.mts +1 -1
  61. package/dist/http/bootstrap.mjs +2 -2
  62. package/dist/http/context.d.mts +1 -1
  63. package/dist/http/context.mjs +2 -2
  64. package/dist/http/middleware/cors.mjs +1 -1
  65. package/dist/http/middleware/csrf.d.mts +1 -1
  66. package/dist/http/middleware/csrf.mjs +2 -2
  67. package/dist/http/middleware/error-handler.d.mts +1 -1
  68. package/dist/http/middleware/error-handler.mjs +2 -2
  69. package/dist/http/middleware/helmet.mjs +1 -1
  70. package/dist/http/middleware/rate-limit.mjs +2 -2
  71. package/dist/http/middleware/request-id.mjs +1 -1
  72. package/dist/http/middleware/request-logger.mjs +2 -2
  73. package/dist/http/middleware/session.mjs +2 -2
  74. package/dist/http/middleware/spa.d.mts +1 -1
  75. package/dist/http/middleware/spa.mjs +2 -2
  76. package/dist/http/middleware/upload.d.mts +1 -1
  77. package/dist/http/middleware/upload.mjs +2 -2
  78. package/dist/http/middleware/validate.mjs +1 -1
  79. package/dist/http/middleware/views.d.mts +1 -1
  80. package/dist/http/middleware/views.mjs +2 -2
  81. package/dist/http/query/index.mjs +2 -2
  82. package/dist/http/router-builder.d.mts +1 -1
  83. package/dist/http/router-builder.mjs +2 -2
  84. package/dist/http/runtimes/fastify.d.mts +3 -1
  85. package/dist/http/runtimes/fastify.d.mts.map +1 -1
  86. package/dist/http/runtimes/fastify.mjs +2 -2
  87. package/dist/http/runtimes/fastify.mjs.map +1 -1
  88. package/dist/http/runtimes/h3-web.d.mts +1 -1
  89. package/dist/http/runtimes/h3-web.mjs +2 -2
  90. package/dist/http/runtimes/h3.d.mts +1 -1
  91. package/dist/http/runtimes/h3.mjs +2 -2
  92. package/dist/http/runtimes/h3.mjs.map +1 -1
  93. package/dist/{index-CDn-p3tq.d.mts → index-CNHSbZTE.d.mts} +4 -4
  94. package/dist/{index-CDn-p3tq.d.mts.map → index-CNHSbZTE.d.mts.map} +1 -1
  95. package/dist/index.d.mts +9 -9
  96. package/dist/index.mjs +2 -2
  97. package/dist/index.mjs.map +1 -1
  98. package/dist/{jobs-dx-MTUUb.mjs → jobs-BmpeFvi0.mjs} +3 -3
  99. package/dist/{jobs-dx-MTUUb.mjs.map → jobs-BmpeFvi0.mjs.map} +1 -1
  100. package/dist/{kv-stores-CetFF9N_.d.mts → kv-stores-BZTe23Tw.d.mts} +3 -3
  101. package/dist/{kv-stores-CetFF9N_.d.mts.map → kv-stores-BZTe23Tw.d.mts.map} +1 -1
  102. package/dist/{kv-stores-Ba5_rcz-.mjs → kv-stores-DJwxntyw.mjs} +3 -3
  103. package/dist/{kv-stores-Ba5_rcz-.mjs.map → kv-stores-DJwxntyw.mjs.map} +1 -1
  104. package/dist/{metadata-CW4I35nN.mjs → metadata-CzzRUnAx.mjs} +2 -2
  105. package/dist/{metadata-CW4I35nN.mjs.map → metadata-CzzRUnAx.mjs.map} +1 -1
  106. package/dist/{module-registry-kqozOSo0.mjs → module-registry-BnmRNy-3.mjs} +2 -2
  107. package/dist/{module-registry-kqozOSo0.mjs.map → module-registry-BnmRNy-3.mjs.map} +1 -1
  108. package/dist/{observers-CC3sO1BM.mjs → observers-COlQfinD.mjs} +2 -2
  109. package/dist/{observers-CC3sO1BM.mjs.map → observers-COlQfinD.mjs.map} +1 -1
  110. package/dist/{query-DPlkWOw6.mjs → query-B5DTq_4W.mjs} +2 -2
  111. package/dist/{query-DPlkWOw6.mjs.map → query-B5DTq_4W.mjs.map} +1 -1
  112. package/dist/reply-CbTYIj_e.mjs +12 -0
  113. package/dist/reply-CbTYIj_e.mjs.map +1 -0
  114. package/dist/{respond-DjxNwGnN.mjs → respond-wbMSqHYo.mjs} +2 -2
  115. package/dist/{respond-DjxNwGnN.mjs.map → respond-wbMSqHYo.mjs.map} +1 -1
  116. package/dist/{route-flag-sIYXay3q.mjs → route-flag-Bb7n9Mnp.mjs} +3 -3
  117. package/dist/{route-flag-sIYXay3q.mjs.map → route-flag-Bb7n9Mnp.mjs.map} +1 -1
  118. package/dist/{route-policy-B6_toGJW.mjs → route-policy-C_Wsc-b1.mjs} +2 -2
  119. package/dist/{route-policy-B6_toGJW.mjs.map → route-policy-C_Wsc-b1.mjs.map} +1 -1
  120. package/dist/{router-builder-BSBSYgYW.d.mts → router-builder-CDkW88KQ.d.mts} +2 -2
  121. package/dist/{router-builder-BSBSYgYW.d.mts.map → router-builder-CDkW88KQ.d.mts.map} +1 -1
  122. package/dist/{router-builder-CzVjPs5y.mjs → router-builder-DawnRzlp.mjs} +3 -3
  123. package/dist/{router-builder-CzVjPs5y.mjs.map → router-builder-DawnRzlp.mjs.map} +1 -1
  124. package/dist/{upload-DjhnyHHm.d.mts → upload-Cy7qQy7M.d.mts} +3 -3
  125. package/dist/{upload-DjhnyHHm.d.mts.map → upload-Cy7qQy7M.d.mts.map} +1 -1
  126. package/dist/{upload-config-d0pisVsK.mjs → upload-config-D01Ty40f.mjs} +2 -2
  127. package/dist/{upload-config-d0pisVsK.mjs.map → upload-config-D01Ty40f.mjs.map} +1 -1
  128. package/dist/web.d.mts +3 -3
  129. package/dist/web.mjs +2 -2
  130. package/dist/web.mjs.map +1 -1
  131. package/package.json +2 -2
  132. package/dist/application-nDj-76EX.mjs +0 -12
  133. package/dist/context-BGPof8bs.mjs +0 -12
  134. package/dist/context-BGPof8bs.mjs.map +0 -1
  135. package/dist/handler-2OebsoPr.mjs +0 -13
  136. package/dist/handler-2OebsoPr.mjs.map +0 -1
  137. package/dist/runtime-CDz5ayBt.mjs +0 -12
  138. package/dist/runtime-CDz5ayBt.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
1
 
2
- import { B as SourcedRegistration, g as RouteEntry } from "./adapter-DhjoIYVI.mjs";
2
+ import { B as SourcedRegistration, g as RouteEntry } from "./adapter-DjfRMob8.mjs";
3
3
  import { s as RouteFlagDeclarations } from "./route-flag-B1lQht6v.mjs";
4
4
  //#region src/http/router-builder.d.ts
5
5
  /** @internal — set by Application.setup() before each `mod.routes()` call. */
@@ -53,4 +53,4 @@ interface BuildRoutesOptions {
53
53
  declare function buildRouteTable(controllerClass: any, options?: BuildRoutesOptions): RouteEntry[];
54
54
  //#endregion
55
55
  export { buildRouteTable as i, _setExternalContributorSources as n, assertRouteUnique as r, BuildRoutesOptions as t };
56
- //# sourceMappingURL=router-builder-BSBSYgYW.d.mts.map
56
+ //# sourceMappingURL=router-builder-CDkW88KQ.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"router-builder-BSBSYgYW.d.mts","names":[],"sources":["../src/http/router-builder.ts"],"mappings":";;;;;iBA2CgB,+BAA+B,kBAAkB;;;;;;;;;;;iBAcjD,kBACd,UAAU,qBACV,gBACA,kBACA;UAUe;;;;;;;EAOf,2BAA2B;;;;;;EAM3B,aAAa;;;;;;;;;;;;;;;;;;;;;;iBAuBC,gBACd,sBACA,UAAS,qBACR"}
1
+ {"version":3,"file":"router-builder-CDkW88KQ.d.mts","names":[],"sources":["../src/http/router-builder.ts"],"mappings":";;;;;iBA2CgB,+BAA+B,kBAAkB;;;;;;;;;;;iBAcjD,kBACd,UAAU,qBACV,gBACA,kBACA;UAUe;;;;;;;EAOf,2BAA2B;;;;;;EAM3B,aAAa;;;;;;;;;;;;;;;;;;;;;;iBAuBC,gBACd,sBACA,UAAS,qBACR"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v9.1.0
2
+ * @forinda/kickjs v9.2.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{METADATA}from"./core/interfaces.mjs";import{o as duplicateRouteError,t as Container}from"./container-ZnfelY_a.mjs";import{a as getMethodMeta,o as getMethodMetaOrUndefined,t as getClassMeta}from"./metadata-CW4I35nN.mjs";import{a as getMethodFlagDeclarations,c as registerMountFlags,i as getClassFlagDeclarations,l as resolveRouteFlags,u as toFlagDeclarations}from"./route-flag-sIYXay3q.mjs";import{i as MissingContributorError,n as DuplicateContributorError,t as ContributorCycleError}from"./context-errors-Cgtrrqfs.mjs";import{t as runContributors}from"./contributor-runner-BSpshfPN.mjs";const PRECEDENCE_ORDER=[`method`,`class`,`module`,`adapter`,`global`];function buildPipeline(sources,options={}){let route=options.route,bySource=new Map;for(let src of PRECEDENCE_ORDER)bySource.set(src,[]);for(let entry of sources)bySource.get(entry.source).push(entry);let winners=new Map;for(let src of PRECEDENCE_ORDER){let rows=bySource.get(src),keysAtThisLevel=new Map;for(let row of rows){let key=row.registration.key,existing=keysAtThisLevel.get(key)??[];existing.push(row),keysAtThisLevel.set(key,existing)}for(let[key,rowsForKey]of keysAtThisLevel){if(rowsForKey.length>1){let labels=rowsForKey.map((r,i)=>r.label??`${src}#${i}`);throw new DuplicateContributorError(key,labels)}winners.has(key)||winners.set(key,rowsForKey[0])}}let survivors=[...winners.values()].map(r=>r.registration),allKeys=new Set(survivors.map(r=>r.key));for(let reg of survivors)for(let dep of reg.dependsOn)if(!allKeys.has(dep))throw new MissingContributorError(dep,reg.key,route,reg.definedAt);let sorted=topoSort(survivors,route);return Object.freeze({contributors:Object.freeze(sorted),keys:allKeys})}function splitBeforeValidation(pipeline,route){let early=pipeline.contributors.filter(reg=>reg.beforeValidation);if(early.length===0)return{early:null,late:pipeline};let earlyKeys=new Set(early.map(reg=>reg.key));for(let reg of early){let lateDep=reg.dependsOn.find(dep=>!earlyKeys.has(dep));if(lateDep!==void 0)throw Error(`Contributor '${reg.key}'${route?` on ${route}`:``} sets beforeValidation but depends on '${lateDep}', which runs after validation. Set beforeValidation on '${lateDep}' too, or remove it from '${reg.key}'.`)}let late=pipeline.contributors.filter(reg=>!reg.beforeValidation);return{early:Object.freeze({contributors:Object.freeze(early),keys:earlyKeys}),late:late.length===0?null:Object.freeze({contributors:Object.freeze(late),keys:new Set(late.map(reg=>reg.key))})}}function topoSort(registrations,route){let indegree=new Map,dependents=new Map,byKey=new Map;for(let reg of registrations)indegree.set(reg.key,reg.dependsOn.length),byKey.set(reg.key,reg);for(let reg of registrations)for(let dep of reg.dependsOn){let list=dependents.get(dep)??[];list.push(reg),dependents.set(dep,list)}let queue=[];for(let reg of registrations)indegree.get(reg.key)===0&&queue.push(reg);let sorted=[];for(;queue.length>0;){let node=queue.shift();sorted.push(node);for(let dependent of dependents.get(node.key)??[]){let next=(indegree.get(dependent.key)??0)-1;indegree.set(dependent.key,next),next===0&&queue.push(dependent)}}if(sorted.length!==registrations.length){let remaining=registrations.filter(r=>(indegree.get(r.key)??0)>0);throw new ContributorCycleError(reconstructCycle(remaining,byKey),route)}return sorted}function reconstructCycle(residual,byKey){let residualKeys=new Set(residual.map(r=>r.key)),start=residual[0].key,path=[start],seen=new Set([start]),cursor=start;for(;;){let nextDep=byKey.get(cursor).dependsOn.find(d=>residualKeys.has(d));if(nextDep===void 0)break;if(seen.has(nextDep)){let cycleStart=path.indexOf(nextDep);return[...path.slice(cycleStart),nextDep]}path.push(nextDep),seen.add(nextDep),cursor=nextDep}return path}let _externalContributorSources=[];function _setExternalContributorSources(sources){_externalContributorSources=sources}function assertRouteUnique(registry,method,fullPath,owner){let key=`${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g,`:*`)}`,prior=registry.get(key);if(prior!==void 0)throw duplicateRouteError(method.toUpperCase(),fullPath,owner,prior);registry.set(key,owner)}function buildRouteTable(controllerClass,options={}){let container=Container.getInstance(),externalSources=options.externalSources??_externalContributorSources,routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classMiddlewares=getClassMeta(METADATA.CLASS_MIDDLEWARES,controllerClass,[]),classContributors=getClassMeta(METADATA.CLASS_CONTRIBUTORS,controllerClass,[]),classFlagDeclarations=getClassFlagDeclarations(controllerClass),mountFlagDeclarations=toFlagDeclarations(options.mountFlags,`${controllerClass?.name??`controller`} mount flags`);registerMountFlags(controllerClass,mountFlagDeclarations);let entries=[];for(let route of routes){let method=route.method.toUpperCase(),fullPath=route.path||`/`,methodMiddlewares=getMethodMeta(METADATA.METHOD_MIDDLEWARES,controllerClass,route.handlerName,[]),fileUploadConfig=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName),methodContributors=getMethodMeta(METADATA.METHOD_CONTRIBUTORS,controllerClass,route.handlerName,[]),flags=resolveRouteFlags(classFlagDeclarations,getMethodFlagDeclarations(controllerClass,route.handlerName),mountFlagDeclarations),contributorRunner=null,earlyContributorRunner=null;if(classContributors.length>0||methodContributors.length>0||externalSources.length>0){let sources=[...methodContributors.map((registration,i)=>({source:`method`,registration,label:`${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`})),...classContributors.map((registration,i)=>({source:`class`,registration,label:`${controllerClass.name}.@class#${i}(${registration.key})`})),...externalSources],routeLabel=`${method} ${fullPath}`,{early,late}=splitBeforeValidation(buildPipeline(sources,{route:routeLabel}),routeLabel);early&&(earlyContributorRunner=ctx=>runContributors({pipeline:early,ctx,container,flags})),late&&(contributorRunner=ctx=>runContributors({pipeline:late,ctx,container,flags}))}let handler=ctx=>container.resolve(controllerClass)[route.handlerName](ctx);entries.push({method,path:fullPath,middlewares:[...classMiddlewares,...methodMiddlewares],contributorRunner,earlyContributorRunner,handler,meta:{controller:controllerClass,handlerName:route.handlerName,validation:route.validation,upload:fileUploadConfig,flags}})}return entries}export{buildPipeline as i,assertRouteUnique as n,buildRouteTable as r,_setExternalContributorSources as t};
12
- //# sourceMappingURL=router-builder-CzVjPs5y.mjs.map
11
+ import{METADATA}from"./core/interfaces.mjs";import{o as duplicateRouteError,t as Container}from"./container-CAPN7pBG.mjs";import{a as getMethodMeta,o as getMethodMetaOrUndefined,t as getClassMeta}from"./metadata-CzzRUnAx.mjs";import{a as getMethodFlagDeclarations,c as registerMountFlags,i as getClassFlagDeclarations,l as resolveRouteFlags,u as toFlagDeclarations}from"./route-flag-Bb7n9Mnp.mjs";import{i as MissingContributorError,n as DuplicateContributorError,t as ContributorCycleError}from"./context-errors-DYJjhalQ.mjs";import{t as runContributors}from"./contributor-runner-vk9XlOED.mjs";const PRECEDENCE_ORDER=[`method`,`class`,`module`,`adapter`,`global`];function buildPipeline(sources,options={}){let route=options.route,bySource=new Map;for(let src of PRECEDENCE_ORDER)bySource.set(src,[]);for(let entry of sources)bySource.get(entry.source).push(entry);let winners=new Map;for(let src of PRECEDENCE_ORDER){let rows=bySource.get(src),keysAtThisLevel=new Map;for(let row of rows){let key=row.registration.key,existing=keysAtThisLevel.get(key)??[];existing.push(row),keysAtThisLevel.set(key,existing)}for(let[key,rowsForKey]of keysAtThisLevel){if(rowsForKey.length>1){let labels=rowsForKey.map((r,i)=>r.label??`${src}#${i}`);throw new DuplicateContributorError(key,labels)}winners.has(key)||winners.set(key,rowsForKey[0])}}let survivors=[...winners.values()].map(r=>r.registration),allKeys=new Set(survivors.map(r=>r.key));for(let reg of survivors)for(let dep of reg.dependsOn)if(!allKeys.has(dep))throw new MissingContributorError(dep,reg.key,route,reg.definedAt);let sorted=topoSort(survivors,route);return Object.freeze({contributors:Object.freeze(sorted),keys:allKeys})}function splitBeforeValidation(pipeline,route){let early=pipeline.contributors.filter(reg=>reg.beforeValidation);if(early.length===0)return{early:null,late:pipeline};let earlyKeys=new Set(early.map(reg=>reg.key));for(let reg of early){let lateDep=reg.dependsOn.find(dep=>!earlyKeys.has(dep));if(lateDep!==void 0)throw Error(`Contributor '${reg.key}'${route?` on ${route}`:``} sets beforeValidation but depends on '${lateDep}', which runs after validation. Set beforeValidation on '${lateDep}' too, or remove it from '${reg.key}'.`)}let late=pipeline.contributors.filter(reg=>!reg.beforeValidation);return{early:Object.freeze({contributors:Object.freeze(early),keys:earlyKeys}),late:late.length===0?null:Object.freeze({contributors:Object.freeze(late),keys:new Set(late.map(reg=>reg.key))})}}function topoSort(registrations,route){let indegree=new Map,dependents=new Map,byKey=new Map;for(let reg of registrations)indegree.set(reg.key,reg.dependsOn.length),byKey.set(reg.key,reg);for(let reg of registrations)for(let dep of reg.dependsOn){let list=dependents.get(dep)??[];list.push(reg),dependents.set(dep,list)}let queue=[];for(let reg of registrations)indegree.get(reg.key)===0&&queue.push(reg);let sorted=[];for(;queue.length>0;){let node=queue.shift();sorted.push(node);for(let dependent of dependents.get(node.key)??[]){let next=(indegree.get(dependent.key)??0)-1;indegree.set(dependent.key,next),next===0&&queue.push(dependent)}}if(sorted.length!==registrations.length){let remaining=registrations.filter(r=>(indegree.get(r.key)??0)>0);throw new ContributorCycleError(reconstructCycle(remaining,byKey),route)}return sorted}function reconstructCycle(residual,byKey){let residualKeys=new Set(residual.map(r=>r.key)),start=residual[0].key,path=[start],seen=new Set([start]),cursor=start;for(;;){let nextDep=byKey.get(cursor).dependsOn.find(d=>residualKeys.has(d));if(nextDep===void 0)break;if(seen.has(nextDep)){let cycleStart=path.indexOf(nextDep);return[...path.slice(cycleStart),nextDep]}path.push(nextDep),seen.add(nextDep),cursor=nextDep}return path}let _externalContributorSources=[];function _setExternalContributorSources(sources){_externalContributorSources=sources}function assertRouteUnique(registry,method,fullPath,owner){let key=`${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g,`:*`)}`,prior=registry.get(key);if(prior!==void 0)throw duplicateRouteError(method.toUpperCase(),fullPath,owner,prior);registry.set(key,owner)}function buildRouteTable(controllerClass,options={}){let container=Container.getInstance(),externalSources=options.externalSources??_externalContributorSources,routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classMiddlewares=getClassMeta(METADATA.CLASS_MIDDLEWARES,controllerClass,[]),classContributors=getClassMeta(METADATA.CLASS_CONTRIBUTORS,controllerClass,[]),classFlagDeclarations=getClassFlagDeclarations(controllerClass),mountFlagDeclarations=toFlagDeclarations(options.mountFlags,`${controllerClass?.name??`controller`} mount flags`);registerMountFlags(controllerClass,mountFlagDeclarations);let entries=[];for(let route of routes){let method=route.method.toUpperCase(),fullPath=route.path||`/`,methodMiddlewares=getMethodMeta(METADATA.METHOD_MIDDLEWARES,controllerClass,route.handlerName,[]),fileUploadConfig=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName),methodContributors=getMethodMeta(METADATA.METHOD_CONTRIBUTORS,controllerClass,route.handlerName,[]),flags=resolveRouteFlags(classFlagDeclarations,getMethodFlagDeclarations(controllerClass,route.handlerName),mountFlagDeclarations),contributorRunner=null,earlyContributorRunner=null;if(classContributors.length>0||methodContributors.length>0||externalSources.length>0){let sources=[...methodContributors.map((registration,i)=>({source:`method`,registration,label:`${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`})),...classContributors.map((registration,i)=>({source:`class`,registration,label:`${controllerClass.name}.@class#${i}(${registration.key})`})),...externalSources],routeLabel=`${method} ${fullPath}`,{early,late}=splitBeforeValidation(buildPipeline(sources,{route:routeLabel}),routeLabel);early&&(earlyContributorRunner=ctx=>runContributors({pipeline:early,ctx,container,flags})),late&&(contributorRunner=ctx=>runContributors({pipeline:late,ctx,container,flags}))}let handler=ctx=>container.resolve(controllerClass)[route.handlerName](ctx);entries.push({method,path:fullPath,middlewares:[...classMiddlewares,...methodMiddlewares],contributorRunner,earlyContributorRunner,handler,meta:{controller:controllerClass,handlerName:route.handlerName,validation:route.validation,upload:fileUploadConfig,flags}})}return entries}export{buildPipeline as i,assertRouteUnique as n,buildRouteTable as r,_setExternalContributorSources as t};
12
+ //# sourceMappingURL=router-builder-DawnRzlp.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"router-builder-CzVjPs5y.mjs","names":[],"sources":["../src/core/contributor-pipeline.ts","../src/http/router-builder.ts"],"sourcesContent":["import type { AnyContributorRegistration } from './context-decorator'\nimport {\n ContributorCycleError,\n DuplicateContributorError,\n MissingContributorError,\n} from './context-errors'\n\n/**\n * Where a contributor was registered, ordered by precedence\n * (lower index wins). Mirrors `architecture.md` §20.4.\n *\n * Levels (highest → lowest precedence):\n * - `'method'` — `@LoadX` on a controller method.\n * - `'class'` — `@LoadX` on a controller class.\n * - `'module'` — returned by `AppModule.contributors?()`. Per-module scope.\n * - `'adapter'` — returned by `AppAdapter.contributors?()`. Cross-cutting, applies\n * to every module's routes.\n * - `'global'` — `ApplicationOptions.contributors`. App-wide default.\n */\nexport type ContributorSource = 'method' | 'class' | 'module' | 'adapter' | 'global'\n\n/** Precedence-ranked source list. Index = precedence; method (0) wins ties. */\nconst PRECEDENCE_ORDER: readonly ContributorSource[] = [\n 'method',\n 'class',\n 'module',\n 'adapter',\n 'global',\n]\n\n/**\n * Input row to {@link buildPipeline}. Pairs a registration with its origin\n * so dedup can apply the precedence rule and diagnostic errors can name\n * the offending site.\n *\n * `registration` is the type-erased {@link AnyContributorRegistration}\n * — the runner doesn't care about each registration's narrow `D` /\n * `Ctx` shape, and adopters frequently mix HTTP / transport-agnostic\n * contributors in the same `externalSources` / `contributors()`\n * array. The narrow generics from `defineContextDecorator` survive\n * at the *definition* site (where `resolve` is typed); they're erased\n * here so collections compose without `as any` casts.\n */\nexport interface SourcedRegistration {\n registration: AnyContributorRegistration\n source: ContributorSource\n /** Human label surfaced in DuplicateContributorError messages. */\n label?: string\n}\n\n/**\n * Resolved, validated, topo-sorted pipeline ready for execution.\n *\n * `contributors` is in execution order — every entry's `dependsOn`\n * keys appear earlier. `keys` is the set of all keys this pipeline\n * produces; the runner uses it for fast existence checks and the\n * builder uses it for missing-dep validation.\n */\nexport interface ContributorPipeline {\n readonly contributors: readonly AnyContributorRegistration[]\n readonly keys: ReadonlySet<string>\n}\n\nexport interface BuildPipelineOptions {\n /** Optional route identifier surfaced in diagnostic errors (e.g., 'GET /projects/:id'). */\n route?: string\n}\n\n/**\n * Build, validate, and topo-sort a contributor pipeline.\n *\n * Pipeline:\n *\n * 1. Group sources by precedence (method > class > module > adapter > global).\n * 2. Within each precedence level, two contributors with the same `key`\n * raise {@link DuplicateContributorError}.\n * 3. Across levels, the highest-precedence contributor wins each key\n * (lower-precedence duplicates are silently dropped — that is the\n * documented override mechanism, not an error).\n * 4. Every key referenced by `dependsOn` must be produced by the\n * surviving set, otherwise {@link MissingContributorError}.\n * 5. Kahn's algorithm topo-sorts the survivors. Any nodes left after\n * the queue drains form a cycle → {@link ContributorCycleError}\n * with a reconstructed path.\n *\n * Pure function — no DI, no I/O. Safe to call once at startup per\n * route and cache the result.\n */\nexport function buildPipeline(\n sources: readonly SourcedRegistration[],\n options: BuildPipelineOptions = {},\n): ContributorPipeline {\n const route = options.route\n\n // Step 1: group by source.\n const bySource = new Map<ContributorSource, SourcedRegistration[]>()\n for (const src of PRECEDENCE_ORDER) bySource.set(src, [])\n for (const entry of sources) bySource.get(entry.source)!.push(entry)\n\n // Step 2 + 3: dedup with precedence. We walk highest precedence first,\n // claim its keys, and drop lower-precedence rows that conflict.\n const winners = new Map<string, SourcedRegistration>()\n for (const src of PRECEDENCE_ORDER) {\n const rows = bySource.get(src)!\n const keysAtThisLevel = new Map<string, SourcedRegistration[]>()\n for (const row of rows) {\n const key = row.registration.key\n const existing = keysAtThisLevel.get(key) ?? []\n existing.push(row)\n keysAtThisLevel.set(key, existing)\n }\n for (const [key, rowsForKey] of keysAtThisLevel) {\n if (rowsForKey.length > 1) {\n const labels = rowsForKey.map((r, i) => r.label ?? `${src}#${i}`)\n throw new DuplicateContributorError(key, labels)\n }\n if (!winners.has(key)) winners.set(key, rowsForKey[0])\n }\n }\n\n const survivors = [...winners.values()].map((r) => r.registration)\n const allKeys = new Set(survivors.map((r) => r.key))\n\n // Step 4: dependsOn validation.\n for (const reg of survivors) {\n for (const dep of reg.dependsOn) {\n if (!allKeys.has(dep)) {\n throw new MissingContributorError(dep, reg.key, route, reg.definedAt)\n }\n }\n }\n\n // Step 5: Kahn topo-sort.\n const sorted = topoSort(survivors, route)\n\n return Object.freeze({\n contributors: Object.freeze(sorted),\n keys: allKeys,\n })\n}\n\n/**\n * Split a built pipeline into the contributors that run before request\n * validation (`beforeValidation: true`) and the rest, which keep their place\n * after `@Middleware()`. Both halves stay in topo order.\n *\n * An early contributor can only depend on other early ones — its dependency\n * would not have run yet otherwise — so that fails here, at boot.\n */\nexport function splitBeforeValidation(\n pipeline: ContributorPipeline,\n route?: string,\n): { early: ContributorPipeline | null; late: ContributorPipeline | null } {\n const early = pipeline.contributors.filter((reg) => reg.beforeValidation)\n if (early.length === 0) return { early: null, late: pipeline }\n\n const earlyKeys = new Set(early.map((reg) => reg.key))\n for (const reg of early) {\n const lateDep = reg.dependsOn.find((dep) => !earlyKeys.has(dep))\n if (lateDep !== undefined) {\n throw new Error(\n `Contributor '${reg.key}'${route ? ` on ${route}` : ''} sets beforeValidation but ` +\n `depends on '${lateDep}', which runs after validation. Set beforeValidation on ` +\n `'${lateDep}' too, or remove it from '${reg.key}'.`,\n )\n }\n }\n\n const late = pipeline.contributors.filter((reg) => !reg.beforeValidation)\n return {\n early: Object.freeze({ contributors: Object.freeze(early), keys: earlyKeys }),\n late:\n late.length === 0\n ? null\n : Object.freeze({\n contributors: Object.freeze(late),\n keys: new Set(late.map((reg) => reg.key)),\n }),\n }\n}\n\n/**\n * Kahn's algorithm with deterministic ordering on ties.\n *\n * Nodes with no remaining incoming edges enter the queue in input\n * order. Cycles surface as nodes still present after the queue\n * drains; we reconstruct one cycle path for diagnostics.\n */\nfunction topoSort(\n registrations: readonly AnyContributorRegistration[],\n route: string | undefined,\n): AnyContributorRegistration[] {\n // indegree[key] = number of unresolved dependsOn entries for key\n const indegree = new Map<string, number>()\n // dependents[depKey] = list of registrations that depend on depKey\n const dependents = new Map<string, AnyContributorRegistration[]>()\n const byKey = new Map<string, AnyContributorRegistration>()\n\n for (const reg of registrations) {\n indegree.set(reg.key, reg.dependsOn.length)\n byKey.set(reg.key, reg)\n }\n for (const reg of registrations) {\n for (const dep of reg.dependsOn) {\n const list = dependents.get(dep) ?? []\n list.push(reg)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: registrations with zero indegree, preserving input order.\n const queue: AnyContributorRegistration[] = []\n for (const reg of registrations) {\n if (indegree.get(reg.key) === 0) queue.push(reg)\n }\n\n const sorted: AnyContributorRegistration[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.key) ?? []) {\n const next = (indegree.get(dependent.key) ?? 0) - 1\n indegree.set(dependent.key, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== registrations.length) {\n const remaining = registrations.filter((r) => (indegree.get(r.key) ?? 0) > 0)\n throw new ContributorCycleError(reconstructCycle(remaining, byKey), route)\n }\n\n return sorted\n}\n\n/**\n * Reconstruct one cycle path from the residual graph.\n *\n * Walks from any remaining node, following its first dependency that's\n * still in the residual set, until we revisit a node. The path returned\n * starts and ends with the same key so the message reads naturally:\n * `tenant → project → tenant`.\n */\nfunction reconstructCycle(\n residual: readonly AnyContributorRegistration[],\n byKey: ReadonlyMap<string, AnyContributorRegistration>,\n): string[] {\n const residualKeys = new Set(residual.map((r) => r.key))\n const start = residual[0].key\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor: string = start\n\n while (true) {\n const reg = byKey.get(cursor)!\n const nextDep = reg.dependsOn.find((d) => residualKeys.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n // Trim the prefix that isn't part of the cycle.\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 // Fallback — couldn't reconstruct a strict cycle, return what we walked.\n return path\n}\n","// Concrete-module imports (NOT the `../core` barrel): the barrel re-exports\n// the asset manager, whose eager `node:fs` import would poison the edge-safe\n// `@forinda/kickjs/web` entry graph that flows through this file.\nimport {\n buildPipeline,\n splitBeforeValidation,\n type SourcedRegistration,\n} from '../core/contributor-pipeline'\nimport { runContributors } from '../core/contributor-runner'\nimport { Container } from '../core/container'\nimport { METADATA } from '../core/interfaces'\nimport type { ContributorRegistration } from '../core/context-decorator'\nimport type { FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core/decorators'\nimport { getClassMeta, getMethodMeta, getMethodMetaOrUndefined } from '../core/metadata'\nimport {\n getClassFlagDeclarations,\n getMethodFlagDeclarations,\n registerMountFlags,\n resolveRouteFlags,\n toFlagDeclarations,\n type RouteFlagDeclarations,\n} from '../core/route-flag'\nimport { duplicateRouteError } from '../core/kick-errors'\nimport type { CtxHandler, RouteEntry, RouteMethod } from './runtime'\n\n/**\n * Per-module SourcedRegistration[] threaded through the route-mount loop by\n * Application.setup(). Carries module + adapter + global contributors so\n * buildRouteTable() can merge them with class + method ones into a single pipeline.\n *\n * Module setup is sequential, so this slot is race-free. Cleared in a finally\n * block after each module mounts. Outside of Application setup the slot is\n * empty — direct buildRouteTable()/buildRoutes() callers (mostly tests) see only\n * class + method contributors unless they pass `externalSources` explicitly.\n *\n * Same idiom as `Container._requestStoreProvider`: an internal escape hatch for\n * cross-module wiring without inversion-of-control gymnastics.\n *\n * @internal\n */\nlet _externalContributorSources: readonly SourcedRegistration[] = []\n\n/** @internal — set by Application.setup() before each `mod.routes()` call. */\nexport function _setExternalContributorSources(sources: readonly SourcedRegistration[]): void {\n _externalContributorSources = sources\n}\n\n/**\n * Boot-time duplicate-route guard shared by the node and web mount loops.\n *\n * Registers `METHOD /mounted/path` into `registry` and throws\n * {@link duplicateRouteError} (KICK006) if another handler already claimed\n * it. Param *names* are ignored — `/tasks/:id` and `/tasks/:taskId` are the\n * same route at dispatch time, so they collide here too.\n *\n * @internal\n */\nexport function assertRouteUnique(\n registry: Map<string, string>,\n method: string,\n fullPath: string,\n owner: string,\n): void {\n const key = `${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g, ':*')}`\n const prior = registry.get(key)\n if (prior !== undefined) {\n throw duplicateRouteError(method.toUpperCase(), fullPath, owner, prior)\n }\n registry.set(key, owner)\n}\n\nexport interface BuildRoutesOptions {\n /**\n * Extra contributors to merge into the per-route pipeline at their declared\n * precedence levels. Pass explicitly when calling buildRouteTable/buildRoutes\n * outside the Application route-mount loop (typically in tests). When omitted,\n * falls back to the slot set by Application.setup().\n */\n externalSources?: readonly SourcedRegistration[]\n /**\n * Flags the mounting module declares for every route on this controller\n * (`ModuleRoutes.flags`). Lowest precedence — a class or method declaration\n * of the same name wins, and `@Flag.off` on a method removes it.\n */\n mountFlags?: RouteFlagDeclarations\n}\n\n/**\n * Turn a controller class decorated with @Get, @Post, etc. into a plain-data\n * {@link RouteEntry}[] — the engine-neutral route table an {@link HttpRuntime}\n * materializes.\n *\n * What used to be per-handler Express `(req, res, next)` closures is now\n * captured as data: `middlewares` keep their `(ctx, next)` shape, the contributor\n * pipeline is pre-built into a `contributorRunner` closure, validation / upload\n * stay as metadata, and the terminal `handler` resolves the controller per-request\n * (to respect DI scoping) and invokes it. The runtime owns how these get wrapped\n * onto its engine.\n *\n * Routes use only the method-level decorator paths (e.g. @Get('/me') → '/me').\n * The @Controller path is NOT baked in — it is metadata for Swagger/introspection.\n * The module's routes().path is the single source of truth for the mount prefix,\n * which avoids path doubling when both the module and controller specify the path.\n *\n * The contributor pipeline is built here, so a cycle or missing `dependsOn`\n * throws at table-build time (boot) rather than on first request.\n */\nexport function buildRouteTable(\n controllerClass: any,\n options: BuildRoutesOptions = {},\n): RouteEntry[] {\n const container = Container.getInstance()\n const externalSources = options.externalSources ?? _externalContributorSources\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n // Class-level middleware\n const classMiddlewares: MiddlewareHandler[] = getClassMeta<MiddlewareHandler[]>(\n METADATA.CLASS_MIDDLEWARES,\n controllerClass,\n [],\n )\n\n // Class-level Context Contributors (#107) — applied to every method on the\n // controller. Method-level contributors are added per-route below.\n const classContributors: ContributorRegistration[] = getClassMeta<ContributorRegistration[]>(\n METADATA.CLASS_CONTRIBUTORS,\n controllerClass,\n [],\n )\n\n // Route flags declared on the controller — inherited by every method below\n // unless a method declares the same flag `false` (see core/route-flag.ts).\n const classFlagDeclarations = getClassFlagDeclarations(controllerClass)\n\n // Flags the module declared for this whole mount. Recorded against the class\n // so `getRouteFlags()` — the resolver Swagger and DevTools use, which sees no\n // mount — reports what the runtime actually resolved.\n const mountFlagDeclarations = toFlagDeclarations(\n options.mountFlags,\n `${controllerClass?.name ?? 'controller'} mount flags`,\n )\n registerMountFlags(controllerClass, mountFlagDeclarations)\n\n const entries: RouteEntry[] = []\n\n for (const route of routes) {\n const method = route.method.toUpperCase() as RouteMethod\n const fullPath = route.path || '/'\n\n // Method-level middleware\n const methodMiddlewares: MiddlewareHandler[] = getMethodMeta<MiddlewareHandler[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // @FileUpload decorator — runtime supplies the upload backend.\n const fileUploadConfig = getMethodMetaOrUndefined<FileUploadConfig>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n\n // Context Contributor pipeline (#107) — class + method contributors,\n // built once at mount time so cycle/missing-dep failures abort boot.\n const methodContributors: ContributorRegistration[] = getMethodMeta<ContributorRegistration[]>(\n METADATA.METHOD_CONTRIBUTORS,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // Flags in force for THIS route: class declarations overridden by method\n // ones. Resolved at build time so the per-request cost is a map lookup.\n const flags = resolveRouteFlags(\n classFlagDeclarations,\n getMethodFlagDeclarations(controllerClass, route.handlerName),\n mountFlagDeclarations,\n )\n\n let contributorRunner: CtxHandler | null = null\n let earlyContributorRunner: CtxHandler | null = null\n if (\n classContributors.length > 0 ||\n methodContributors.length > 0 ||\n externalSources.length > 0\n ) {\n // Labels are surfaced in DuplicateContributorError messages — include\n // the registration's key + the original index so a same-key collision\n // at one precedence level points at the conflicting decorator slot,\n // not just the host method/class.\n const sources: SourcedRegistration[] = [\n ...methodContributors.map((registration, i): SourcedRegistration => ({\n source: 'method',\n registration,\n label: `${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`,\n })),\n ...classContributors.map((registration, i): SourcedRegistration => ({\n source: 'class',\n registration,\n label: `${controllerClass.name}.@class#${i}(${registration.key})`,\n })),\n ...externalSources,\n ]\n const routeLabel = `${method} ${fullPath}`\n const { early, late } = splitBeforeValidation(\n buildPipeline(sources, { route: routeLabel }),\n routeLabel,\n )\n if (early) {\n earlyContributorRunner = (ctx) =>\n runContributors({ pipeline: early, ctx, container, flags })\n }\n if (late) {\n contributorRunner = (ctx) => runContributors({ pipeline: late, ctx, container, flags })\n }\n }\n\n // Terminal handler — resolve controller per-request to respect DI scoping.\n const handler: CtxHandler = (ctx) => container.resolve(controllerClass)[route.handlerName](ctx)\n\n entries.push({\n method,\n path: fullPath,\n middlewares: [...classMiddlewares, ...methodMiddlewares],\n contributorRunner,\n earlyContributorRunner,\n handler,\n meta: {\n controller: controllerClass,\n handlerName: route.handlerName,\n validation: route.validation,\n upload: fileUploadConfig,\n flags,\n },\n })\n }\n\n return entries\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"router-builder-DawnRzlp.mjs","names":[],"sources":["../src/core/contributor-pipeline.ts","../src/http/router-builder.ts"],"sourcesContent":["import type { AnyContributorRegistration } from './context-decorator'\nimport {\n ContributorCycleError,\n DuplicateContributorError,\n MissingContributorError,\n} from './context-errors'\n\n/**\n * Where a contributor was registered, ordered by precedence\n * (lower index wins). Mirrors `architecture.md` §20.4.\n *\n * Levels (highest → lowest precedence):\n * - `'method'` — `@LoadX` on a controller method.\n * - `'class'` — `@LoadX` on a controller class.\n * - `'module'` — returned by `AppModule.contributors?()`. Per-module scope.\n * - `'adapter'` — returned by `AppAdapter.contributors?()`. Cross-cutting, applies\n * to every module's routes.\n * - `'global'` — `ApplicationOptions.contributors`. App-wide default.\n */\nexport type ContributorSource = 'method' | 'class' | 'module' | 'adapter' | 'global'\n\n/** Precedence-ranked source list. Index = precedence; method (0) wins ties. */\nconst PRECEDENCE_ORDER: readonly ContributorSource[] = [\n 'method',\n 'class',\n 'module',\n 'adapter',\n 'global',\n]\n\n/**\n * Input row to {@link buildPipeline}. Pairs a registration with its origin\n * so dedup can apply the precedence rule and diagnostic errors can name\n * the offending site.\n *\n * `registration` is the type-erased {@link AnyContributorRegistration}\n * — the runner doesn't care about each registration's narrow `D` /\n * `Ctx` shape, and adopters frequently mix HTTP / transport-agnostic\n * contributors in the same `externalSources` / `contributors()`\n * array. The narrow generics from `defineContextDecorator` survive\n * at the *definition* site (where `resolve` is typed); they're erased\n * here so collections compose without `as any` casts.\n */\nexport interface SourcedRegistration {\n registration: AnyContributorRegistration\n source: ContributorSource\n /** Human label surfaced in DuplicateContributorError messages. */\n label?: string\n}\n\n/**\n * Resolved, validated, topo-sorted pipeline ready for execution.\n *\n * `contributors` is in execution order — every entry's `dependsOn`\n * keys appear earlier. `keys` is the set of all keys this pipeline\n * produces; the runner uses it for fast existence checks and the\n * builder uses it for missing-dep validation.\n */\nexport interface ContributorPipeline {\n readonly contributors: readonly AnyContributorRegistration[]\n readonly keys: ReadonlySet<string>\n}\n\nexport interface BuildPipelineOptions {\n /** Optional route identifier surfaced in diagnostic errors (e.g., 'GET /projects/:id'). */\n route?: string\n}\n\n/**\n * Build, validate, and topo-sort a contributor pipeline.\n *\n * Pipeline:\n *\n * 1. Group sources by precedence (method > class > module > adapter > global).\n * 2. Within each precedence level, two contributors with the same `key`\n * raise {@link DuplicateContributorError}.\n * 3. Across levels, the highest-precedence contributor wins each key\n * (lower-precedence duplicates are silently dropped — that is the\n * documented override mechanism, not an error).\n * 4. Every key referenced by `dependsOn` must be produced by the\n * surviving set, otherwise {@link MissingContributorError}.\n * 5. Kahn's algorithm topo-sorts the survivors. Any nodes left after\n * the queue drains form a cycle → {@link ContributorCycleError}\n * with a reconstructed path.\n *\n * Pure function — no DI, no I/O. Safe to call once at startup per\n * route and cache the result.\n */\nexport function buildPipeline(\n sources: readonly SourcedRegistration[],\n options: BuildPipelineOptions = {},\n): ContributorPipeline {\n const route = options.route\n\n // Step 1: group by source.\n const bySource = new Map<ContributorSource, SourcedRegistration[]>()\n for (const src of PRECEDENCE_ORDER) bySource.set(src, [])\n for (const entry of sources) bySource.get(entry.source)!.push(entry)\n\n // Step 2 + 3: dedup with precedence. We walk highest precedence first,\n // claim its keys, and drop lower-precedence rows that conflict.\n const winners = new Map<string, SourcedRegistration>()\n for (const src of PRECEDENCE_ORDER) {\n const rows = bySource.get(src)!\n const keysAtThisLevel = new Map<string, SourcedRegistration[]>()\n for (const row of rows) {\n const key = row.registration.key\n const existing = keysAtThisLevel.get(key) ?? []\n existing.push(row)\n keysAtThisLevel.set(key, existing)\n }\n for (const [key, rowsForKey] of keysAtThisLevel) {\n if (rowsForKey.length > 1) {\n const labels = rowsForKey.map((r, i) => r.label ?? `${src}#${i}`)\n throw new DuplicateContributorError(key, labels)\n }\n if (!winners.has(key)) winners.set(key, rowsForKey[0])\n }\n }\n\n const survivors = [...winners.values()].map((r) => r.registration)\n const allKeys = new Set(survivors.map((r) => r.key))\n\n // Step 4: dependsOn validation.\n for (const reg of survivors) {\n for (const dep of reg.dependsOn) {\n if (!allKeys.has(dep)) {\n throw new MissingContributorError(dep, reg.key, route, reg.definedAt)\n }\n }\n }\n\n // Step 5: Kahn topo-sort.\n const sorted = topoSort(survivors, route)\n\n return Object.freeze({\n contributors: Object.freeze(sorted),\n keys: allKeys,\n })\n}\n\n/**\n * Split a built pipeline into the contributors that run before request\n * validation (`beforeValidation: true`) and the rest, which keep their place\n * after `@Middleware()`. Both halves stay in topo order.\n *\n * An early contributor can only depend on other early ones — its dependency\n * would not have run yet otherwise — so that fails here, at boot.\n */\nexport function splitBeforeValidation(\n pipeline: ContributorPipeline,\n route?: string,\n): { early: ContributorPipeline | null; late: ContributorPipeline | null } {\n const early = pipeline.contributors.filter((reg) => reg.beforeValidation)\n if (early.length === 0) return { early: null, late: pipeline }\n\n const earlyKeys = new Set(early.map((reg) => reg.key))\n for (const reg of early) {\n const lateDep = reg.dependsOn.find((dep) => !earlyKeys.has(dep))\n if (lateDep !== undefined) {\n throw new Error(\n `Contributor '${reg.key}'${route ? ` on ${route}` : ''} sets beforeValidation but ` +\n `depends on '${lateDep}', which runs after validation. Set beforeValidation on ` +\n `'${lateDep}' too, or remove it from '${reg.key}'.`,\n )\n }\n }\n\n const late = pipeline.contributors.filter((reg) => !reg.beforeValidation)\n return {\n early: Object.freeze({ contributors: Object.freeze(early), keys: earlyKeys }),\n late:\n late.length === 0\n ? null\n : Object.freeze({\n contributors: Object.freeze(late),\n keys: new Set(late.map((reg) => reg.key)),\n }),\n }\n}\n\n/**\n * Kahn's algorithm with deterministic ordering on ties.\n *\n * Nodes with no remaining incoming edges enter the queue in input\n * order. Cycles surface as nodes still present after the queue\n * drains; we reconstruct one cycle path for diagnostics.\n */\nfunction topoSort(\n registrations: readonly AnyContributorRegistration[],\n route: string | undefined,\n): AnyContributorRegistration[] {\n // indegree[key] = number of unresolved dependsOn entries for key\n const indegree = new Map<string, number>()\n // dependents[depKey] = list of registrations that depend on depKey\n const dependents = new Map<string, AnyContributorRegistration[]>()\n const byKey = new Map<string, AnyContributorRegistration>()\n\n for (const reg of registrations) {\n indegree.set(reg.key, reg.dependsOn.length)\n byKey.set(reg.key, reg)\n }\n for (const reg of registrations) {\n for (const dep of reg.dependsOn) {\n const list = dependents.get(dep) ?? []\n list.push(reg)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: registrations with zero indegree, preserving input order.\n const queue: AnyContributorRegistration[] = []\n for (const reg of registrations) {\n if (indegree.get(reg.key) === 0) queue.push(reg)\n }\n\n const sorted: AnyContributorRegistration[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.key) ?? []) {\n const next = (indegree.get(dependent.key) ?? 0) - 1\n indegree.set(dependent.key, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== registrations.length) {\n const remaining = registrations.filter((r) => (indegree.get(r.key) ?? 0) > 0)\n throw new ContributorCycleError(reconstructCycle(remaining, byKey), route)\n }\n\n return sorted\n}\n\n/**\n * Reconstruct one cycle path from the residual graph.\n *\n * Walks from any remaining node, following its first dependency that's\n * still in the residual set, until we revisit a node. The path returned\n * starts and ends with the same key so the message reads naturally:\n * `tenant → project → tenant`.\n */\nfunction reconstructCycle(\n residual: readonly AnyContributorRegistration[],\n byKey: ReadonlyMap<string, AnyContributorRegistration>,\n): string[] {\n const residualKeys = new Set(residual.map((r) => r.key))\n const start = residual[0].key\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor: string = start\n\n while (true) {\n const reg = byKey.get(cursor)!\n const nextDep = reg.dependsOn.find((d) => residualKeys.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n // Trim the prefix that isn't part of the cycle.\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 // Fallback — couldn't reconstruct a strict cycle, return what we walked.\n return path\n}\n","// Concrete-module imports (NOT the `../core` barrel): the barrel re-exports\n// the asset manager, whose eager `node:fs` import would poison the edge-safe\n// `@forinda/kickjs/web` entry graph that flows through this file.\nimport {\n buildPipeline,\n splitBeforeValidation,\n type SourcedRegistration,\n} from '../core/contributor-pipeline'\nimport { runContributors } from '../core/contributor-runner'\nimport { Container } from '../core/container'\nimport { METADATA } from '../core/interfaces'\nimport type { ContributorRegistration } from '../core/context-decorator'\nimport type { FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core/decorators'\nimport { getClassMeta, getMethodMeta, getMethodMetaOrUndefined } from '../core/metadata'\nimport {\n getClassFlagDeclarations,\n getMethodFlagDeclarations,\n registerMountFlags,\n resolveRouteFlags,\n toFlagDeclarations,\n type RouteFlagDeclarations,\n} from '../core/route-flag'\nimport { duplicateRouteError } from '../core/kick-errors'\nimport type { CtxHandler, RouteEntry, RouteMethod } from './runtime'\n\n/**\n * Per-module SourcedRegistration[] threaded through the route-mount loop by\n * Application.setup(). Carries module + adapter + global contributors so\n * buildRouteTable() can merge them with class + method ones into a single pipeline.\n *\n * Module setup is sequential, so this slot is race-free. Cleared in a finally\n * block after each module mounts. Outside of Application setup the slot is\n * empty — direct buildRouteTable()/buildRoutes() callers (mostly tests) see only\n * class + method contributors unless they pass `externalSources` explicitly.\n *\n * Same idiom as `Container._requestStoreProvider`: an internal escape hatch for\n * cross-module wiring without inversion-of-control gymnastics.\n *\n * @internal\n */\nlet _externalContributorSources: readonly SourcedRegistration[] = []\n\n/** @internal — set by Application.setup() before each `mod.routes()` call. */\nexport function _setExternalContributorSources(sources: readonly SourcedRegistration[]): void {\n _externalContributorSources = sources\n}\n\n/**\n * Boot-time duplicate-route guard shared by the node and web mount loops.\n *\n * Registers `METHOD /mounted/path` into `registry` and throws\n * {@link duplicateRouteError} (KICK006) if another handler already claimed\n * it. Param *names* are ignored — `/tasks/:id` and `/tasks/:taskId` are the\n * same route at dispatch time, so they collide here too.\n *\n * @internal\n */\nexport function assertRouteUnique(\n registry: Map<string, string>,\n method: string,\n fullPath: string,\n owner: string,\n): void {\n const key = `${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g, ':*')}`\n const prior = registry.get(key)\n if (prior !== undefined) {\n throw duplicateRouteError(method.toUpperCase(), fullPath, owner, prior)\n }\n registry.set(key, owner)\n}\n\nexport interface BuildRoutesOptions {\n /**\n * Extra contributors to merge into the per-route pipeline at their declared\n * precedence levels. Pass explicitly when calling buildRouteTable/buildRoutes\n * outside the Application route-mount loop (typically in tests). When omitted,\n * falls back to the slot set by Application.setup().\n */\n externalSources?: readonly SourcedRegistration[]\n /**\n * Flags the mounting module declares for every route on this controller\n * (`ModuleRoutes.flags`). Lowest precedence — a class or method declaration\n * of the same name wins, and `@Flag.off` on a method removes it.\n */\n mountFlags?: RouteFlagDeclarations\n}\n\n/**\n * Turn a controller class decorated with @Get, @Post, etc. into a plain-data\n * {@link RouteEntry}[] — the engine-neutral route table an {@link HttpRuntime}\n * materializes.\n *\n * What used to be per-handler Express `(req, res, next)` closures is now\n * captured as data: `middlewares` keep their `(ctx, next)` shape, the contributor\n * pipeline is pre-built into a `contributorRunner` closure, validation / upload\n * stay as metadata, and the terminal `handler` resolves the controller per-request\n * (to respect DI scoping) and invokes it. The runtime owns how these get wrapped\n * onto its engine.\n *\n * Routes use only the method-level decorator paths (e.g. @Get('/me') → '/me').\n * The @Controller path is NOT baked in — it is metadata for Swagger/introspection.\n * The module's routes().path is the single source of truth for the mount prefix,\n * which avoids path doubling when both the module and controller specify the path.\n *\n * The contributor pipeline is built here, so a cycle or missing `dependsOn`\n * throws at table-build time (boot) rather than on first request.\n */\nexport function buildRouteTable(\n controllerClass: any,\n options: BuildRoutesOptions = {},\n): RouteEntry[] {\n const container = Container.getInstance()\n const externalSources = options.externalSources ?? _externalContributorSources\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n // Class-level middleware\n const classMiddlewares: MiddlewareHandler[] = getClassMeta<MiddlewareHandler[]>(\n METADATA.CLASS_MIDDLEWARES,\n controllerClass,\n [],\n )\n\n // Class-level Context Contributors (#107) — applied to every method on the\n // controller. Method-level contributors are added per-route below.\n const classContributors: ContributorRegistration[] = getClassMeta<ContributorRegistration[]>(\n METADATA.CLASS_CONTRIBUTORS,\n controllerClass,\n [],\n )\n\n // Route flags declared on the controller — inherited by every method below\n // unless a method declares the same flag `false` (see core/route-flag.ts).\n const classFlagDeclarations = getClassFlagDeclarations(controllerClass)\n\n // Flags the module declared for this whole mount. Recorded against the class\n // so `getRouteFlags()` — the resolver Swagger and DevTools use, which sees no\n // mount — reports what the runtime actually resolved.\n const mountFlagDeclarations = toFlagDeclarations(\n options.mountFlags,\n `${controllerClass?.name ?? 'controller'} mount flags`,\n )\n registerMountFlags(controllerClass, mountFlagDeclarations)\n\n const entries: RouteEntry[] = []\n\n for (const route of routes) {\n const method = route.method.toUpperCase() as RouteMethod\n const fullPath = route.path || '/'\n\n // Method-level middleware\n const methodMiddlewares: MiddlewareHandler[] = getMethodMeta<MiddlewareHandler[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // @FileUpload decorator — runtime supplies the upload backend.\n const fileUploadConfig = getMethodMetaOrUndefined<FileUploadConfig>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n\n // Context Contributor pipeline (#107) — class + method contributors,\n // built once at mount time so cycle/missing-dep failures abort boot.\n const methodContributors: ContributorRegistration[] = getMethodMeta<ContributorRegistration[]>(\n METADATA.METHOD_CONTRIBUTORS,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // Flags in force for THIS route: class declarations overridden by method\n // ones. Resolved at build time so the per-request cost is a map lookup.\n const flags = resolveRouteFlags(\n classFlagDeclarations,\n getMethodFlagDeclarations(controllerClass, route.handlerName),\n mountFlagDeclarations,\n )\n\n let contributorRunner: CtxHandler | null = null\n let earlyContributorRunner: CtxHandler | null = null\n if (\n classContributors.length > 0 ||\n methodContributors.length > 0 ||\n externalSources.length > 0\n ) {\n // Labels are surfaced in DuplicateContributorError messages — include\n // the registration's key + the original index so a same-key collision\n // at one precedence level points at the conflicting decorator slot,\n // not just the host method/class.\n const sources: SourcedRegistration[] = [\n ...methodContributors.map((registration, i): SourcedRegistration => ({\n source: 'method',\n registration,\n label: `${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`,\n })),\n ...classContributors.map((registration, i): SourcedRegistration => ({\n source: 'class',\n registration,\n label: `${controllerClass.name}.@class#${i}(${registration.key})`,\n })),\n ...externalSources,\n ]\n const routeLabel = `${method} ${fullPath}`\n const { early, late } = splitBeforeValidation(\n buildPipeline(sources, { route: routeLabel }),\n routeLabel,\n )\n if (early) {\n earlyContributorRunner = (ctx) =>\n runContributors({ pipeline: early, ctx, container, flags })\n }\n if (late) {\n contributorRunner = (ctx) => runContributors({ pipeline: late, ctx, container, flags })\n }\n }\n\n // Terminal handler — resolve controller per-request to respect DI scoping.\n const handler: CtxHandler = (ctx) => container.resolve(controllerClass)[route.handlerName](ctx)\n\n entries.push({\n method,\n path: fullPath,\n middlewares: [...classMiddlewares, ...methodMiddlewares],\n contributorRunner,\n earlyContributorRunner,\n handler,\n meta: {\n controller: controllerClass,\n handlerName: route.handlerName,\n validation: route.validation,\n upload: fileUploadConfig,\n flags,\n },\n })\n }\n\n return entries\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
 
2
- import { g as FileUploadConfig, o as BaseUploadOptions } from "./decorators-DOcWsXci.mjs";
3
- import "./adapter-DhjoIYVI.mjs";
2
+ import { g as FileUploadConfig, o as BaseUploadOptions } from "./decorators-BxNSz9Qt.mjs";
3
+ import "./adapter-DjfRMob8.mjs";
4
4
  import { NextFunction, Request, RequestHandler, Response } from "express";
5
5
  import { Options } from "multer";
6
6
  //#region src/http/middleware/upload-config.d.ts
@@ -101,4 +101,4 @@ declare const upload: {
101
101
  };
102
102
  //#endregion
103
103
  export { RawUploadPart as a, buildFileTypeFilter as c, upload as i, resolveMimeTypes as l, buildUploadMiddleware as n, UploadedFileLike as o, cleanupFiles as r, applyUploadConfig as s, UploadOptions as t };
104
- //# sourceMappingURL=upload-DjhnyHHm.d.mts.map
104
+ //# sourceMappingURL=upload-Cy7qQy7M.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload-DjhnyHHm.d.mts","names":[],"sources":["../src/http/middleware/upload-config.ts","../src/http/middleware/upload.ts"],"mappings":";;;;;;;;;;;;;;;;iBAuEgB,iBAAiB;;;;;;;;iBAcjB,oBACd,cAAc,mCACd,gBAAgB,0BACd,kBAAkB;;UAYL;EACf;EACA;EACA;EACA;EACA;EACA,QAAQ;;;UAIO;EACf;EACA;EACA;EACA,QAAQ;;;;;;;;;iBAYM,kBACd,OAAO,iBACP,QAAQ;EACL,OAAO;EAAkB,QAAQ;;;;;;;;;UC/ErB,sBAAsB;;EAErC,UAAU;;EAEV;;iBAwGO,OAAO,mBAAmB,UAAS,gBAAqB;;;;iBAQxD,MAAM,mBAAmB,mBAAe,UAAS,gBAAqB;;;;iBAUtE,KAAK,UAAS,gBAAqB;;;;;;;;;;;;;;;iBAmB5B,iBAAY,KACb,SAAO,KAAO,UAAQ,MAAQ;;;;;;iBA+B7B,sBAAsB,QAAQ,mBAAmB;;cAmBpD"}
1
+ {"version":3,"file":"upload-Cy7qQy7M.d.mts","names":[],"sources":["../src/http/middleware/upload-config.ts","../src/http/middleware/upload.ts"],"mappings":";;;;;;;;;;;;;;;;iBAuEgB,iBAAiB;;;;;;;;iBAcjB,oBACd,cAAc,mCACd,gBAAgB,0BACd,kBAAkB;;UAYL;EACf;EACA;EACA;EACA;EACA;EACA,QAAQ;;;UAIO;EACf;EACA;EACA;EACA,QAAQ;;;;;;;;;iBAYM,kBACd,OAAO,iBACP,QAAQ;EACL,OAAO;EAAkB,QAAQ;;;;;;;;;UC/ErB,sBAAsB;;EAErC,UAAU;;EAEV;;iBAwGO,OAAO,mBAAmB,UAAS,gBAAqB;;;;iBAQxD,MAAM,mBAAmB,mBAAe,UAAS,gBAAqB;;;;iBAUtE,KAAK,UAAS,gBAAqB;;;;;;;;;;;;;;;iBAmB5B,iBAAY,KACb,SAAO,KAAO,UAAQ,MAAQ;;;;;;iBA+B7B,sBAAsB,QAAQ,mBAAmB;;cAmBpD"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v9.1.0
2
+ * @forinda/kickjs v9.2.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -9,4 +9,4 @@
9
9
  * @license MIT
10
10
  */
11
11
  import{HttpException,HttpStatus}from"./core/errors.mjs";const MIME_MAP={jpg:`image/jpeg`,jpeg:`image/jpeg`,png:`image/png`,gif:`image/gif`,webp:`image/webp`,svg:`image/svg+xml`,bmp:`image/bmp`,ico:`image/x-icon`,tiff:`image/tiff`,tif:`image/tiff`,avif:`image/avif`,pdf:`application/pdf`,doc:`application/msword`,docx:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,xls:`application/vnd.ms-excel`,xlsx:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,ppt:`application/vnd.ms-powerpoint`,pptx:`application/vnd.openxmlformats-officedocument.presentationml.presentation`,odt:`application/vnd.oasis.opendocument.text`,ods:`application/vnd.oasis.opendocument.spreadsheet`,odp:`application/vnd.oasis.opendocument.presentation`,rtf:`application/rtf`,txt:`text/plain`,csv:`text/csv`,zip:`application/zip`,gz:`application/gzip`,tar:`application/x-tar`,rar:`application/vnd.rar`,"7z":`application/x-7z-compressed`,mp3:`audio/mpeg`,wav:`audio/wav`,ogg:`audio/ogg`,flac:`audio/flac`,aac:`audio/aac`,mp4:`video/mp4`,webm:`video/webm`,avi:`video/x-msvideo`,mov:`video/quicktime`,mkv:`video/x-matroska`,json:`application/json`,xml:`application/xml`,html:`text/html`};function resolveMimeTypes(types){return types.map(t=>{let lower=t.toLowerCase().replace(/^\./,``);return MIME_MAP[lower]??t})}function buildFileTypeFilter(allowedTypes,customMimeMap){if(!allowedTypes)return()=>!0;if(typeof allowedTypes==`function`)return allowedTypes;let mimeMap=customMimeMap?{...MIME_MAP,...customMimeMap}:MIME_MAP,resolved=allowedTypes.map(t=>mimeMap[t.toLowerCase().replace(/^\./,``)]??t);return mimetype=>resolved.some(type=>type.endsWith(`/*`)?mimetype.startsWith(type.replace(`/*`,`/`)):mimetype===type)}function applyUploadConfig(parts,config){if(config.mode===`none`)return{};let field=config.fieldName??`file`,maxSize=config.maxSize??5242880,typeAllowed=buildFileTypeFilter(config.allowedTypes,config.customMimeMap),files=parts.filter(p=>p.fieldname===field).map(p=>{if(!typeAllowed(p.mimetype,p.filename))throw new HttpException(HttpStatus.UNSUPPORTED_MEDIA_TYPE,`File type ${p.mimetype} is not allowed`);if(p.buffer.length>maxSize)throw new HttpException(HttpStatus.PAYLOAD_TOO_LARGE,`File ${p.filename} exceeds the ${maxSize}-byte limit`);return{fieldname:p.fieldname,originalname:p.filename,encoding:`7bit`,mimetype:p.mimetype,size:p.buffer.length,buffer:p.buffer}});if(config.mode===`single`)return{file:files[0]};let maxCount=config.maxCount??10;return{files:files.slice(0,maxCount)}}export{buildFileTypeFilter as n,resolveMimeTypes as r,applyUploadConfig as t};
12
- //# sourceMappingURL=upload-config-d0pisVsK.mjs.map
12
+ //# sourceMappingURL=upload-config-D01Ty40f.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload-config-d0pisVsK.mjs","names":[],"sources":["../src/http/middleware/upload-config.ts"],"sourcesContent":["// Pure (edge-safe) upload core — the engine-neutral half of the upload\n// middleware. Extracted from `upload.ts` so the `@forinda/kickjs/web` entry\n// graph can validate/shape buffered multipart parts WITHOUT dragging in\n// upload.ts's node-only surface (multer peer loading via `node:module`,\n// disk cleanup via `node:fs/promises`). `upload.ts` re-exports everything\n// here, so existing import sites are unchanged.\n\nimport { HttpException, HttpStatus } from '../../core/errors'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core/decorators'\n\nexport const MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * Build a runtime-agnostic `(mimetype, filename) => boolean` file-type filter\n * from `allowedTypes` (a predicate, or a list of extensions / MIME types /\n * wildcards). Returned by both the multer middleware (Express) and the\n * Fastify / h3 / web multipart paths so the accept/reject rule stays\n * identical across engines. No `allowedTypes` → accept everything.\n */\nexport function buildFileTypeFilter(\n allowedTypes: BaseUploadOptions['allowedTypes'],\n customMimeMap?: Record<string, string>,\n): (mimetype: string, filename: string) => boolean {\n if (!allowedTypes) return () => true\n if (typeof allowedTypes === 'function') return allowedTypes\n const mimeMap = customMimeMap ? { ...MIME_MAP, ...customMimeMap } : MIME_MAP\n const resolved = allowedTypes.map((t) => mimeMap[t.toLowerCase().replace(/^\\./, '')] ?? t)\n return (mimetype: string) =>\n resolved.some((type) =>\n type.endsWith('/*') ? mimetype.startsWith(type.replace('/*', '/')) : mimetype === type,\n )\n}\n\n/** A multer-shaped uploaded file — what `ctx.file` / `ctx.files` expose. */\nexport interface UploadedFileLike {\n fieldname: string\n originalname: string\n encoding: string\n mimetype: string\n size: number\n buffer: Buffer\n}\n\n/** A buffered multipart file part, as the runtime hands it to {@link applyUploadConfig}. */\nexport interface RawUploadPart {\n fieldname: string\n filename: string\n mimetype: string\n buffer: Buffer\n}\n\nconst DEFAULT_MAX_SIZE = 5 * 1024 * 1024\n\n/**\n * Apply a `@FileUpload` config to already-buffered multipart parts and return\n * the multer-shaped `file` / `files` for `ctx`. Enforces the field name, type\n * filter, per-file size limit, and (array mode) max count — throwing\n * `HttpException` on a violation, mirroring the Express/multer error path.\n * Used by the Fastify, h3 and web runtimes; Express keeps using multer directly.\n */\nexport function applyUploadConfig(\n parts: RawUploadPart[],\n config: FileUploadConfig,\n): { file?: UploadedFileLike; files?: UploadedFileLike[] } {\n if (config.mode === 'none') return {}\n\n const field = config.fieldName ?? 'file'\n const maxSize = config.maxSize ?? DEFAULT_MAX_SIZE\n const typeAllowed = buildFileTypeFilter(config.allowedTypes, config.customMimeMap)\n\n const matched = parts.filter((p) => p.fieldname === field)\n const files: UploadedFileLike[] = matched.map((p) => {\n if (!typeAllowed(p.mimetype, p.filename)) {\n throw new HttpException(\n HttpStatus.UNSUPPORTED_MEDIA_TYPE,\n `File type ${p.mimetype} is not allowed`,\n )\n }\n if (p.buffer.length > maxSize) {\n throw new HttpException(\n HttpStatus.PAYLOAD_TOO_LARGE,\n `File ${p.filename} exceeds the ${maxSize}-byte limit`,\n )\n }\n return {\n fieldname: p.fieldname,\n originalname: p.filename,\n encoding: '7bit',\n mimetype: p.mimetype,\n size: p.buffer.length,\n buffer: p.buffer,\n }\n })\n\n if (config.mode === 'single') return { file: files[0] }\n const maxCount = config.maxCount ?? 10\n return { files: files.slice(0, maxCount) }\n}\n"],"mappings":";;;;;;;;;;wDAUA,MAAa,SAAmC,CAE9C,IAAK,aACL,KAAM,aACN,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,gBACL,IAAK,YACL,IAAK,eACL,KAAM,aACN,IAAK,aACL,KAAM,aAEN,IAAK,kBACL,IAAK,qBACL,KAAM,0EACN,IAAK,2BACL,KAAM,oEACN,IAAK,gCACL,KAAM,4EACN,IAAK,0CACL,IAAK,iDACL,IAAK,kDACL,IAAK,kBACL,IAAK,aACL,IAAK,WAEL,IAAK,kBACL,GAAI,mBACJ,IAAK,oBACL,IAAK,sBACL,KAAM,8BAEN,IAAK,aACL,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,YAEL,IAAK,YACL,KAAM,aACN,IAAK,kBACL,IAAK,kBACL,IAAK,mBAEL,KAAM,mBACN,IAAK,kBACL,KAAM,WACR,EAYA,SAAgB,iBAAiB,MAA2B,CAC1D,OAAO,MAAM,IAAK,GAAM,CACtB,IAAM,MAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,EAC/C,OAAO,SAAS,QAAU,CAC5B,CAAC,CACH,CASA,SAAgB,oBACd,aACA,cACiD,CACjD,GAAI,CAAC,aAAc,UAAa,GAChC,GAAI,OAAO,cAAiB,WAAY,OAAO,aAC/C,IAAM,QAAU,cAAgB,CAAE,GAAG,SAAU,GAAG,aAAc,EAAI,SAC9D,SAAW,aAAa,IAAK,GAAM,QAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,IAAM,CAAC,EACzF,MAAQ,WACN,SAAS,KAAM,MACb,KAAK,SAAS,IAAI,EAAI,SAAS,WAAW,KAAK,QAAQ,KAAM,GAAG,CAAC,EAAI,WAAa,IACpF,CACJ,CA6BA,SAAgB,kBACd,MACA,OACyD,CACzD,GAAI,OAAO,OAAS,OAAQ,MAAO,CAAC,EAEpC,IAAM,MAAQ,OAAO,WAAa,OAC5B,QAAU,OAAO,SAAW,QAC5B,YAAc,oBAAoB,OAAO,aAAc,OAAO,aAAa,EAG3E,MADU,MAAM,OAAQ,GAAM,EAAE,YAAc,KACZ,CAAC,CAAC,IAAK,GAAM,CACnD,GAAI,CAAC,YAAY,EAAE,SAAU,EAAE,QAAQ,EACrC,MAAM,IAAI,cACR,WAAW,uBACX,aAAa,EAAE,SAAS,gBAC1B,EAEF,GAAI,EAAE,OAAO,OAAS,QACpB,MAAM,IAAI,cACR,WAAW,kBACX,QAAQ,EAAE,SAAS,eAAe,QAAQ,YAC5C,EAEF,MAAO,CACL,UAAW,EAAE,UACb,aAAc,EAAE,SAChB,SAAU,OACV,SAAU,EAAE,SACZ,KAAM,EAAE,OAAO,OACf,OAAQ,EAAE,MACZ,CACF,CAAC,EAED,GAAI,OAAO,OAAS,SAAU,MAAO,CAAE,KAAM,MAAM,EAAG,EACtD,IAAM,SAAW,OAAO,UAAY,GACpC,MAAO,CAAE,MAAO,MAAM,MAAM,EAAG,QAAQ,CAAE,CAC3C"}
1
+ {"version":3,"file":"upload-config-D01Ty40f.mjs","names":[],"sources":["../src/http/middleware/upload-config.ts"],"sourcesContent":["// Pure (edge-safe) upload core — the engine-neutral half of the upload\n// middleware. Extracted from `upload.ts` so the `@forinda/kickjs/web` entry\n// graph can validate/shape buffered multipart parts WITHOUT dragging in\n// upload.ts's node-only surface (multer peer loading via `node:module`,\n// disk cleanup via `node:fs/promises`). `upload.ts` re-exports everything\n// here, so existing import sites are unchanged.\n\nimport { HttpException, HttpStatus } from '../../core/errors'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core/decorators'\n\nexport const MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * Build a runtime-agnostic `(mimetype, filename) => boolean` file-type filter\n * from `allowedTypes` (a predicate, or a list of extensions / MIME types /\n * wildcards). Returned by both the multer middleware (Express) and the\n * Fastify / h3 / web multipart paths so the accept/reject rule stays\n * identical across engines. No `allowedTypes` → accept everything.\n */\nexport function buildFileTypeFilter(\n allowedTypes: BaseUploadOptions['allowedTypes'],\n customMimeMap?: Record<string, string>,\n): (mimetype: string, filename: string) => boolean {\n if (!allowedTypes) return () => true\n if (typeof allowedTypes === 'function') return allowedTypes\n const mimeMap = customMimeMap ? { ...MIME_MAP, ...customMimeMap } : MIME_MAP\n const resolved = allowedTypes.map((t) => mimeMap[t.toLowerCase().replace(/^\\./, '')] ?? t)\n return (mimetype: string) =>\n resolved.some((type) =>\n type.endsWith('/*') ? mimetype.startsWith(type.replace('/*', '/')) : mimetype === type,\n )\n}\n\n/** A multer-shaped uploaded file — what `ctx.file` / `ctx.files` expose. */\nexport interface UploadedFileLike {\n fieldname: string\n originalname: string\n encoding: string\n mimetype: string\n size: number\n buffer: Buffer\n}\n\n/** A buffered multipart file part, as the runtime hands it to {@link applyUploadConfig}. */\nexport interface RawUploadPart {\n fieldname: string\n filename: string\n mimetype: string\n buffer: Buffer\n}\n\nconst DEFAULT_MAX_SIZE = 5 * 1024 * 1024\n\n/**\n * Apply a `@FileUpload` config to already-buffered multipart parts and return\n * the multer-shaped `file` / `files` for `ctx`. Enforces the field name, type\n * filter, per-file size limit, and (array mode) max count — throwing\n * `HttpException` on a violation, mirroring the Express/multer error path.\n * Used by the Fastify, h3 and web runtimes; Express keeps using multer directly.\n */\nexport function applyUploadConfig(\n parts: RawUploadPart[],\n config: FileUploadConfig,\n): { file?: UploadedFileLike; files?: UploadedFileLike[] } {\n if (config.mode === 'none') return {}\n\n const field = config.fieldName ?? 'file'\n const maxSize = config.maxSize ?? DEFAULT_MAX_SIZE\n const typeAllowed = buildFileTypeFilter(config.allowedTypes, config.customMimeMap)\n\n const matched = parts.filter((p) => p.fieldname === field)\n const files: UploadedFileLike[] = matched.map((p) => {\n if (!typeAllowed(p.mimetype, p.filename)) {\n throw new HttpException(\n HttpStatus.UNSUPPORTED_MEDIA_TYPE,\n `File type ${p.mimetype} is not allowed`,\n )\n }\n if (p.buffer.length > maxSize) {\n throw new HttpException(\n HttpStatus.PAYLOAD_TOO_LARGE,\n `File ${p.filename} exceeds the ${maxSize}-byte limit`,\n )\n }\n return {\n fieldname: p.fieldname,\n originalname: p.filename,\n encoding: '7bit',\n mimetype: p.mimetype,\n size: p.buffer.length,\n buffer: p.buffer,\n }\n })\n\n if (config.mode === 'single') return { file: files[0] }\n const maxCount = config.maxCount ?? 10\n return { files: files.slice(0, maxCount) }\n}\n"],"mappings":";;;;;;;;;;wDAUA,MAAa,SAAmC,CAE9C,IAAK,aACL,KAAM,aACN,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,gBACL,IAAK,YACL,IAAK,eACL,KAAM,aACN,IAAK,aACL,KAAM,aAEN,IAAK,kBACL,IAAK,qBACL,KAAM,0EACN,IAAK,2BACL,KAAM,oEACN,IAAK,gCACL,KAAM,4EACN,IAAK,0CACL,IAAK,iDACL,IAAK,kDACL,IAAK,kBACL,IAAK,aACL,IAAK,WAEL,IAAK,kBACL,GAAI,mBACJ,IAAK,oBACL,IAAK,sBACL,KAAM,8BAEN,IAAK,aACL,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,YAEL,IAAK,YACL,KAAM,aACN,IAAK,kBACL,IAAK,kBACL,IAAK,mBAEL,KAAM,mBACN,IAAK,kBACL,KAAM,WACR,EAYA,SAAgB,iBAAiB,MAA2B,CAC1D,OAAO,MAAM,IAAK,GAAM,CACtB,IAAM,MAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,EAC/C,OAAO,SAAS,QAAU,CAC5B,CAAC,CACH,CASA,SAAgB,oBACd,aACA,cACiD,CACjD,GAAI,CAAC,aAAc,UAAa,GAChC,GAAI,OAAO,cAAiB,WAAY,OAAO,aAC/C,IAAM,QAAU,cAAgB,CAAE,GAAG,SAAU,GAAG,aAAc,EAAI,SAC9D,SAAW,aAAa,IAAK,GAAM,QAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,IAAM,CAAC,EACzF,MAAQ,WACN,SAAS,KAAM,MACb,KAAK,SAAS,IAAI,EAAI,SAAS,WAAW,KAAK,QAAQ,KAAM,GAAG,CAAC,EAAI,WAAa,IACpF,CACJ,CA6BA,SAAgB,kBACd,MACA,OACyD,CACzD,GAAI,OAAO,OAAS,OAAQ,MAAO,CAAC,EAEpC,IAAM,MAAQ,OAAO,WAAa,OAC5B,QAAU,OAAO,SAAW,QAC5B,YAAc,oBAAoB,OAAO,aAAc,OAAO,aAAa,EAG3E,MADU,MAAM,OAAQ,GAAM,EAAE,YAAc,KACZ,CAAC,CAAC,IAAK,GAAM,CACnD,GAAI,CAAC,YAAY,EAAE,SAAU,EAAE,QAAQ,EACrC,MAAM,IAAI,cACR,WAAW,uBACX,aAAa,EAAE,SAAS,gBAC1B,EAEF,GAAI,EAAE,OAAO,OAAS,QACpB,MAAM,IAAI,cACR,WAAW,kBACX,QAAQ,EAAE,SAAS,eAAe,QAAQ,YAC5C,EAEF,MAAO,CACL,UAAW,EAAE,UACb,aAAc,EAAE,SAChB,SAAU,OACV,SAAU,EAAE,SACZ,KAAM,EAAE,OAAO,OACf,OAAQ,EAAE,MACZ,CACF,CAAC,EAED,GAAI,OAAO,OAAS,SAAU,MAAO,CAAE,KAAM,MAAM,EAAG,EACtD,IAAM,SAAW,OAAO,UAAY,GACpC,MAAO,CAAE,MAAO,MAAM,MAAM,EAAG,QAAQ,CAAE,CAC3C"}
package/dist/web.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
 
2
- import { x as MiddlewareHandler } from "./decorators-DOcWsXci.mjs";
3
- import { S as RuntimeResponse, g as RouteEntry, k as SseHandler } from "./adapter-DhjoIYVI.mjs";
2
+ import { x as MiddlewareHandler } from "./decorators-BxNSz9Qt.mjs";
3
+ import { S as RuntimeResponse, g as RouteEntry, k as SseHandler } from "./adapter-DjfRMob8.mjs";
4
4
  import { m as ContributorRegistrations, r as AppModuleEntry } from "./app-module-DCA65GZZ.mjs";
5
5
  import { f as runCronJobs, o as CronRun } from "./cron-BTGytNg-.mjs";
6
- import { A as runJob, C as JobLike, a as KvSessionStoreOptions, b as JobContextCarrier, c as InferHandlerResponse, d as isReply, f as reply, g as JOB_CONTEXT_FIELD, h as waitUntil, i as KvSessionStore, j as stampJobContext, k as registerJobContext, l as Reply, n as KvRateLimitStore, o as RateLimitGuardOptions, p as PlatformContext, r as KvRateLimitStoreOptions, s as rateLimitGuard, t as KvLike } from "./kv-stores-CetFF9N_.mjs";
6
+ import { A as runJob, C as JobLike, a as KvSessionStoreOptions, b as JobContextCarrier, c as InferHandlerResponse, d as isReply, f as reply, g as JOB_CONTEXT_FIELD, h as waitUntil, i as KvSessionStore, j as stampJobContext, k as registerJobContext, l as Reply, n as KvRateLimitStore, o as RateLimitGuardOptions, p as PlatformContext, r as KvRateLimitStoreOptions, s as rateLimitGuard, t as KvLike } from "./kv-stores-BZTe23Tw.mjs";
7
7
  //#region src/http/web/driver.d.ts
8
8
  /**
9
9
  * Presents a web `Request` as the express-shaped `req` object that
package/dist/web.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v9.1.0
2
+ * @forinda/kickjs v9.2.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-ZnfelY_a.mjs";import{t as MutableModuleRegistry}from"./module-registry-kqozOSo0.mjs";import{runCronJobs}from"./core/cron.mjs";import{d as stampJobContext,l as registerJobContext,t as JOB_CONTEXT_FIELD,u as runJob}from"./jobs-dx-MTUUb.mjs";import{buildMountPath}from"./core/path.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-CzVjPs5y.mjs";import{i as reply,r as isReply}from"./runtime-CDz5ayBt.mjs";import{l as requestStore,o as waitUntil,r as forwardBackgroundWork}from"./context-BGPof8bs.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-Ba5_rcz-.mjs";import{n as WebRequestShim,r as WebResponseDriver,t as compileWebRoute}from"./handler-2OebsoPr.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.middlewares??[],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`,moduleSources=(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel}));_setExternalContributorSources([...moduleSources,...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 version=route.version??defaultVersion,mountPath=buildMountPath(apiPrefix,version,route.path);for(let entry of buildRouteTable(route.controller,{mountFlags:route.flags})){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:async(request,platform)=>{let response=await app.fetch(request);return forwardBackgroundWork(platform),response},h3:app}}function createFetchHandler(build){let app;return{fetch:(request,env={},ctx)=>(app??=createWebApp(build(env)),app.fetch(request,ctx)),scheduled:async(event,env={},ctx)=>{app??=createWebApp(build(env));let run=runCronJobs(Container.getInstance(),{expression:event.cron},`workers`).then(()=>{});ctx?.waitUntil?.(run),await run},queue:async(batch,env={})=>{app??=createWebApp(build(env));let container=Container.getInstance();await Promise.all(batch.messages.map(async message=>{let body=message.body,named=typeof body==`object`&&!!body&&typeof body.name==`string`;try{await runJob(container,batch.queue,{name:named?body.name:``,data:named?body.data:message.body,id:message.id}),message.ack()}catch{message.retry()}}))}}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{JOB_CONTEXT_FIELD,KvRateLimitStore,KvSessionStore,WebRequestShim,WebResponseDriver,compileWebRoute,createFetchHandler,createWebApp,isReply,rateLimitGuard,registerJobContext,reply,runCronJobs,runJob,stampJobContext,waitUntil};
11
+ import{t as Container}from"./container-CAPN7pBG.mjs";import{t as MutableModuleRegistry}from"./module-registry-BnmRNy-3.mjs";import{runCronJobs}from"./core/cron.mjs";import{d as stampJobContext,l as registerJobContext,t as JOB_CONTEXT_FIELD,u as runJob}from"./jobs-BmpeFvi0.mjs";import{buildMountPath}from"./core/path.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-DawnRzlp.mjs";import{n as isReply,r as reply}from"./reply-CbTYIj_e.mjs";import{a as forwardBackgroundWork,c as waitUntil,d as requestStore}from"./context-gpvcMlG5.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-DJwxntyw.mjs";import{n as WebRequestShim,r as WebResponseDriver,t as compileWebRoute}from"./handler-DnZbcleS.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.middlewares??[],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`,moduleSources=(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel}));_setExternalContributorSources([...moduleSources,...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 version=route.version??defaultVersion,mountPath=buildMountPath(apiPrefix,version,route.path);for(let entry of buildRouteTable(route.controller,{mountFlags:route.flags})){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:async(request,platform)=>{let response=await app.fetch(request);return forwardBackgroundWork(platform),response},h3:app}}function createFetchHandler(build){let app;return{fetch:(request,env={},ctx)=>(app??=createWebApp(build(env)),app.fetch(request,ctx)),scheduled:async(event,env={},ctx)=>{app??=createWebApp(build(env));let run=runCronJobs(Container.getInstance(),{expression:event.cron},`workers`).then(()=>{});ctx?.waitUntil?.(run),await run},queue:async(batch,env={})=>{app??=createWebApp(build(env));let container=Container.getInstance();await Promise.all(batch.messages.map(async message=>{let body=message.body,named=typeof body==`object`&&!!body&&typeof body.name==`string`;try{await runJob(container,batch.queue,{name:named?body.name:``,data:named?body.data:message.body,id:message.id}),message.ack()}catch{message.retry()}}))}}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{JOB_CONTEXT_FIELD,KvRateLimitStore,KvSessionStore,WebRequestShim,WebResponseDriver,compileWebRoute,createFetchHandler,createWebApp,isReply,rateLimitGuard,registerJobContext,reply,runCronJobs,runJob,stampJobContext,waitUntil};
12
12
  //# sourceMappingURL=web.mjs.map
package/dist/web.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"web.mjs","names":[],"sources":["../src/web.ts"],"sourcesContent":["// `@forinda/kickjs/web` — the web-standard fetch entry for edge runtimes\n// (Cloudflare Workers), Bun, and Deno.\n//\n// PURITY CONTRACT: this entry's import graph must stay free of node-only\n// runtime imports — no `node:http`, `node:cluster`, `node:fs`, `node:module`,\n// no `express`, no `Application`/`bootstrap`. The single sanctioned node API\n// is `node:async_hooks` (AsyncLocalStorage — available on Workers with\n// `nodejs_compat`, Deno, and Bun). Enforced by the bundle-graph purity test.\n//\n// ```ts\n// // Cloudflare Workers (wrangler.toml: compatibility_flags = [\"nodejs_compat\"])\n// import { createWebApp } from '@forinda/kickjs/web'\n// import * as h3 from 'h3'\n// import { modules } from './modules'\n//\n// const app = createWebApp({ h3, modules })\n// export default { fetch: (req: Request) => app.fetch(req) }\n//\n// // Bun\n// Bun.serve({ fetch: (req) => app.fetch(req) })\n// ```\n\nimport { Container } from './core/container'\nimport { MutableModuleRegistry } from './core/module-registry'\nimport type { AppModule, AppModuleClass, AppModuleEntry } from './core/app-module'\nimport type { ContributorRegistrations } from './core/context-decorator'\nimport type { MiddlewareHandler } from './core/decorators'\nimport type { SourcedRegistration } from './core/contributor-pipeline'\nimport {\n _setExternalContributorSources,\n assertRouteUnique,\n buildRouteTable,\n} from './http/router-builder'\nimport { requestStore } from './http/request-store'\nimport { forwardBackgroundWork, type PlatformContext } from './http/background'\nimport { runCronJobs } from './core/cron'\nimport { runJob } from './core/jobs'\nimport { compileWebRoute } from './http/web/handler'\nimport { buildMountPath } from './core/path'\n\nexport { WebRequestShim, WebResponseDriver } from './http/web/driver'\nexport { compileWebRoute } from './http/web/handler'\n// Return-value handler helpers — edge consumers can't import the main barrel.\nexport { reply, isReply, type Reply, type InferHandlerResponse } from './http/reply'\nexport type { SseHandler } from './http/context'\nexport { waitUntil, type PlatformContext } from './http/background'\nexport { runCronJobs, type CronRun } from './core/cron'\nexport {\n runJob,\n registerJobContext,\n stampJobContext,\n JOB_CONTEXT_FIELD,\n type JobLike,\n type JobContextCarrier,\n} from './core/jobs'\n// Edge-safe stores + ctx-style rate limiter (zero runtime imports).\nexport {\n KvRateLimitStore,\n KvSessionStore,\n type KvLike,\n type KvRateLimitStoreOptions,\n type KvSessionStoreOptions,\n} from './http/middleware/kv-stores'\nexport { rateLimitGuard, type RateLimitGuardOptions } from './http/middleware/rate-limit-guard'\n\n// Minimal structural surface of h3 v2 used here (kept local so this entry\n// never imports the node-coupled h3-web runtime module).\ninterface H3AppLike {\n on(method: string, path: string, handler: (event: H3EventLike) => unknown): unknown\n all(path: string, handler: (event: H3EventLike) => unknown): unknown\n fetch(request: Request): Promise<Response>\n}\ninterface H3EventLike {\n req: Request\n url: URL\n context: { params?: Record<string, string> }\n}\n\nexport interface CreateWebAppOptions {\n /**\n * The h3 v2 module, statically imported by the caller:\n * `import * as h3 from 'h3'`. Passed in (rather than loaded here) because\n * edge bundlers have no `createRequire`, and a static `import 'h3'` inside\n * this entry would force the peer on every consumer of the subpath.\n */\n h3: unknown\n /** App modules — same shapes `bootstrap({ modules })` accepts. */\n modules: AppModuleEntry[]\n /** Route prefix (default '/api') — parity with bootstrap. */\n apiPrefix?: string\n /**\n * Default route version (default 1) — parity with bootstrap. `false` opts\n * out of URL versioning; a module's own `version` still overrides it.\n */\n defaultVersion?: number | false\n /** Global Context Contributors — parity with `bootstrap({ contributors })`. */\n contributors?: ContributorRegistrations\n /**\n * Env values for ConfigService/@Value resolution. On Workers, pass the\n * `env` binding from the fetch handler (see `createFetchHandler`); on\n * Bun/Deno, ambient `process.env` already works and this can be omitted.\n */\n env?: Record<string, string | undefined>\n /**\n * Global `(ctx, next)` middlewares, run before route middlewares on every\n * route — the web-entry counterpart of `bootstrap({ middlewares })`. Only\n * ctx-style handlers are accepted (no connect/express middleware here).\n */\n middlewares?: MiddlewareHandler[]\n}\n\nexport interface WebApp {\n /**\n * Web-standard entry: `Request` in, `Response` out. Pass the platform\n * context (Workers `ctx`, Netlify `context`) so work handed to\n * `ctx.waitUntil()` finishes after the response.\n */\n fetch: (request: Request, platform?: PlatformContext) => Promise<Response>\n /** The underlying h3 v2 app, for advanced composition. */\n h3: unknown\n}\n\n/**\n * Build a KickJS app as a web-standard fetch handler — no node http server,\n * no Application/bootstrap. Module registration, DI, and the contributor\n * pipeline behave exactly like `bootstrap()`; routes are compiled once here\n * and served through h3 v2's router.\n */\nexport function createWebApp(options: CreateWebAppOptions): WebApp {\n const h3mod = options.h3 as { H3?: new () => H3AppLike }\n if (typeof h3mod?.H3 !== 'function') {\n throw new Error(\n '@forinda/kickjs/web: pass the h3 v2 module — `createWebApp({ h3: await import(\"h3\"), ... })`. ' +\n \"The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.\",\n )\n }\n\n const container = Container.getInstance()\n\n // Wire ConfigService/@Value env resolution when the platform has no\n // ambient process.env (Workers). Latest-wins like loadEnv().\n if (options.env) {\n const env = options.env\n Container._envResolver = (key: string) => env[key]\n }\n\n // Request-scope provider — same wiring Application.setup() performs.\n Container._requestStoreProvider = () => requestStore.getStore() ?? null\n\n // Module mounting — mirrors Application.setup() steps 1:1 for the module\n // path (plugins/adapters are out of scope for the edge entry at launch).\n const registry = new MutableModuleRegistry()\n for (const m of options.modules) registry.mount(m)\n const modules = registry.entries.map((entry) => {\n const mod: AppModule = typeof entry === 'function' ? new (entry as AppModuleClass)() : entry\n mod.register?.(container)\n return mod\n })\n container.bootstrap()\n\n const app = new h3mod.H3()\n const apiPrefix = options.apiPrefix ?? '/api'\n const defaultVersion = options.defaultVersion ?? 1\n const globalMiddleware = options.middlewares ?? []\n\n const globalSources: SourcedRegistration[] = (options.contributors ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'global',\n registration: registration as SourcedRegistration['registration'],\n label: 'createWebApp',\n }),\n )\n\n // Duplicate-route guard (KICK006) — same boot-time check as the node path.\n const seenRoutes = new Map<string, string>()\n\n for (const mod of modules) {\n const declaredName = (mod as { name?: unknown }).name\n const moduleLabel =\n typeof declaredName === 'string' && declaredName.length > 0\n ? declaredName\n : (mod.constructor?.name ?? 'module')\n const moduleSources: SourcedRegistration[] = (mod.contributors?.() ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'module',\n registration,\n label: moduleLabel,\n }),\n )\n\n _setExternalContributorSources([...moduleSources, ...globalSources])\n try {\n const result = mod.routes()\n if (!result) continue\n const routeSets = Array.isArray(result) ? result : [result]\n for (const route of routeSets) {\n if (!route.controller) {\n throw new Error(\n `@forinda/kickjs/web: module route at '${route.path}' has no controller. ` +\n 'Express-style `router` mounts are not supported on the web entry.',\n )\n }\n const version = route.version ?? defaultVersion\n const mountPath = buildMountPath(apiPrefix, version, route.path)\n for (const entry of buildRouteTable(route.controller, { mountFlags: route.flags })) {\n const url = joinPath(mountPath, entry.path)\n // Per-handler owner so intra-controller duplicates name both methods.\n const owner = `${route.controller.name ?? 'controller'}.${String(entry.meta.handlerName ?? '?')}`\n assertRouteUnique(seenRoutes, entry.method, url, owner)\n if (globalMiddleware.length > 0) {\n entry.middlewares = [...globalMiddleware, ...entry.middlewares]\n }\n const run = compileWebRoute(entry)\n app.on(entry.method, url, (event: H3EventLike) =>\n run({\n request: event.req,\n url: event.url,\n params: event.context.params ?? {},\n }),\n )\n }\n }\n } finally {\n _setExternalContributorSources([])\n }\n }\n\n return {\n fetch: async (request: Request, platform?: PlatformContext) => {\n const response = await app.fetch(request)\n forwardBackgroundWork(platform)\n return response\n },\n h3: app,\n }\n}\n\n/**\n * Cloudflare Workers convenience: lazily build the app on the first request\n * so the Workers `env` binding can seed config before any module resolves.\n *\n * The Workers execution context (third argument) is forwarded, so work\n * handed to `ctx.waitUntil()` keeps running after the response.\n *\n * `scheduled` runs the `@Cron` jobs whose expression equals the trigger's\n * `event.cron` — list the same expressions under `[triggers] crons` in\n * wrangler.toml.\n *\n * `queue` consumes Cloudflare Queues: each message goes to the `@Job` class\n * for the batch's queue via `runJob`, and is acked when its handler\n * succeeds or retried when it throws. A message body of `{ name, data }`\n * picks the `@Process(name)` method; any other body goes to the queue's\n * catch-all `@Process()` as `data`.\n *\n * ```ts\n * export default createFetchHandler((env) => ({ h3, modules, env }))\n * ```\n */\nexport function createFetchHandler(\n build: (env: Record<string, string | undefined>) => CreateWebAppOptions,\n): {\n fetch: (\n request: Request,\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<Response>\n scheduled: (\n event: { cron: string },\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<void>\n queue: (\n batch: QueueBatch,\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<void>\n} {\n let app: WebApp | undefined\n return {\n fetch: (request, env = {}, ctx) => {\n app ??= createWebApp(build(env))\n return app.fetch(request, ctx)\n },\n scheduled: async (event, env = {}, ctx) => {\n app ??= createWebApp(build(env))\n const run = runCronJobs(Container.getInstance(), { expression: event.cron }, 'workers').then(\n () => {},\n )\n ctx?.waitUntil?.(run)\n await run\n },\n queue: async (batch, env = {}) => {\n app ??= createWebApp(build(env))\n const container = Container.getInstance()\n await Promise.all(\n batch.messages.map(async (message) => {\n const body = message.body as { name?: unknown; data?: unknown } | null\n const named = body !== null && typeof body === 'object' && typeof body.name === 'string'\n try {\n await runJob(container, batch.queue, {\n name: named ? (body!.name as string) : '',\n data: named ? body!.data : message.body,\n id: message.id,\n })\n message.ack()\n } catch {\n // Reported by runJob; Cloudflare redelivers per the queue's retry settings.\n message.retry()\n }\n }),\n )\n },\n }\n}\n\n/** The part of a Cloudflare Queues `MessageBatch` the consumer uses. */\nexport interface QueueBatch {\n readonly queue: string\n readonly messages: ReadonlyArray<{\n readonly id: string\n readonly body: unknown\n ack(): void\n retry(): void\n }>\n}\n\n/** Join a mount prefix and a route path into one URL, collapsing slashes. */\nfunction joinPath(mountPath: string, path: string): string {\n const a = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n if (path === '/' || path === '') return a === '' ? '/' : a\n const b = path.startsWith('/') ? path : `/${path}`\n return `${a}${b}`\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"web.mjs","names":[],"sources":["../src/web.ts"],"sourcesContent":["// `@forinda/kickjs/web` — the web-standard fetch entry for edge runtimes\n// (Cloudflare Workers), Bun, and Deno.\n//\n// PURITY CONTRACT: this entry's import graph must stay free of node-only\n// runtime imports — no `node:http`, `node:cluster`, `node:fs`, `node:module`,\n// no `express`, no `Application`/`bootstrap`. The single sanctioned node API\n// is `node:async_hooks` (AsyncLocalStorage — available on Workers with\n// `nodejs_compat`, Deno, and Bun). Enforced by the bundle-graph purity test.\n//\n// ```ts\n// // Cloudflare Workers (wrangler.toml: compatibility_flags = [\"nodejs_compat\"])\n// import { createWebApp } from '@forinda/kickjs/web'\n// import * as h3 from 'h3'\n// import { modules } from './modules'\n//\n// const app = createWebApp({ h3, modules })\n// export default { fetch: (req: Request) => app.fetch(req) }\n//\n// // Bun\n// Bun.serve({ fetch: (req) => app.fetch(req) })\n// ```\n\nimport { Container } from './core/container'\nimport { MutableModuleRegistry } from './core/module-registry'\nimport type { AppModule, AppModuleClass, AppModuleEntry } from './core/app-module'\nimport type { ContributorRegistrations } from './core/context-decorator'\nimport type { MiddlewareHandler } from './core/decorators'\nimport type { SourcedRegistration } from './core/contributor-pipeline'\nimport {\n _setExternalContributorSources,\n assertRouteUnique,\n buildRouteTable,\n} from './http/router-builder'\nimport { requestStore } from './http/request-store'\nimport { forwardBackgroundWork, type PlatformContext } from './http/background'\nimport { runCronJobs } from './core/cron'\nimport { runJob } from './core/jobs'\nimport { compileWebRoute } from './http/web/handler'\nimport { buildMountPath } from './core/path'\n\nexport { WebRequestShim, WebResponseDriver } from './http/web/driver'\nexport { compileWebRoute } from './http/web/handler'\n// Return-value handler helpers — edge consumers can't import the main barrel.\nexport { reply, isReply, type Reply, type InferHandlerResponse } from './http/reply'\nexport type { SseHandler } from './http/context'\nexport { waitUntil, type PlatformContext } from './http/background'\nexport { runCronJobs, type CronRun } from './core/cron'\nexport {\n runJob,\n registerJobContext,\n stampJobContext,\n JOB_CONTEXT_FIELD,\n type JobLike,\n type JobContextCarrier,\n} from './core/jobs'\n// Edge-safe stores + ctx-style rate limiter (zero runtime imports).\nexport {\n KvRateLimitStore,\n KvSessionStore,\n type KvLike,\n type KvRateLimitStoreOptions,\n type KvSessionStoreOptions,\n} from './http/middleware/kv-stores'\nexport { rateLimitGuard, type RateLimitGuardOptions } from './http/middleware/rate-limit-guard'\n\n// Minimal structural surface of h3 v2 used here (kept local so this entry\n// never imports the node-coupled h3-web runtime module).\ninterface H3AppLike {\n on(method: string, path: string, handler: (event: H3EventLike) => unknown): unknown\n all(path: string, handler: (event: H3EventLike) => unknown): unknown\n fetch(request: Request): Promise<Response>\n}\ninterface H3EventLike {\n req: Request\n url: URL\n context: { params?: Record<string, string> }\n}\n\nexport interface CreateWebAppOptions {\n /**\n * The h3 v2 module, statically imported by the caller:\n * `import * as h3 from 'h3'`. Passed in (rather than loaded here) because\n * edge bundlers have no `createRequire`, and a static `import 'h3'` inside\n * this entry would force the peer on every consumer of the subpath.\n */\n h3: unknown\n /** App modules — same shapes `bootstrap({ modules })` accepts. */\n modules: AppModuleEntry[]\n /** Route prefix (default '/api') — parity with bootstrap. */\n apiPrefix?: string\n /**\n * Default route version (default 1) — parity with bootstrap. `false` opts\n * out of URL versioning; a module's own `version` still overrides it.\n */\n defaultVersion?: number | false\n /** Global Context Contributors — parity with `bootstrap({ contributors })`. */\n contributors?: ContributorRegistrations\n /**\n * Env values for ConfigService/@Value resolution. On Workers, pass the\n * `env` binding from the fetch handler (see `createFetchHandler`); on\n * Bun/Deno, ambient `process.env` already works and this can be omitted.\n */\n env?: Record<string, string | undefined>\n /**\n * Global `(ctx, next)` middlewares, run before route middlewares on every\n * route — the web-entry counterpart of `bootstrap({ middlewares })`. Only\n * ctx-style handlers are accepted (no connect/express middleware here).\n */\n middlewares?: MiddlewareHandler[]\n}\n\nexport interface WebApp {\n /**\n * Web-standard entry: `Request` in, `Response` out. Pass the platform\n * context (Workers `ctx`, Netlify `context`) so work handed to\n * `ctx.waitUntil()` finishes after the response.\n */\n fetch: (request: Request, platform?: PlatformContext) => Promise<Response>\n /** The underlying h3 v2 app, for advanced composition. */\n h3: unknown\n}\n\n/**\n * Build a KickJS app as a web-standard fetch handler — no node http server,\n * no Application/bootstrap. Module registration, DI, and the contributor\n * pipeline behave exactly like `bootstrap()`; routes are compiled once here\n * and served through h3 v2's router.\n */\nexport function createWebApp(options: CreateWebAppOptions): WebApp {\n const h3mod = options.h3 as { H3?: new () => H3AppLike }\n if (typeof h3mod?.H3 !== 'function') {\n throw new Error(\n '@forinda/kickjs/web: pass the h3 v2 module — `createWebApp({ h3: await import(\"h3\"), ... })`. ' +\n \"The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.\",\n )\n }\n\n const container = Container.getInstance()\n\n // Wire ConfigService/@Value env resolution when the platform has no\n // ambient process.env (Workers). Latest-wins like loadEnv().\n if (options.env) {\n const env = options.env\n Container._envResolver = (key: string) => env[key]\n }\n\n // Request-scope provider — same wiring Application.setup() performs.\n Container._requestStoreProvider = () => requestStore.getStore() ?? null\n\n // Module mounting — mirrors Application.setup() steps 1:1 for the module\n // path (plugins/adapters are out of scope for the edge entry at launch).\n const registry = new MutableModuleRegistry()\n for (const m of options.modules) registry.mount(m)\n const modules = registry.entries.map((entry) => {\n const mod: AppModule = typeof entry === 'function' ? new (entry as AppModuleClass)() : entry\n mod.register?.(container)\n return mod\n })\n container.bootstrap()\n\n const app = new h3mod.H3()\n const apiPrefix = options.apiPrefix ?? '/api'\n const defaultVersion = options.defaultVersion ?? 1\n const globalMiddleware = options.middlewares ?? []\n\n const globalSources: SourcedRegistration[] = (options.contributors ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'global',\n registration: registration as SourcedRegistration['registration'],\n label: 'createWebApp',\n }),\n )\n\n // Duplicate-route guard (KICK006) — same boot-time check as the node path.\n const seenRoutes = new Map<string, string>()\n\n for (const mod of modules) {\n const declaredName = (mod as { name?: unknown }).name\n const moduleLabel =\n typeof declaredName === 'string' && declaredName.length > 0\n ? declaredName\n : (mod.constructor?.name ?? 'module')\n const moduleSources: SourcedRegistration[] = (mod.contributors?.() ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'module',\n registration,\n label: moduleLabel,\n }),\n )\n\n _setExternalContributorSources([...moduleSources, ...globalSources])\n try {\n const result = mod.routes()\n if (!result) continue\n const routeSets = Array.isArray(result) ? result : [result]\n for (const route of routeSets) {\n if (!route.controller) {\n throw new Error(\n `@forinda/kickjs/web: module route at '${route.path}' has no controller. ` +\n 'Express-style `router` mounts are not supported on the web entry.',\n )\n }\n const version = route.version ?? defaultVersion\n const mountPath = buildMountPath(apiPrefix, version, route.path)\n for (const entry of buildRouteTable(route.controller, { mountFlags: route.flags })) {\n const url = joinPath(mountPath, entry.path)\n // Per-handler owner so intra-controller duplicates name both methods.\n const owner = `${route.controller.name ?? 'controller'}.${String(entry.meta.handlerName ?? '?')}`\n assertRouteUnique(seenRoutes, entry.method, url, owner)\n if (globalMiddleware.length > 0) {\n entry.middlewares = [...globalMiddleware, ...entry.middlewares]\n }\n const run = compileWebRoute(entry)\n app.on(entry.method, url, (event: H3EventLike) =>\n run({\n request: event.req,\n url: event.url,\n params: event.context.params ?? {},\n }),\n )\n }\n }\n } finally {\n _setExternalContributorSources([])\n }\n }\n\n return {\n fetch: async (request: Request, platform?: PlatformContext) => {\n const response = await app.fetch(request)\n forwardBackgroundWork(platform)\n return response\n },\n h3: app,\n }\n}\n\n/**\n * Cloudflare Workers convenience: lazily build the app on the first request\n * so the Workers `env` binding can seed config before any module resolves.\n *\n * The Workers execution context (third argument) is forwarded, so work\n * handed to `ctx.waitUntil()` keeps running after the response.\n *\n * `scheduled` runs the `@Cron` jobs whose expression equals the trigger's\n * `event.cron` — list the same expressions under `[triggers] crons` in\n * wrangler.toml.\n *\n * `queue` consumes Cloudflare Queues: each message goes to the `@Job` class\n * for the batch's queue via `runJob`, and is acked when its handler\n * succeeds or retried when it throws. A message body of `{ name, data }`\n * picks the `@Process(name)` method; any other body goes to the queue's\n * catch-all `@Process()` as `data`.\n *\n * ```ts\n * export default createFetchHandler((env) => ({ h3, modules, env }))\n * ```\n */\nexport function createFetchHandler(\n build: (env: Record<string, string | undefined>) => CreateWebAppOptions,\n): {\n fetch: (\n request: Request,\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<Response>\n scheduled: (\n event: { cron: string },\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<void>\n queue: (\n batch: QueueBatch,\n env?: Record<string, string | undefined>,\n ctx?: PlatformContext,\n ) => Promise<void>\n} {\n let app: WebApp | undefined\n return {\n fetch: (request, env = {}, ctx) => {\n app ??= createWebApp(build(env))\n return app.fetch(request, ctx)\n },\n scheduled: async (event, env = {}, ctx) => {\n app ??= createWebApp(build(env))\n const run = runCronJobs(Container.getInstance(), { expression: event.cron }, 'workers').then(\n () => {},\n )\n ctx?.waitUntil?.(run)\n await run\n },\n queue: async (batch, env = {}) => {\n app ??= createWebApp(build(env))\n const container = Container.getInstance()\n await Promise.all(\n batch.messages.map(async (message) => {\n const body = message.body as { name?: unknown; data?: unknown } | null\n const named = body !== null && typeof body === 'object' && typeof body.name === 'string'\n try {\n await runJob(container, batch.queue, {\n name: named ? (body!.name as string) : '',\n data: named ? body!.data : message.body,\n id: message.id,\n })\n message.ack()\n } catch {\n // Reported by runJob; Cloudflare redelivers per the queue's retry settings.\n message.retry()\n }\n }),\n )\n },\n }\n}\n\n/** The part of a Cloudflare Queues `MessageBatch` the consumer uses. */\nexport interface QueueBatch {\n readonly queue: string\n readonly messages: ReadonlyArray<{\n readonly id: string\n readonly body: unknown\n ack(): void\n retry(): void\n }>\n}\n\n/** Join a mount prefix and a route path into one URL, collapsing slashes. */\nfunction joinPath(mountPath: string, path: string): string {\n const a = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n if (path === '/' || path === '') return a === '' ? '/' : a\n const b = path.startsWith('/') ? path : `/${path}`\n return `${a}${b}`\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs",
3
- "version": "9.1.0",
3
+ "version": "9.2.0",
4
4
  "description": "Modern type-safe backend framework, powered by Vite. Built for developer experience, pluggable as you need — runs on Express, Fastify or h3.",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -116,7 +116,7 @@
116
116
  ],
117
117
  "dependencies": {
118
118
  "reflect-metadata": "^0.2.2",
119
- "@forinda/kickjs-schema": "^0.2.3"
119
+ "@forinda/kickjs-schema": "^0.2.4"
120
120
  },
121
121
  "peerDependencies": {
122
122
  "@fastify/middie": "^9.0.0",
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v9.1.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{METADATA}from"./core/interfaces.mjs";import{c as http2UnsupportedRuntimeError,i as createToken,l as moduleRouteMissingControllerError,n as tokenName,s as http2RequiresTlsError,t as Container,u as tlsCredentialsError}from"./container-ZnfelY_a.mjs";import{Logger,createLogger}from"./core/logger.mjs";import{t as getClassMeta}from"./metadata-CW4I35nN.mjs";import{Controller,Get}from"./core/decorators.mjs";import{t as MutableModuleRegistry}from"./module-registry-kqozOSo0.mjs";import{a as setObserverPublisher,i as reportResponse,n as releaseObservers,o as setObservers,r as reportError,t as hasResponseObservers}from"./observers-CC3sO1BM.mjs";import{runCronJobs}from"./core/cron.mjs";import{i as runDisposables,n as drainDisposables,r as registerDisposable,t as disposeAll}from"./disposables-DIXt8ze-.mjs";import{HttpException}from"./core/errors.mjs";import{buildMountPath,joinPaths}from"./core/path.mjs";import{t as ROUTE_SLOT}from"./route-flag-sIYXay3q.mjs";import"./core-_WNyghl_.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-CzVjPs5y.mjs";import{r as offerRoutePolicy,t as RoutePolicyTable}from"./route-policy-B6_toGJW.mjs";import{c as requestScopeMiddleware,d as rejectUnsupportedBody,i as reply,l as EXPRESS_JSON_TYPES,n as applyHandlerResult,s as isRequestScopeMiddleware,t as publishMatchedRoute}from"./runtime-CDz5ayBt.mjs";import{requestId}from"./http/middleware/request-id.mjs";import{a as settleBackgroundWork,i as pendingBackgroundWork,l as requestStore,t as RequestContext}from"./context-BGPof8bs.mjs";import{errorHandler,notFoundHandler}from"./http/middleware/error-handler.mjs";import{t as __decorate}from"./decorate-BBwu7--P.mjs";import{validate}from"./http/middleware/validate.mjs";import{buildUploadMiddleware}from"./http/middleware/upload.mjs";import http from"node:http";import{randomBytes,timingSafeEqual}from"node:crypto";import https from"node:https";import http2 from"node:http2";import tls from"node:tls";import express,{Router}from"express";import diagnostics from"node:diagnostics_channel";const mergeConfig=(defaults={},overrides={})=>({...defaults,...overrides}),composeName=(base,scope)=>`${base}:${scope}`;function defineModule(options){if(typeof options!=`object`||!options)throw TypeError(`defineModule: options must be an object literal. See architecture.md §21 for the spec shape.`);if(typeof options.name!=`string`||options.name.length===0){let got=typeof options.name==`string`?`""`:typeof options.name;throw TypeError(`defineModule: options.name must be a non-empty string (got ${got}). Module names appear in diagnostics and \`.scoped()\` namespacing — anonymous modules make cycle / missing-dep errors hard to localize.`)}if(typeof options.build!=`function`)throw TypeError(`defineModule(${options.name}): options.build is required and must be a function (got ${typeof options.build}). The build function returns the AppModule shape — register() / routes() / contributors() — wired with the resolved config.`);let buildSync=(instanceName,scoped,overrides)=>{let config=mergeConfig(options.defaults,overrides),built=options.build(config,{name:instanceName,scoped});return Object.defineProperty(built,"__moduleName",{value:instanceName,enumerable:!1,writable:!1,configurable:!1}),built},factory=(config=>buildSync(options.name,!1,config));return factory.scoped=(scopeName,config)=>buildSync(composeName(options.name,scopeName),!0,config),Object.defineProperty(factory,"definition",{value:Object.freeze({...options}),writable:!1,enumerable:!0,configurable:!1}),factory}var DuplicateMountNameError=class extends Error{kind;mountName;constructor(kind,mountName){super(`Duplicate ${kind} name '${mountName}' — every ${kind} must declare a unique name`),this.name=`DuplicateMountNameError`,this.kind=kind,this.mountName=mountName}},MissingMountDepError=class extends Error{kind;missing;dependent;constructor(kind,missing,dependent){super(`Missing ${kind} dependency '${missing}' required by '${dependent}' — is the ${kind} installed and named correctly?`),this.name=`MissingMountDepError`,this.kind=kind,this.missing=missing,this.dependent=dependent}},MountCycleError=class extends Error{kind;cycle;constructor(kind,cycle){super(`Cycle in ${kind} dependsOn declarations: ${cycle.join(` → `)}`),this.name=`MountCycleError`,this.kind=kind,this.cycle=Object.freeze([...cycle])}};function mountSort(items,kind){let byName=new Map;for(let item of items){if(byName.has(item.name))throw new DuplicateMountNameError(kind,item.name);byName.set(item.name,item)}let indegree=new Map,dependents=new Map;for(let item of items)indegree.set(item.name,item.dependsOn?.length??0);for(let item of items)for(let dep of item.dependsOn??[]){if(!byName.has(dep))throw new MissingMountDepError(kind,dep,item.name);let list=dependents.get(dep)??[];list.push(item),dependents.set(dep,list)}let queue=[];for(let item of items)indegree.get(item.name)===0&&queue.push(item);let sorted=[];for(;queue.length>0;){let node=queue.shift();sorted.push(node);for(let dependent of dependents.get(node.name)??[]){let next=(indegree.get(dependent.name)??0)-1;indegree.set(dependent.name,next),next===0&&queue.push(dependent)}}if(sorted.length!==items.length)throw new MountCycleError(kind,reconstructCycle(items.filter(it=>(indegree.get(it.name)??0)>0),byName));return sorted}function reconstructCycle(residual,byName){let residualNames=new Set(residual.map(r=>r.name)),start=residual[0].name,path=[start],seen=new Set([start]),cursor=start;for(;;){let nextDep=byName.get(cursor).dependsOn?.find(d=>residualNames.has(d));if(nextDep===void 0)break;if(seen.has(nextDep)){let cycleStart=path.indexOf(nextDep);return[...path.slice(cycleStart),nextDep]}path.push(nextDep),seen.add(nextDep),cursor=nextDep}return path}const HANDLER_CHANNEL=`kickjs:handler`,ERROR_CHANNEL=`kickjs:error`,RESPONSE_CHANNEL=`kickjs:response`,handlerChannel=diagnostics.tracingChannel(HANDLER_CHANNEL),errorChannel=diagnostics.channel(ERROR_CHANNEL),responseChannel=diagnostics.channel(RESPONSE_CHANNEL);function installChannelPublisher(){setObserverPublisher({name:`diagnostics_channel`,onError(error,info){errorChannel.hasSubscribers&&errorChannel.publish({error,...info})},onResponse(info){responseChannel.hasSubscribers&&responseChannel.publish(info)}},()=>responseChannel.hasSubscribers)}function traceHandler(handler,meta){return ctx=>handlerChannel.hasSubscribers?handlerChannel.tracePromise(async()=>handler(ctx),{...meta,requestId:ctx.requestId,ctx}):handler(ctx)}function __decorateMetadata(k,v){if(typeof Reflect==`object`&&typeof Reflect.metadata==`function`)return Reflect.metadata(k,v)}const HEALTH_PROBE=createToken(`kick/Health/probe`);let HealthController=class{probe;constructor(probe){this.probe=probe}live(){return this.probe.isDraining()?reply(503,{status:`draining`,uptime:process.uptime()}):reply(200,{status:`ok`,uptime:process.uptime()})}async ready(){if(this.probe.isDraining())return reply(503,{status:`draining`,checks:[]});let checks=await this.probe.checks(),healthy=checks.every(c=>c.status===`up`);return reply(healthy?200:503,{status:healthy?`ready`:`degraded`,checks})}};__decorate([Get(`/live`),__decorateMetadata(`design:type`,Function),__decorateMetadata(`design:paramtypes`,[]),__decorateMetadata(`design:returntype`,void 0)],HealthController.prototype,`live`,null),__decorate([Get(`/ready`),__decorateMetadata(`design:type`,Function),__decorateMetadata(`design:paramtypes`,[]),__decorateMetadata(`design:returntype`,Promise)],HealthController.prototype,`ready`,null),HealthController=__decorate([Controller(),__decorateMetadata(`design:paramtypes`,[Object])],HealthController);const healthModule=defineModule({name:`HealthModule`,build:config=>({register(container){container.registerFactory(HealthController,()=>{let probe=container.resolve(HEALTH_PROBE);return new HealthController(probe)})},routes(){return{path:`/health`,controller:HealthController,version:!1,prefix:!1,flags:config.flags}}})});function materializeRouter(entries){let router=Router();for(let entry of entries){let method=entry.method.toLowerCase(),handlers=[];handlers.push((req,_res,next)=>{publishMatchedRoute(req,entry),next()});let contributorStep=run=>async(req,res,next)=>{let ctx=new RequestContext(req,res,next);try{await run(ctx),res.headersSent||next()}catch(err){next(err)}};entry.earlyContributorRunner&&handlers.push(contributorStep(entry.earlyContributorRunner)),entry.meta.validation&&handlers.push(validate(entry.meta.validation)),entry.meta.upload&&handlers.push(buildUploadMiddleware(entry.meta.upload));for(let mw of entry.middlewares)handlers.push((req,res,next)=>{let ctx=new RequestContext(req,res,next);Promise.resolve(mw(ctx,next)).catch(next)});entry.contributorRunner&&handlers.push(contributorStep(entry.contributorRunner)),handlers.push(async(req,res,next)=>{let ctx=new RequestContext(req,res,next);try{let result=await entry.handler(ctx);res.headersSent||applyHandlerResult(ctx,result)}catch(err){next(err)}}),router[method](entry.path,...handlers)}return router}function buildRoutes(controllerClass,options={}){return materializeRouter(buildRouteTable(controllerClass,options))}function expressRuntime(){return{name:`express`,createApp(options={}){let app=express();return app.disable(`x-powered-by`),app.set(`trust proxy`,options.trustProxy??!1),app},nodeHandler(app){return(req,res,next)=>{next?app(req,res,next):app(req,res)}},mountRoutes(app,table){for(let{mountPath,routes}of table)app.use(mountPath,materializeRouter(routes))},useConnect(app,mw,opts={}){opts.path===void 0?app.use(mw):app.use(opts.path,mw)},serveStatic(app,prefix,dir){app.use(prefix,express.static(dir))},setNotFound(app,mw){app.use(mw)},setErrorHandler(app,mw){app.use(mw)},capabilities:{render:!0,uploads:!0,connectMiddleware:!0,nativeBodyParsing:!1,http2:!1}}}const HOP_BY_HOP_REQUEST=[`connection`,`keep-alive`,`proxy-connection`,`transfer-encoding`,`upgrade`,`te`,`trailer`,`host`,`content-length`],HOP_BY_HOP_RESPONSE=[`connection`,`keep-alive`,`proxy-connection`,`transfer-encoding`,`upgrade`,`trailer`,`content-encoding`,`content-length`],LOOPBACK_HOST=`x-kick-loopback-host`,LOOPBACK_SECRET=`x-kick-loopback-secret`;async function streamedBody(request){if(!request.body||request.method===`GET`||request.method===`HEAD`)return;let reader=request.body.getReader(),first=await reader.read();if(!first.done)return new ReadableStream({start(controller){controller.enqueue(first.value)},async pull(controller){let{done,value}=await reader.read();done?controller.close():controller.enqueue(value)},cancel(reason){return reader.cancel(reason)}})}function createLoopback(handle){let started,secret=randomBytes(24).toString(`hex`),server=()=>(started??=new Promise((resolve,reject)=>{let instance=http.createServer((req,res)=>{let host=req.headers[LOOPBACK_HOST];req.headers[LOOPBACK_SECRET]===secret&&typeof host==`string`&&(req.headers.host=host),delete req.headers[LOOPBACK_HOST],delete req.headers[LOOPBACK_SECRET],handle(req,res)});instance.once(`error`,reject),instance.listen(0,`127.0.0.1`,()=>{instance.unref(),resolve({origin:`http://127.0.0.1:${instance.address().port}`,server:instance})})}).catch(err=>{throw started=void 0,err}),started);return{async fetch(request){let{origin}=await server(),url=new URL(request.url),headers=new Headers(request.headers);for(let name of HOP_BY_HOP_REQUEST)headers.delete(name);headers.set(`x-forwarded-host`,url.host),headers.set(`x-forwarded-proto`,url.protocol.slice(0,-1)),headers.set(LOOPBACK_HOST,url.host),headers.set(LOOPBACK_SECRET,secret);let body=await streamedBody(request),upstream=await fetch(origin+url.pathname+url.search,{method:request.method,headers,body,...body?{duplex:`half`}:{},redirect:`manual`,signal:request.signal}),responseHeaders=new Headers(upstream.headers),named=responseHeaders.get(`connection`)?.split(`,`)??[];for(let name of[...HOP_BY_HOP_RESPONSE,...named])responseHeaders.delete(name.trim());return new Response(upstream.body,{status:upstream.status,statusText:upstream.statusText,headers:responseHeaders})},async close(){let running=await started?.catch(()=>void 0);if(started=void 0,!running)return;let closed=new Promise(resolve=>running.server.close(()=>resolve()));running.server.closeAllConnections(),await closed}}}const log=createLogger(`Application`);function assertUsableTls(options){let{key,cert,pfx,SNICallback,pskCallback}=options;if(!pfx&&!SNICallback&&!pskCallback){if(!key&&!cert)throw tlsCredentialsError(`no key/cert, pfx, SNICallback, or pskCallback`);if(!key)throw tlsCredentialsError(`cert is set but key is missing`);if(!cert)throw tlsCredentialsError(`key is set but cert is missing`)}try{tls.createSecureContext(options)}catch(err){throw tlsCredentialsError(err.message)}}function isModuleFactory(entry){return typeof entry==`function`&&`definition`in entry&&typeof entry.scoped==`function`}function isConfigurable(factory){return factory.definition?.defaults!=null}function toAppModule(entry){if(typeof entry!=`function`)return entry;if(isModuleFactory(entry)){if(isConfigurable(entry)){let name=entry.definition?.name||entry.name||`<anonymous>`;throw TypeError(`bootstrap: module \`${name}\` takes configuration, so it must be invoked.\n Write \`${name}()\` for its defaults, or \`${name}({ … })\` to configure it.\n Passing it bare would have silently selected the defaults.`)}return entry()}let name=entry.name||`<anonymous>`;if(!isConstructible(entry))throw TypeError(`bootstrap: module entry \`${name}\` is a function, but not a module class and not a defineModule() factory (no \`definition\`).\n A defineModule() module is passed as \`${name}\` or \`${name}()\`; a class must implement AppModule.`);let mod=new entry;if(typeof mod?.routes!=`function`)throw TypeError(`bootstrap: module entry \`${name}\` constructed, but the result does not implement AppModule — \`routes()\` is missing.\n A module class needs \`routes()\`; a defineModule() module is passed as \`${name}\` or \`${name}()\`.`);return mod}function isConstructible(fn){try{return Reflect.construct(String,[],fn),!0}catch{return!1}}var Application=class{options;app;runtime;container;httpServer=null;handlerMode=!1;observers=[];http2Sessions=new Set;loopback;adapters;routePolicy=new RoutePolicyTable;plugins;_moduleContributors=[];_inFlightRequests=0;_draining=!1;_shutdownInitiated=!1;_drainResolvers=[];constructor(options){this.options=options,this.runtime=options.runtime??expressRuntime(),this.app=this.runtime.createApp({trustProxy:options.trustProxy}),this.container=Container.getInstance(),this.plugins=mountSort(options.plugins??[],`plugin`);let allAdapters=[...this.plugins.flatMap(p=>p.adapters?.()??[]),...options.adapters??[]],anonAdapterCount=0,namedAdapters=allAdapters.map(adapter=>{if(!adapter.name){let ctorName=adapter.constructor?.name,fallback=ctorName&&ctorName!==`Object`?ctorName:`AnonymousAdapter#${anonAdapterCount++}`;return Object.assign(adapter,{name:fallback})}return adapter});this.adapters=mountSort(namedAdapters,`adapter`),this.observers=setObservers([...this.adapters,...this.plugins]),installChannelPublisher(),Container._requestStoreProvider=()=>requestStore.getStore()??null}get isDraining(){return this._draining}get inFlightRequests(){return this._inFlightRequests}getContainer(){return this.container}fetch(request){let runtimeApp=this.app;return typeof runtimeApp?.fetch==`function`?Promise.resolve(runtimeApp.fetch(request)):(this.loopback??=createLoopback((req,res)=>this.handle(req,res)),this.loopback.fetch(request))}handle(req,res,next){let handler=this.runtime.nodeHandler(this.app);next?handler(req,res,next):handler(req,res)}adapterCtx(server){let env=process.env.NODE_ENV??`development`;return{http:this.adapterHttp(),app:this.app,fetch:request=>this.fetch(request),container:this.container,server,env,isProduction:env===`production`}}adapterHttp(){return{route:(method,path,handler)=>{let entry={method,path,middlewares:[],contributorRunner:null,handler,meta:{}};this.runtime.mountRoutes(this.app,[{mountPath:`/`,routes:[entry]}])},mount:(prefix,routes)=>{this.runtime.mountRoutes(this.app,[{mountPath:prefix,routes}])},serveStatic:(prefix,dir)=>{this.runtime.serveStatic(this.app,prefix,dir)},use:(mw,opts)=>{this.runtime.useConnect(this.app,mw,opts)}}}async callHook(hook,ctx,adapterName=`adapter`,phase=`setup`){if(hook)try{await hook(ctx)}catch(err){if(phase===`setup`)throw err;log.error(err,`${adapterName} afterStart hook failed`)}}async setup(){log.debug(`Bootstrapping application...`),this._moduleContributors=[];let adapterMw=this.collectAdapterMiddleware();this.app.__kickApp=this;let ctx=this.adapterCtx();for(let adapter of this.adapters)await this.callHook(adapter.beforeMount?.bind(adapter),ctx,adapter.name);this.handlerMode&&this.mountCronTrigger(),this.runtime.useConnect(this.app,this.requestTrackingMiddleware()),this.shouldAutoMountRequestScope()&&this.runtime.useConnect(this.app,requestScopeMiddleware()),this.mountMiddlewareList(adapterMw.beforeGlobal);for(let plugin of this.plugins)plugin.register?.(this.container);for(let plugin of this.plugins)try{let mw=plugin.middleware?.()??[];for(let handler of mw)this.runtime.useConnect(this.app,handler)}catch(err){log.error(err,`Plugin middleware failed: ${plugin.name??`unknown`}`)}let declaredHelmet=(this.options.middlewares??[]).some(entry=>typeof entry==`function`&&Symbol.for(`kick/http/helmet`)in entry);if(this.options.security?.helmet!==!1&&!declaredHelmet)try{let{helmet:helmetFn}=await import(`./http/middleware/helmet.mjs`);this.runtime.useConnect(this.app,helmetFn())}catch{}let userMiddlewares=this.options.middlewares;if(userMiddlewares)for(let entry of userMiddlewares)this.mountMiddlewareEntry(entry);else if(this.runtime.useConnect(this.app,requestId()),!this.runtime.capabilities.nativeBodyParsing){let limit=this.options.jsonLimit??`1mb`;this.runtime.useConnect(this.app,express.json({limit,type:[...EXPRESS_JSON_TYPES]})),this.runtime.useConnect(this.app,express.urlencoded({extended:!0,limit})),this.runtime.useConnect(this.app,express.text({limit,type:`text/*`})),this.runtime.useConnect(this.app,rejectUnsupportedBody())}this.mountMiddlewareList(adapterMw.afterGlobal);let moduleRegistry=new MutableModuleRegistry;for(let plugin of this.plugins){for(let m of plugin.modules?.()??[])moduleRegistry.mount(m);plugin.setup?.(moduleRegistry)}for(let m of this.options.modules)moduleRegistry.mount(m);this.options.setup?.(moduleRegistry),this.options.health!==!1&&(this.container.registerInstance(HEALTH_PROBE,{isDraining:()=>this._draining,checks:async()=>{let withCheck=this.adapters.filter(a=>a.onHealthCheck);return(await Promise.allSettled(withCheck.map(a=>a.onHealthCheck()))).map((c,i)=>c.status===`fulfilled`?c.value:{name:withCheck[i].name??`unknown`,status:`down`})}}),moduleRegistry.mount(healthModule(this.options.health===!0?{}:this.options.health)));let modules=moduleRegistry.entries.map(entry=>{let mod=toAppModule(entry);return mod.register?.(this.container),mod});this.container.bootstrap(),this.container.has(Logger)||this.container.registerInstance(Logger,new Logger),this.mountMiddlewareList(adapterMw.beforeRoutes);let apiPrefix=this.options.apiPrefix??`/api`,defaultVersion=this.options.defaultVersion??1,shouldLogRoutes=this.options.logRouteTable??this.options.logRoutesTable??!1,mountedRoutes=[],adapterSources=[];for(let adapter of this.adapters){let adapterContribs=adapter.contributors?.()??[],adapterLabel=adapter.name??adapter.constructor.name??`adapter`;for(let registration of adapterContribs)adapterSources.push({source:`adapter`,registration,label:adapterLabel})}for(let plugin of this.plugins){let pluginContribs=plugin.contributors?.()??[],pluginLabel=plugin.name??plugin.constructor?.name??`plugin`;for(let registration of pluginContribs)adapterSources.push({source:`adapter`,registration,label:pluginLabel})}let globalSources=(this.options.contributors??[]).map(registration=>({source:`global`,registration,label:`bootstrap`}));this._moduleContributors=[];let seenRoutes=new Map,mountedPaths=[];for(let mod of modules){let declaredName=mod.name,ctorName=mod.constructor?.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:ctorName&&ctorName!==`Object`?ctorName:`module`,moduleSources=(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel}));for(let src of moduleSources)this._moduleContributors.push({registration:src.registration,label:moduleLabel});_setExternalContributorSources([...moduleSources,...adapterSources,...globalSources]);try{let result=mod.routes();if(!result)continue;let routeSets=Array.isArray(result)?result:[result];for(let route of routeSets){let version=route.version??defaultVersion,mountPath=buildMountPath(route.prefix===!1?``:apiPrefix,version,route.path);if(route.router)this.runtime.useConnect(this.app,route.router,{path:mountPath});else if(route.controller){let routeTable=buildRouteTable(route.controller,{mountFlags:route.flags}),ctrl=route.controller.name??`controller`;for(let entry of routeTable){let owner=`${ctrl}.${String(entry.meta.handlerName??`?`)}`,fullPath=joinPaths(mountPath,entry.path);assertRouteUnique(seenRoutes,entry.method,fullPath,owner),mountedPaths.push({method:entry.method,path:fullPath}),this.routePolicy.add(entry.method,fullPath,entry.meta.flags)}let traced=routeTable.map(entry=>{let pattern=joinPaths(mountPath,entry.path);return{...entry,handler:traceHandler(entry.handler,{method:entry.method,route:pattern,controller:route.controller?.name,handler:entry.meta.handlerName}),meta:{...entry.meta,pattern}}});this.runtime.mountRoutes(this.app,[{mountPath,routes:traced}])}else throw moduleRouteMissingControllerError(mountPath);if(route.controller){for(let adapter of this.adapters)try{adapter.onRouteMount?.(route.controller,mountPath)}catch(err){log.error(err,`adapter.onRouteMount() failed for ${mountPath}`)}shouldLogRoutes&&mountedRoutes.push({controller:route.controller,mountPath})}}}finally{_setExternalContributorSources([])}}if(shouldLogRoutes&&mountedRoutes.length>0){let methodOrder=[`GET`,`POST`,`PUT`,`PATCH`,`DELETE`,`HEAD`,`OPTIONS`],methodRank=m=>{let i=methodOrder.indexOf(m);return i===-1?99:i},totalRoutes=0;log.info(`Routes:`);for(let{controller,mountPath}of mountedRoutes){let defs=getClassMeta(METADATA.ROUTES,controller,[]);if(defs.length===0)continue;let counts={};for(let def of defs){let m=def.method.toUpperCase();counts[m]=(counts[m]||0)+1}totalRoutes+=defs.length;let methods=Object.entries(counts).toSorted(([a],[b])=>methodRank(a)-methodRank(b)).map(([m,n])=>`${n} ${m}`).join(`, `),name=controller.name||`Controller`;log.info(` ${name.padEnd(30)} ${mountPath.padEnd(25)} ${defs.length} routes (${methods})`)}log.info(` Total: ${totalRoutes} routes`)}this.mountMiddlewareList(adapterMw.afterRoutes);for(let adapter of this.adapters)await this.callHook(adapter.beforeStart?.bind(adapter),ctx,adapter.name);for(let entry of this.options.middlewares??[])offerRoutePolicy(typeof entry==`function`?entry:entry.handler,this.routePolicy);for(let adapter of this.adapters)for(let mw of adapter.middleware?.()??[])offerRoutePolicy(mw.handler,this.routePolicy);this.runtime.setNotFound(this.app,this.options.onNotFound??notFoundHandler(mountedPaths)),this.runtime.setErrorHandler(this.app,observeErrors(this.options.onError??errorHandler()))}async registerOnly(){await this.setup()}async startWithoutServer(){this.handlerMode=!0,await this.setup();let needServer=this.adapters.filter(adapter=>adapter.afterStart);needServer.length>0&&log.warn(`No HTTP server in handler mode, so afterStart does not run for: ${needServer.map(adapter=>adapter.name).join(`, `)}. Anything they attach to the server (WebSockets, upgrade handlers) is unavailable.`);for(let plugin of this.plugins)await plugin.onReady?.(this.container)}async start(){let g=globalThis,serverOptions=this.options.server;if(serverOptions&&g.__kickjs_httpServer)log.warn(`server options (tls / http2) are ignored in dev mode — Vite owns the server`);else if(serverOptions){if(serverOptions.http2&&!serverOptions.tls)throw http2RequiresTlsError();if(serverOptions.http2&&!this.runtime.capabilities.http2)throw http2UnsupportedRuntimeError(this.runtime.name);serverOptions.tls&&assertUsableTls(serverOptions.tls)}if(await this.setup(),g.__kickjs_httpServer){this.httpServer=g.__kickjs_httpServer,log.debug(`Attached to Vite dev server`),g.__kickjs_app_shutdown=()=>this.shutdown();for(let adapter of this.adapters){let ctx=this.adapterCtx(this.httpServer);await this.callHook(adapter.afterStart?.bind(adapter),ctx,adapter.name,`afterStart`)}for(let plugin of this.plugins)await plugin.onReady?.(this.container);return}let port=this.options.port??parseInt(process.env.PORT||`3000`,10);this.httpServer=this.createNodeServer(),this.httpServer.on(`error`,err=>{throw err.code===`EADDRINUSE`&&(log.error(`Port ${port} is already in use. Kill the existing process or use a different port:\n PORT=${port+1} kick dev\n lsof -i :${port} # find what's using it\n kill <PID> # stop it`),process.exit(1)),err}),await new Promise((resolve,reject)=>{this.httpServer.listen(port,async()=>{try{let scheme=serverOptions?.tls?`https`:`http`,protocol=serverOptions?.http2?` (HTTP/2)`:``;log.info(`Server running on ${scheme}://localhost:${port}${protocol}`);for(let adapter of this.adapters){let afterCtx=this.adapterCtx(this.httpServer);await this.callHook(adapter.afterStart?.bind(adapter),afterCtx,adapter.name,`afterStart`)}for(let plugin of this.plugins)await plugin.onReady?.(this.container);resolve()}catch(err){reject(err)}})})}createNodeServer(){let handler=this.runtime.nodeHandler(this.app),{tls,http2:useHttp2}=this.options.server??{};if(useHttp2){let server=http2.createSecureServer({...tls,allowHTTP1:!0},handler);return server.on(`session`,session=>{this.http2Sessions.add(session),session.once(`close`,()=>this.http2Sessions.delete(session))}),server}return tls?https.createServer(tls,handler):http.createServer(handler)}async rebuild(){let prevApp=this.app,prevContainer=this.container,staleDisposables=drainDisposables();try{Container.reset(),this.container=Container.getInstance(),this.app=this.runtime.createApp({trustProxy:this.options.trustProxy}),await this.setup()}catch(err){log.error(err,`HMR rebuild failed, keeping previous app`),await runDisposables(drainDisposables());for(let d of staleDisposables)registerDisposable(d);this.app=prevApp,this.container=prevContainer;return}await runDisposables(staleDisposables),this.httpServer&&(this.httpServer.removeAllListeners(`request`),this.httpServer.on(`request`,this.runtime.nodeHandler(this.app)),log.debug(`HMR: app rebuilt and swapped`))}async shutdown(options={}){let closeServer=options.closeServer??!0;if(this._shutdownInitiated){log.debug(`Shutdown already in progress, skipping duplicate call`);return}this._shutdownInitiated=!0,this._draining=!0,log.debug(`Shutting down — draining in-flight requests...`);let timeoutMs=this.options.shutdownTimeout??3e4,timer,forceExitPromise=timeoutMs>0?new Promise(resolve=>{timer=setTimeout(()=>resolve(`timeout`),timeoutMs),timer.unref()}):new Promise(()=>{});try{if(closeServer&&this.httpServer){this.httpServer.close(()=>{});for(let session of this.http2Sessions)session.close()}if(closeServer&&this._inFlightRequests>0){log.debug(`Waiting for ${this._inFlightRequests} in-flight request(s) to complete...`);let drainPromise=new Promise(resolve=>{this._drainResolvers.push(()=>resolve(`drained`))});if(await Promise.race([drainPromise,forceExitPromise])===`timeout`){log.warn(`Shutdown timeout (${timeoutMs}ms) reached with ${this._inFlightRequests} request(s) still in-flight, forcing shutdown`);for(let session of this.http2Sessions)session.destroy()}else log.debug(`All in-flight requests completed`)}closeServer&&pendingBackgroundWork()>0&&(log.debug(`Waiting for ${pendingBackgroundWork()} waitUntil() task(s) to settle...`),await Promise.race([settleBackgroundWork().then(()=>`settled`),forceExitPromise])===`timeout`&&log.warn(`Shutdown timeout (${timeoutMs}ms) reached with ${pendingBackgroundWork()} waitUntil() task(s) still running`));let hookTimeoutMs=closeServer?timeoutMs:Math.min(timeoutMs,5e3),timeBoxed=(label,run)=>{let hook=(async()=>run())();return hookTimeoutMs<=0?hook:new Promise((resolve,reject)=>{let t=setTimeout(()=>{log.warn(`${label} did not finish shutting down within ${hookTimeoutMs}ms — continuing without it. Its resources may still be held.`),resolve(void 0)},hookTimeoutMs);t.unref(),hook.then(value=>{clearTimeout(t),resolve(value)},err=>{clearTimeout(t),reject(err)})})},wasListening=this.httpServer?.listening===!0,results=await Promise.allSettled([...this.plugins.map(plugin=>timeBoxed(`Plugin '${plugin.name}'`,()=>plugin.shutdown?.())),...this.adapters.map(adapter=>timeBoxed(`Adapter '${adapter.name}'`,()=>adapter.shutdown?.()))]);!closeServer&&wasListening&&this.httpServer?.listening===!1&&log.error("An adapter or plugin closed the shared HTTP server during an HMR reload. The dev server is now unreachable and will not rebind until you restart it. Close only what you own: socket.io's `io.close()` closes the HTTP server passed to its constructor — on a reload, disconnect the sockets instead.");for(let result of results)result.status===`rejected`&&log.error({err:result.reason},`Adapter shutdown failed`);await disposeAll(),this.container.flushChanges(),await this.loopback?.close(),this.loopback=void 0,closeServer&&releaseObservers(this.observers)}finally{timer&&clearTimeout(timer)}}getRuntimeApp(){return this.app}getActiveRuntime(){return{name:this.runtime.name,capabilities:this.runtime.capabilities}}getExpressApp(){return this.app}getAdapters(){return this.adapters}getPlugins(){return this.plugins}getContributors(){let out=[],ingest=(list,source,label)=>{if(list)for(let reg of list){let r=reg;if(r.key===void 0||r.key===null)continue;let key=typeof r.key==`string`?r.key:tokenName(r.key);out.push({key,source,label,dependsOn:Array.isArray(r.dependsOn)?r.dependsOn:[]})}};for(let adapter of this.adapters)ingest(adapter.contributors?.(),`adapter`,adapter.name??`adapter`);for(let plugin of this.plugins)ingest(plugin.contributors?.(),`plugin`,plugin.name??`plugin`);for(let{registration,label}of this._moduleContributors)ingest([registration],`module`,label);return ingest(this.options.contributors,`global`,`bootstrap`),out}getHttpServer(){return this.httpServer}shouldAutoMountRequestScope(){if(this.options.contextStore===`manual`)return!1;let userEntries=this.options.middlewares??[];for(let entry of userEntries){let handler=typeof entry==`function`?entry:entry.handler;if(isRequestScopeMiddleware(handler))return!1}return!0}collectAdapterMiddleware(){let result={beforeGlobal:[],afterGlobal:[],beforeRoutes:[],afterRoutes:[]};for(let adapter of this.adapters){let entries=adapter.middleware?.()??[];for(let entry of entries)result[entry.phase??`afterGlobal`].push(entry)}return result}mountMiddlewareList(entries){for(let entry of entries){if(entry.path===void 0){this.runtime.useConnect(this.app,entry.handler);continue}this.runtime.useConnect(this.app,entry.handler,{path:entry.path})}}mountMiddlewareEntry(entry){typeof entry==`function`?this.runtime.useConnect(this.app,entry):this.runtime.useConnect(this.app,entry.handler,{path:entry.path})}mountCronTrigger(){let secret=process.env.CRON_SECRET;secret&&this.adapterHttp().route(`GET`,`/_kick/cron/:scheduleId`,async ctx=>{if(!matchesBearer(ctx.headers.authorization,secret))throw new HttpException(401,`Missing or invalid CRON_SECRET bearer token`);let result=await runCronJobs(this.container,{scheduleId:ctx.params.scheduleId},`http`);ctx.json(result,result.failed>0?500:200)})}requestTrackingMiddleware(){return(req,res,next)=>{this._inFlightRequests++;let started=hasResponseObservers()?performance.now():void 0,onFinish=()=>settle(!0),onClose=()=>settle(!1),settle=finished=>{if(res.removeListener(`finish`,onFinish),res.removeListener(`close`,onClose),finished&&started!==void 0){let matched=req[ROUTE_SLOT];reportResponse({method:req.method,path:requestPath(req),route:matched?.pattern,status:res.statusCode,durationMs:performance.now()-started,requestId:requestIdOf(req)})}if(this._inFlightRequests--,this._draining&&this._inFlightRequests===0){for(let resolve of this._drainResolvers)resolve();this._drainResolvers=[]}};res.on(`finish`,onFinish),res.on(`close`,onClose),next()}}};function requestPath(req){return(req.originalUrl??req.url??``).split(`?`)[0]}function requestIdOf(req){let header=req.headers?.[`x-request-id`];return req.requestId??(typeof header==`string`?header:void 0)}function errorStatus(err){return typeof err?.problem?.status==`number`?err.problem.status:typeof err?.status==`number`?err.status:typeof err?.statusCode==`number`?err.statusCode:err?.name===`ZodError`?422:500}function observeErrors(handler){return((err,req,res,next)=>{let matched=req?.[ROUTE_SLOT];return reportError(err,{source:`request`,requestId:requestIdOf(req??{}),method:req?.method,path:requestPath(req??{}),route:matched?.pattern,status:errorStatus(err)}),handler(err,req,res,next)})}function matchesBearer(header,secret){if(typeof header!=`string`)return!1;let given=Buffer.from(header),expected=Buffer.from(`Bearer ${secret}`);return given.length===expected.length&&timingSafeEqual(given,expected)}export{HEALTH_PROBE as a,ERROR_CHANNEL as c,DuplicateMountNameError as d,MissingMountDepError as f,defineModule as h,materializeRouter as i,HANDLER_CHANNEL as l,mountSort as m,buildRoutes as n,HealthController as o,MountCycleError as p,expressRuntime as r,healthModule as s,Application as t,RESPONSE_CHANNEL as u};
12
- //# sourceMappingURL=application-nDj-76EX.mjs.map
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v9.1.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{createLogger}from"./core/logger.mjs";import{r as reportError}from"./observers-CC3sO1BM.mjs";import{normalizeProblem}from"./core/errors.mjs";import{t as ROUTE_SLOT}from"./route-flag-sIYXay3q.mjs";import{r as MissingContextValueError}from"./context-errors-Cgtrrqfs.mjs";import{t as resolveClientIp}from"./client-ip-DMKc-Trg.mjs";import{a as parseQuery}from"./query-DPlkWOw6.mjs";import{AsyncLocalStorage}from"node:async_hooks";const requestStore=new AsyncLocalStorage;function getRequestStore(){let store=requestStore.getStore();if(!store)throw Error(`No active request context. REQUEST-scoped services can only be resolved during an HTTP request.`);return store}function getRequestValue(key){return requestStore.getStore()?.values.get(key)}const log=createLogger(`waitUntil`),pending=new Set;function waitUntil(promise){let requestId=requestStore.getStore()?.requestId,tracked=Promise.resolve(promise).then(()=>{},err=>{log.error({err,requestId},`Background work passed to waitUntil() failed`),reportError(err,{source:`background`,requestId})});pending.add(tracked),tracked.finally(()=>pending.delete(tracked))}async function settleBackgroundWork(){for(;pending.size>0;)await Promise.all(pending)}function pendingBackgroundWork(){return pending.size}function forwardBackgroundWork(platform){platform?.waitUntil?.(settleBackgroundWork())}const signalControllerKey=Symbol.for(`kickjs.requestContext.signalController`),readOnlyProxyCacheKey=Symbol.for(`kickjs.requestContext.readOnlyProxyCache`);function isPlainObjectOrArray(v){if(typeof v!=`object`||!v)return!1;if(Array.isArray(v))return!0;let proto=Object.getPrototypeOf(v);return proto===Object.prototype||proto===null}function makeReadOnlyProxy(target,req,label){if(process.env.NODE_ENV===`production`||!isPlainObjectOrArray(target))return target;let reqObj=req,cache=reqObj[readOnlyProxyCacheKey];cache||(cache=new WeakMap,reqObj[readOnlyProxyCacheKey]=cache);let targetObj=target,cached=cache.get(targetObj);if(cached)return cached;let warnWrite=(verb,prop)=>{console.warn(`[kickjs] Attempted to ${verb} \`ctx.${label}.${String(prop)}\` — request data is read-only. Stash computed values via \`ctx.set('key', value)\` or a Context Contributor, or reach \`ctx.req.${label}\` if you genuinely need the raw mutable handle.`)},proxy=new Proxy(targetObj,{get(t,prop,receiver){let value=Reflect.get(t,prop,receiver);return isPlainObjectOrArray(value)?makeReadOnlyProxy(value,req,`${label}.${String(prop)}`):value},set(_t,prop){return warnWrite(`assign`,prop),!0},deleteProperty(_t,prop){return warnWrite(`delete`,prop),!0},defineProperty(_t,prop){return warnWrite(`Object.defineProperty on`,prop),!0}});return cache.set(targetObj,proxy),proxy}var RequestContext=class{req;res;next;_response;get route(){return this.req?.[ROUTE_SLOT]}constructor(req,res,next,responseDriver){this.req=req,this.res=res,this.next=next,this._response=responseDriver??res}_qsMemo;_problem;metadataReadOnly(){return requestStore.getStore()?.values??null}metadataForWrite(){let store=requestStore.getStore();if(store)return store.values;throw Error(`RequestContext.set called outside an AsyncLocalStorage frame. Mount requestScopeMiddleware() before constructing RequestContext, or wrap test setup in requestStore.run({ requestId, instances: new Map(), values: new Map() }, () => ...).`)}get body(){return makeReadOnlyProxy(this.req.body,this.req,`body`)}get params(){return makeReadOnlyProxy(this.req.params,this.req,`params`)}get query(){return makeReadOnlyProxy(this.req.query,this.req,`query`)}get headers(){return makeReadOnlyProxy(this.req.headers,this.req,`headers`)}get requestId(){return this.req.requestId??this.req.headers[`x-request-id`]}get signal(){let req=this.req,cached=req[signalControllerKey];if(cached)return cached.signal;let ctrl=new AbortController;req[signalControllerKey]=ctrl;let abort=()=>ctrl.abort(`request closed`);return this.req.once(`close`,abort),this._response.once(`close`,abort),ctrl.signal}get session(){return this.req.session}get user(){return this.metadataReadOnly()?.get(`user`)??this.req.user}get tenantId(){return this.user?.tenantId}get roles(){let u=this.user;return u?.tenantRoles??u?.roles??[]}qs(fieldConfig,options){let memo=this._qsMemo;if(memo&&memo.config===fieldConfig&&memo.options===options)return memo.result;let effective={...options,onReject:options?.onReject??(rejection=>{let id=this.requestId?`[${this.requestId}] `:``;console.warn(`${id}query ${rejection.kind} '${rejection.field}' rejected (${rejection.reason})`)})},result=parseQuery(this.req.query,fieldConfig,effective);return this._qsMemo={config:fieldConfig,options,result},result}get file(){let f=this.req.file;return f?makeReadOnlyProxy(f,this.req,`file`):void 0}get files(){let f=this.req.files;if(Array.isArray(f))return makeReadOnlyProxy(f,this.req,`files`)}get(key){return this.metadataReadOnly()?.get(key)}require(key){let value=this.metadataReadOnly()?.get(key);if(value===void 0)throw new MissingContextValueError(key,this.routeLabel());return value}routeLabel(){let req=this.req,method=typeof req?.method==`string`?req.method:void 0,url=typeof req?.url==`string`?req.url:void 0;return method&&url?`${method} ${url}`:method??url}set(key,value){this.metadataForWrite().set(key,value)}setHeader(name,value){return this._response.setHeader(name,value),this}json(data,status=200){return this._response.status(status).json(data)}created(data){return this._response.status(201).json(data)}noContent(){return this._response.status(204).end()}notFound(message=`Not Found`){return this._response.status(404).json({message})}badRequest(message){return this._response.status(400).json({message})}get problem(){if(this._problem)return this._problem;let send=input=>this.sendProblem(input);return send.badRequest=(input={})=>this.sendProblem({status:400,...input}),send.unauthorized=(input={})=>this.sendProblem({status:401,...input}),send.forbidden=(input={})=>this.sendProblem({status:403,...input}),send.notFound=(input={})=>this.sendProblem({status:404,...input}),send.conflict=(input={})=>this.sendProblem({status:409,...input}),send.unprocessable=(input={})=>this.sendProblem({status:422,...input}),send.tooManyRequests=(input={})=>this.sendProblem({status:429,...input}),send.internal=(input={})=>this.sendProblem({status:500,...input}),send.validation=(issues,input={})=>{let errors=(issues||[]).map(issue=>({field:issue.path?.join?.(`.`)??``,message:issue.message??``,code:issue.code}));return this.sendProblem({status:422,detail:issues?.[0]?.message,...input,errors})},this._problem=send,this._problem}sendProblem(input){let body=normalizeProblem(input);return this._response.setHeader(`Content-Type`,`application/problem+json`),this._response.status(body.status).json(body)}get ip(){return resolveClientIp(this.req)}redirect(url,status=302){return this._response.status(status).setHeader(`location`,url),this._response.end()}html(content,status=200){return this._response.status(status).type(`text/html; charset=utf-8`).send(content)}download(buffer,filename,contentType=`application/octet-stream`){return this._response.setHeader(`Content-Disposition`,contentDisposition(filename)),this._response.setHeader(`Content-Type`,contentType),this._response.send(buffer)}render(template,data={}){return this._response.render(template,data)}async paginate(fetcher,fieldConfig){let parsed=this.qs(fieldConfig),{data,total}=await fetcher(parsed),{page,limit}=parsed.pagination,totalPages=Math.ceil(total/limit)||1,response={data,meta:{page,limit,total,totalPages,hasNext:page<totalPages,hasPrev:page>1}};return this.json(response),response}async sendResponse(response){let headers={};response.headers.forEach((value,name)=>{name!==`set-cookie`&&(headers[name]=value)});let cookies=response.headers.getSetCookie();if(cookies.length>0&&(headers[`set-cookie`]=cookies),!response.body){this._response.status(response.status);for(let[name,value]of Object.entries(headers))this._response.setHeader(name,value);this._response.send(``);return}this._response.writeHead(response.status,headers),this._response.flushHeaders();let reader=response.body.getReader(),stop=()=>{reader.cancel().catch(()=>{})};this.signal.addEventListener(`abort`,stop,{once:!0});try{for(;;){let{done,value}=await reader.read();if(done)break;this._response.write(Buffer.from(value))===!1&&!this.signal.aborted&&await new Promise(resolve=>{let resume=()=>{this.signal.removeEventListener(`abort`,resume),resolve()};this._response.once(`drain`,resume),this.signal.addEventListener(`abort`,resume,{once:!0})})}}finally{this.signal.removeEventListener(`abort`,stop),this._response.end()}}waitUntil(promise){waitUntil(promise)}sse(){this._response.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`,"X-Accel-Buffering":`no`}),this._response.flushHeaders();let closeCallbacks=[];return this.req.on(`close`,()=>{for(let cb of closeCallbacks)cb()}),{send:(data,event,id)=>{id&&this._response.write(`id: ${id}\n`),event&&this._response.write(`event: ${event}\n`),this._response.write(`data: ${JSON.stringify(data)}\n\n`)},comment:text=>{this._response.write(`: ${text}\n\n`)},onClose:fn=>{closeCallbacks.push(fn)},close:()=>{this._response.end()}}}};function contentDisposition(filename){return`attachment; filename="${filename.replace(/[^\x20-\x7e]|["\\]/g,`_`)}"; filename*=UTF-8''${encodeURIComponent(filename).replace(/['()*]/g,c=>`%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`}export{settleBackgroundWork as a,getRequestValue as c,pendingBackgroundWork as i,requestStore as l,contentDisposition as n,waitUntil as o,forwardBackgroundWork as r,getRequestStore as s,RequestContext as t};
12
- //# sourceMappingURL=context-BGPof8bs.mjs.map