@forinda/kickjs 6.4.0 → 6.5.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 (127) hide show
  1. package/dist/{adapter-BjxEv8oW.d.mts → adapter-CEt1MEdF.d.mts} +120 -10
  2. package/dist/adapter-CEt1MEdF.d.mts.map +1 -0
  3. package/dist/{app-module-BwYtACD6.d.mts → app-module-BSpHKlhE.d.mts} +149 -28
  4. package/dist/app-module-BSpHKlhE.d.mts.map +1 -0
  5. package/dist/{application-Bo3jSfmL.d.mts → application-Bzdzl5wP.d.mts} +3 -3
  6. package/dist/{application-Bo3jSfmL.d.mts.map → application-Bzdzl5wP.d.mts.map} +1 -1
  7. package/dist/{application-TiHVcGRL.mjs → application-C-r79XqD.mjs} +3 -3
  8. package/dist/{application-TiHVcGRL.mjs.map → application-C-r79XqD.mjs.map} +1 -1
  9. package/dist/{bootstrap-DW2yzKh6.mjs → bootstrap-C1VGQQVF.mjs} +3 -3
  10. package/dist/{bootstrap-DW2yzKh6.mjs.map → bootstrap-C1VGQQVF.mjs.map} +1 -1
  11. package/dist/{bootstrap-DUUxZXvF.d.mts → bootstrap-DYexY169.d.mts} +2 -2
  12. package/dist/{bootstrap-DUUxZXvF.d.mts.map → bootstrap-DYexY169.d.mts.map} +1 -1
  13. package/dist/config/index.mjs +2 -2
  14. package/dist/{config-BTDTXaeZ.mjs → config-BeIZJBXA.mjs} +3 -3
  15. package/dist/{config-BTDTXaeZ.mjs.map → config-BeIZJBXA.mjs.map} +1 -1
  16. package/dist/{container-Da9BLMHy.mjs → container-CBaL5lus.mjs} +3 -3
  17. package/dist/{container-Da9BLMHy.mjs.map → container-CBaL5lus.mjs.map} +1 -1
  18. package/dist/context-BS5o1IuU.mjs +12 -0
  19. package/dist/context-BS5o1IuU.mjs.map +1 -0
  20. package/dist/context-errors-CwD27Vbj.mjs +13 -0
  21. package/dist/context-errors-CwD27Vbj.mjs.map +1 -0
  22. package/dist/contributor-runner-Cu13cDQp.mjs +12 -0
  23. package/dist/contributor-runner-Cu13cDQp.mjs.map +1 -0
  24. package/dist/core/adapter.d.mts +1 -1
  25. package/dist/core/adapter.mjs +1 -1
  26. package/dist/core/app-module.d.mts +1 -1
  27. package/dist/core/app-module.mjs +1 -1
  28. package/dist/core/cache.mjs +1 -1
  29. package/dist/core/container.mjs +2 -2
  30. package/dist/core/cron.mjs +2 -2
  31. package/dist/core/decorators.mjs +2 -2
  32. package/dist/core/errors.mjs +1 -1
  33. package/dist/core/interfaces.mjs +1 -1
  34. package/dist/core/logger.d.mts +2 -2
  35. package/dist/core/logger.mjs +2 -2
  36. package/dist/core/logger.mjs.map +1 -1
  37. package/dist/core/path.mjs +1 -1
  38. package/dist/core/plugin.d.mts +1 -1
  39. package/dist/core/plugin.mjs +1 -1
  40. package/dist/core/reactivity.mjs +1 -1
  41. package/dist/core-TDR0XGzO.mjs +12 -0
  42. package/dist/core-TDR0XGzO.mjs.map +1 -0
  43. package/dist/{csrf-Dlo-P8Il.mjs → csrf-DNh_7aOk.mjs} +2 -2
  44. package/dist/{csrf-Dlo-P8Il.mjs.map → csrf-DNh_7aOk.mjs.map} +1 -1
  45. package/dist/{disposables-rPSyh69r.mjs → disposables-mnjJuaVx.mjs} +2 -2
  46. package/dist/{disposables-rPSyh69r.mjs.map → disposables-mnjJuaVx.mjs.map} +1 -1
  47. package/dist/{env-Ce-aYRxQ.mjs → env-Dgtjncd5.mjs} +3 -3
  48. package/dist/{env-Ce-aYRxQ.mjs.map → env-Dgtjncd5.mjs.map} +1 -1
  49. package/dist/{error-handler-3ih4E-QZ.d.mts → error-handler-DR01K_rI.d.mts} +5 -1
  50. package/dist/error-handler-DR01K_rI.d.mts.map +1 -0
  51. package/dist/handler-UvmjQGYc.mjs +13 -0
  52. package/dist/handler-UvmjQGYc.mjs.map +1 -0
  53. package/dist/http/application.d.mts +1 -1
  54. package/dist/http/application.mjs +2 -2
  55. package/dist/http/bootstrap.d.mts +1 -1
  56. package/dist/http/bootstrap.mjs +2 -2
  57. package/dist/http/context.d.mts +2 -2
  58. package/dist/http/context.mjs +2 -2
  59. package/dist/http/middleware/cors.mjs +1 -1
  60. package/dist/http/middleware/csrf.mjs +2 -2
  61. package/dist/http/middleware/error-handler.d.mts +1 -1
  62. package/dist/http/middleware/error-handler.mjs +3 -2
  63. package/dist/http/middleware/error-handler.mjs.map +1 -1
  64. package/dist/http/middleware/helmet.mjs +1 -1
  65. package/dist/http/middleware/rate-limit.mjs +2 -2
  66. package/dist/http/middleware/request-id.mjs +1 -1
  67. package/dist/http/middleware/request-logger.mjs +2 -2
  68. package/dist/http/middleware/session.mjs +2 -2
  69. package/dist/http/middleware/spa.d.mts +1 -1
  70. package/dist/http/middleware/spa.mjs +2 -2
  71. package/dist/http/middleware/upload.mjs +2 -2
  72. package/dist/http/middleware/validate.mjs +1 -1
  73. package/dist/http/middleware/views.d.mts +1 -1
  74. package/dist/http/middleware/views.mjs +2 -2
  75. package/dist/http/query/index.mjs +2 -2
  76. package/dist/http/router-builder.d.mts +1 -1
  77. package/dist/http/router-builder.mjs +2 -2
  78. package/dist/http/runtimes/fastify.d.mts +1 -1
  79. package/dist/http/runtimes/fastify.mjs +2 -2
  80. package/dist/http/runtimes/h3-web.d.mts +1 -1
  81. package/dist/http/runtimes/h3-web.d.mts.map +1 -1
  82. package/dist/http/runtimes/h3-web.mjs +2 -2
  83. package/dist/http/runtimes/h3-web.mjs.map +1 -1
  84. package/dist/http/runtimes/h3.d.mts +1 -1
  85. package/dist/http/runtimes/h3.mjs +2 -2
  86. package/dist/index.d.mts +11 -11
  87. package/dist/index.mjs +2 -2
  88. package/dist/index.mjs.map +1 -1
  89. package/dist/{kv-stores-CAYc5VKm.mjs → kv-stores-O8HUSwpv.mjs} +2 -2
  90. package/dist/{kv-stores-CAYc5VKm.mjs.map → kv-stores-O8HUSwpv.mjs.map} +1 -1
  91. package/dist/{kv-stores-gQrfH0KQ.d.mts → kv-stores-duYHIEr1.d.mts} +2 -2
  92. package/dist/{kv-stores-gQrfH0KQ.d.mts.map → kv-stores-duYHIEr1.d.mts.map} +1 -1
  93. package/dist/{logger-COlEkn_C.d.mts → logger-DA43DvNY.d.mts} +18 -3
  94. package/dist/logger-DA43DvNY.d.mts.map +1 -0
  95. package/dist/{metadata-BhKXlN2F.mjs → metadata-CUDfhy4e.mjs} +2 -2
  96. package/dist/{metadata-BhKXlN2F.mjs.map → metadata-CUDfhy4e.mjs.map} +1 -1
  97. package/dist/{module-registry-CvP9mee-.mjs → module-registry-Mv5FqVlq.mjs} +2 -2
  98. package/dist/{module-registry-CvP9mee-.mjs.map → module-registry-Mv5FqVlq.mjs.map} +1 -1
  99. package/dist/{query-Cs_Llg77.mjs → query-gMAVXzx2.mjs} +2 -2
  100. package/dist/{query-Cs_Llg77.mjs.map → query-gMAVXzx2.mjs.map} +1 -1
  101. package/dist/{reply-edT8RGBB.mjs → reply-BMBvs-FB.mjs} +3 -3
  102. package/dist/{reply-edT8RGBB.mjs.map → reply-BMBvs-FB.mjs.map} +1 -1
  103. package/dist/{rolldown-runtime-Bzotz84Y.mjs → rolldown-runtime-CuwksmWW.mjs} +1 -1
  104. package/dist/{router-builder-CiS2ASTJ.d.mts → router-builder-GWgxoOdC.d.mts} +2 -2
  105. package/dist/{router-builder-CiS2ASTJ.d.mts.map → router-builder-GWgxoOdC.d.mts.map} +1 -1
  106. package/dist/{spa-Dp9onKJt.d.mts → spa-DWzSWAzx.d.mts} +2 -2
  107. package/dist/{spa-Dp9onKJt.d.mts.map → spa-DWzSWAzx.d.mts.map} +1 -1
  108. package/dist/{upload-config-C3MSF7RF.mjs → upload-config-CFfHvoHF.mjs} +2 -2
  109. package/dist/{upload-config-C3MSF7RF.mjs.map → upload-config-CFfHvoHF.mjs.map} +1 -1
  110. package/dist/{views-DjgUXRdu.d.mts → views-Kp8Z3AO1.d.mts} +4 -4
  111. package/dist/{views-DjgUXRdu.d.mts.map → views-Kp8Z3AO1.d.mts.map} +1 -1
  112. package/dist/web.d.mts +3 -3
  113. package/dist/web.d.mts.map +1 -1
  114. package/dist/web.mjs +2 -2
  115. package/package.json +1 -1
  116. package/dist/adapter-BjxEv8oW.d.mts.map +0 -1
  117. package/dist/app-module-BwYtACD6.d.mts.map +0 -1
  118. package/dist/context-Cs0939u1.mjs +0 -12
  119. package/dist/context-Cs0939u1.mjs.map +0 -1
  120. package/dist/contributor-runner-B3Z3Q7J5.mjs +0 -13
  121. package/dist/contributor-runner-B3Z3Q7J5.mjs.map +0 -1
  122. package/dist/core-CQ6u5kOq.mjs +0 -12
  123. package/dist/core-CQ6u5kOq.mjs.map +0 -1
  124. package/dist/error-handler-3ih4E-QZ.d.mts.map +0 -1
  125. package/dist/handler-D8Zyie-B.mjs +0 -12
  126. package/dist/handler-D8Zyie-B.mjs.map +0 -1
  127. package/dist/logger-COlEkn_C.d.mts.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-TDR0XGzO.mjs","names":["mergeConfig","composeName","mergeConfig","composeName"],"sources":["../src/core/define-module.ts","../src/core/define-modules.ts","../src/core/circuit-breaker.ts","../src/core/context-decorator.ts","../src/core/mount-sort.ts","../src/core/define-plugin.ts","../src/core/define-adapter.ts","../src/core/augmentation.ts","../src/core/asset-keys.ts","../src/core/assets.ts"],"sourcesContent":["import type { AppModule } from './app-module'\nimport type { BuildContext } from './define-plugin'\n\n/**\n * Options passed to {@link defineModule}. Mirrors {@link DefineAdapterOptions}\n * and {@link DefinePluginOptions} so adopters learn one mental model\n * across all three primitives.\n *\n * The optional `TExtra` generic lets a module ship public methods\n * beyond the standard {@link AppModule} contract — they're preserved\n * on the returned module instance so external callers (tests, sibling\n * modules) can invoke them directly.\n */\nexport interface DefineModuleOptions<TConfig, TExtra = unknown> {\n /**\n * Stable identity surfaced in diagnostics, route logs, and\n * `.scoped()` namespacing. Required because boot-time error\n * messages reference modules by name — anonymous modules make\n * cycles + missing-dep errors hard to localize.\n */\n name: string\n /** Optional version string surfaced in diagnostics. */\n version?: string\n /**\n * Optional kickjs version constraint. Enforced by the framework\n * when implemented; today it lives on the definition for\n * documentation and forward compatibility.\n */\n requires?: { kickjs?: string }\n /**\n * Default config merged with overrides at instantiation time\n * (`MyModule({ field: x })`). Call-site overrides win.\n */\n defaults?: Partial<TConfig>\n /**\n * Build the AppModule shape from the resolved config and context.\n * `ctx.name` is the resolved instance name (the bare definition\n * name for the default call, or `${name}:${scope}` for\n * `.scoped()`); `ctx.scoped` discriminates so the build function\n * can branch on namespacing if needed.\n *\n * Return any extension methods alongside the standard\n * {@link AppModule} surface — they're typed via `TExtra` and stay\n * accessible on the resulting instance.\n */\n build(config: TConfig, ctx: BuildContext): AppModule & TExtra\n}\n\n/**\n * Factory returned by {@link defineModule}. Mirrors {@link AdapterFactory}\n * — call (`MyModule({...})`), scope (`MyModule.scoped('public', {...})`),\n * inspect (`MyModule.definition`).\n *\n * `.async()` is intentionally **not** part of the module factory\n * surface. Module config has no async-resolution window: `register()`\n * and `routes()` both run synchronously during bootstrap, before any\n * adapter `beforeStart` hook fires. Adopters that need async-resolved\n * config push it into an adapter (which has `beforeStart`), then\n * inject the resolved value into the module via DI tokens.\n */\nexport interface ModuleFactory<TConfig, TExtra = unknown> {\n (config?: Partial<TConfig>): AppModule & TExtra\n scoped(scopeName: string, config?: Partial<TConfig>): AppModule & TExtra\n readonly definition: Readonly<DefineModuleOptions<TConfig, TExtra>>\n}\n\nconst mergeConfig = <TConfig>(\n defaults: Partial<TConfig> = {},\n overrides: Partial<TConfig> = {},\n): TConfig => ({ ...defaults, ...overrides }) as TConfig\n\nconst composeName = (base: string, scope: string): string => `${base}:${scope}`\n\n/**\n * Build an {@link AppModule} factory from a typed definition. Pairs\n * with {@link defineAdapter}, {@link definePlugin}, and\n * {@link defineContextDecorator} as the fourth `define*` primitive\n * — adopters write modules the same way they write adapters and\n * plugins.\n *\n * The returned factory produces real {@link AppModule} instances\n * that drop into `bootstrap({ modules })` directly. The legacy\n * class form (`class TasksModule implements AppModule { ... }`) keeps\n * working — `bootstrap` accepts either shape.\n *\n * @example\n * ```ts\n * interface TasksConfig {\n * scope: 'public' | 'admin'\n * }\n *\n * const TasksModule = defineModule<TasksConfig>({\n * name: 'TasksModule',\n * defaults: { scope: 'public' },\n * build: (config, { name }) => ({\n * register(container) {\n * container.registerInstance(`tasks:scope:${name}`, config.scope)\n * },\n * routes() {\n * return { path: `/${config.scope}/tasks`, router: buildTasksRouter() }\n * },\n * contributors() {\n * return [LoadTenant.registration]\n * },\n * }),\n * })\n *\n * bootstrap({\n * modules: [\n * TasksModule(), // public scope (defaults)\n * TasksModule.scoped('admin', { scope: 'admin' }),\n * ],\n * })\n * ```\n */\nexport function defineModule<TConfig = Record<string, unknown>, TExtra = unknown>(\n options: DefineModuleOptions<TConfig, TExtra>,\n): ModuleFactory<TConfig, TExtra> {\n if (options === null || typeof options !== 'object') {\n throw new TypeError(\n 'defineModule: options must be an object literal. ' +\n 'See architecture.md §21 for the spec shape.',\n )\n }\n if (typeof options.name !== 'string' || options.name.length === 0) {\n const got = typeof options.name === 'string' ? '\"\"' : typeof options.name\n throw new TypeError(\n `defineModule: options.name must be a non-empty string (got ${got}). ` +\n 'Module names appear in diagnostics and `.scoped()` namespacing — anonymous ' +\n 'modules make cycle / missing-dep errors hard to localize.',\n )\n }\n if (typeof options.build !== 'function') {\n throw new TypeError(\n `defineModule(${options.name}): options.build is required and must be a function ` +\n `(got ${typeof options.build}). The build function returns the AppModule shape — ` +\n 'register() / routes() / contributors() — wired with the resolved config.',\n )\n }\n\n const buildSync = (\n instanceName: string,\n scoped: boolean,\n overrides?: Partial<TConfig>,\n ): AppModule & TExtra => {\n const config = mergeConfig(options.defaults, overrides)\n // Mutate-in-place rather than spread: spread strips prototype methods\n // when build() returns a class instance, and the existing defineAdapter\n // pattern uses the same approach. `TExtra` properties survive because\n // they're own properties on the build result.\n const built = options.build(config, { name: instanceName, scoped })\n // AppModule has no `name` field today, but stamping the resolved\n // instance name onto the result lets diagnostics surface it without\n // adopter wiring. The cast keeps the public AppModule type\n // unchanged while the data is available for any framework code\n // that wants it.\n Object.defineProperty(built, '__moduleName', {\n value: instanceName,\n enumerable: false,\n writable: false,\n configurable: false,\n })\n return built\n }\n\n const factory = ((config?: Partial<TConfig>) =>\n buildSync(options.name, false, config)) as ModuleFactory<TConfig, TExtra>\n\n factory.scoped = (scopeName: string, config?: Partial<TConfig>) =>\n buildSync(composeName(options.name, scopeName), true, config)\n\n Object.defineProperty(factory, 'definition', {\n value: Object.freeze({ ...options }),\n writable: false,\n enumerable: true,\n configurable: false,\n })\n\n return factory\n}\n","import type { AppModuleEntry } from './app-module'\n\n/**\n * Chainable registry returned by {@link defineModules}. Extends\n * `Array<AppModuleEntry>` so it drops into `bootstrap({ modules })`\n * unchanged — the framework loader already iterates the array, so a\n * `ModuleList` IS a valid `modules` value with no special handling.\n *\n * The added affordance is `.mount(module)` returning `this` so\n * adopters can build the list fluently:\n *\n * ```ts\n * const modules = defineModules()\n * .mount(HelloModule())\n * .mount(AdminModule())\n *\n * bootstrap({ modules })\n * ```\n *\n * **Why not just use a plain array?** Two reasons. First, the\n * fluent shape reads more naturally for module-list construction —\n * adopters used to `definePlugin().scoped(...)` / `defineAdapter(...)`\n * see the same call-then-call pattern. Second, the registry is the\n * stable extension point: a future `.use(module)` for non-HTTP\n * modules (queues, cron, workers) lands as a method on `ModuleList`\n * without breaking the `Array<AppModuleEntry>` superclass contract.\n */\nexport class ModuleList extends Array<AppModuleEntry> {\n /**\n * Append a module to the list. Returns `this` for chaining.\n *\n * Accepts both class form (`SomeModule extends AppModule`) and\n * `defineModule` factory output — same as the underlying\n * `bootstrap({ modules })` array. The framework loader\n * discriminates `typeof entry === 'function'` at boot.\n */\n mount(module: AppModuleEntry): this {\n this.push(module)\n return this\n }\n}\n\n/**\n * Build a {@link ModuleList} for the application. Optional vararg\n * lets adopters seed the list inline; subsequent `.mount()` calls\n * append.\n *\n * @example\n * ```ts\n * import { bootstrap, defineModules } from '@forinda/kickjs'\n * import { HelloModule } from './modules/hello/hello.module'\n * import { AdminModule } from './modules/admin/admin.module'\n *\n * // Fluent — most common\n * const modules = defineModules()\n * .mount(HelloModule())\n * .mount(AdminModule())\n *\n * // Or seeded inline + chained\n * const modules2 = defineModules(HelloModule()).mount(AdminModule())\n *\n * await bootstrap({ modules })\n * ```\n *\n * The returned value is an `AppModuleEntry[]` subclass — `bootstrap`\n * accepts it directly without any special-casing. For conditional /\n * dynamic registration at bootstrap time (env flags, runtime config,\n * tenant lists), use the `setup(registry)` callback on `bootstrap`\n * instead — both surfaces compose.\n */\nexport function defineModules(...initial: AppModuleEntry[]): ModuleList {\n const list = new ModuleList()\n for (const entry of initial) list.push(entry)\n return list\n}\n","/**\n * Circuit Breaker pattern for external service calls.\n *\n * Protects your application from cascading failures when downstream\n * services are unhealthy by short-circuiting requests after a\n * configurable failure threshold.\n *\n * @example\n * ```ts\n * const breaker = new CircuitBreaker('payment-api', {\n * failureThreshold: 5,\n * resetTimeout: 30_000,\n * })\n *\n * const result = await breaker.execute(() => fetch('https://payment.example.com/charge'))\n * ```\n */\n\nexport type CircuitBreakerState = 'closed' | 'open' | 'half_open'\n\nexport interface CircuitBreakerOptions {\n /** Number of consecutive failures before the circuit opens. */\n failureThreshold: number\n /** Milliseconds to wait before transitioning from OPEN to HALF_OPEN. */\n resetTimeout: number\n /** Max requests allowed in HALF_OPEN state before deciding (default 1). */\n halfOpenMax?: number\n}\n\nexport interface CircuitBreakerStats {\n failures: number\n successes: number\n state: CircuitBreakerState\n lastFailure?: Date\n}\n\n/**\n * Error thrown when the circuit is open and calls are being rejected.\n */\nexport class CircuitOpenError extends Error {\n constructor(public readonly breakerName: string) {\n super(`Circuit breaker \"${breakerName}\" is open — request rejected`)\n this.name = 'CircuitOpenError'\n }\n}\n\nexport class CircuitBreaker {\n private state: CircuitBreakerState = 'closed'\n private failures = 0\n private successes = 0\n private halfOpenAttempts = 0\n private lastFailure?: Date\n private openedAt?: number\n\n private readonly failureThreshold: number\n private readonly resetTimeout: number\n private readonly halfOpenMax: number\n\n constructor(\n public readonly name: string,\n options: CircuitBreakerOptions,\n ) {\n this.failureThreshold = options.failureThreshold\n this.resetTimeout = options.resetTimeout\n this.halfOpenMax = options.halfOpenMax ?? 1\n }\n\n /**\n * Execute an async function through the circuit breaker.\n * Throws `CircuitOpenError` if the circuit is open.\n */\n async execute<T>(fn: () => Promise<T>): Promise<T> {\n this.maybeTransitionToHalfOpen()\n\n if (this.state === 'open') {\n throw new CircuitOpenError(this.name)\n }\n\n if (this.state === 'half_open' && this.halfOpenAttempts >= this.halfOpenMax) {\n throw new CircuitOpenError(this.name)\n }\n\n if (this.state === 'half_open') {\n this.halfOpenAttempts++\n }\n\n try {\n const result = await fn()\n this.onSuccess()\n return result\n } catch (error) {\n this.onFailure()\n throw error\n }\n }\n\n /** Returns the current circuit state. */\n getState(): CircuitBreakerState {\n this.maybeTransitionToHalfOpen()\n return this.state\n }\n\n /** Returns current statistics. */\n getStats(): CircuitBreakerStats {\n this.maybeTransitionToHalfOpen()\n return {\n failures: this.failures,\n successes: this.successes,\n state: this.state,\n ...(this.lastFailure ? { lastFailure: this.lastFailure } : {}),\n }\n }\n\n /** Manually reset the circuit breaker to CLOSED state. */\n reset(): void {\n this.state = 'closed'\n this.failures = 0\n this.successes = 0\n this.halfOpenAttempts = 0\n this.lastFailure = undefined\n this.openedAt = undefined\n }\n\n private onSuccess(): void {\n if (this.state === 'half_open') {\n // Recovery confirmed — close the circuit\n this.state = 'closed'\n this.failures = 0\n this.halfOpenAttempts = 0\n this.openedAt = undefined\n }\n this.successes++\n }\n\n private onFailure(): void {\n this.failures++\n this.lastFailure = new Date()\n\n if (this.state === 'half_open') {\n // Recovery failed — re-open\n this.state = 'open'\n this.openedAt = Date.now()\n this.halfOpenAttempts = 0\n } else if (this.state === 'closed' && this.failures >= this.failureThreshold) {\n this.state = 'open'\n this.openedAt = Date.now()\n }\n }\n\n private maybeTransitionToHalfOpen(): void {\n if (this.state === 'open' && this.openedAt != null) {\n if (Date.now() - this.openedAt >= this.resetTimeout) {\n this.state = 'half_open'\n this.halfOpenAttempts = 0\n }\n }\n }\n}\n","import 'reflect-metadata'\nimport { METADATA, type Constructor, type MaybePromise } from './interfaces'\nimport { pushClassMeta, pushMethodMeta } from './metadata'\nimport type { InjectionToken } from './token'\nimport type { ContextKeys, ContextMeta, ExecutionContext, MetaValue } from './execution-context'\n\n/**\n * String-literal union of every known context key — the union of the\n * keys declared on {@link ContextKeys} (key-only registry) and\n * {@link ContextMeta} (value-type registry). Resolves to `string` when\n * the project hasn't augmented either yet (so a first-day project keeps\n * compiling), and narrows to the concrete keys once the\n * `declare module '@forinda/kickjs' { interface ContextKeys/ContextMeta { ... } }`\n * blocks land.\n *\n * Used as the element type of `dependsOn`, so a typo like\n * `dependsOn: ['tenent']` becomes a TS error instead of a boot-time\n * `MissingContributorError`.\n *\n * Unioning BOTH registries is deliberate: it means adding a value type\n * via `ContextMeta` automatically makes that key a valid `dependsOn`\n * target, and you can register a dependsOn-able key in `ContextKeys`\n * without being forced to give it a value type. (Before the split,\n * `dependsOn` was keyed off `ContextMeta` alone, so augmenting\n * `ContextMeta` for some keys broke `dependsOn` for every key you\n * hadn't added there.)\n */\ntype KnownContextKey = keyof ContextMeta | keyof ContextKeys\nexport type ContextMetaKey = [KnownContextKey] extends [never] ? string : KnownContextKey & string\n\n/**\n * What a single `deps` entry is allowed to be — the runtime calls\n * `container.resolve(value)`, which accepts an injection token brand\n * or a constructor (decorated class). Anything else (a string literal,\n * an array, a plain object) silently fails at boot when the runner\n * tries to resolve it; constraining at the type level surfaces the\n * mistake in the editor instead.\n */\nexport type DepValue = InjectionToken<unknown> | Constructor<unknown>\n\n/**\n * Maps a `deps` declaration object to its resolved-instance shape.\n *\n * `deps: { db: DB_TOKEN, cache: CacheService }` produces\n * `{ db: Db, cache: CacheService }` — the runner resolves each value\n * against the DI container and hands the result to `resolve()`.\n */\nexport type ResolvedDeps<D extends Record<string, DepValue>> = {\n [K in keyof D]: D[K] extends InjectionToken<infer T>\n ? T\n : D[K] extends Constructor<infer T>\n ? T\n : never\n}\n\n/**\n * Keys of `T` that are declared required (i.e. not `?`-optional).\n */\ntype RequiredParamKeys<T> = {\n // eslint-disable-next-line @typescript-eslint/ban-types\n [K in keyof T]-?: {} extends Pick<T, K> ? never : K\n}[keyof T]\n\n/**\n * Params a call site MUST supply: the required keys of `P` that\n * `paramDefaults` (type `PD`) doesn't already provide.\n *\n * The `string extends keyof P` guard short-circuits the index-signature\n * case — `P` defaults to `Record<string, never>` for the overwhelmingly\n * common zero-params contributor, and a mapped type over `string` would\n * otherwise report \"every key is required\" and break `@Foo` for every\n * existing decorator in the wild.\n */\nexport type MissingParamKeys<P, PD> = string extends keyof P\n ? never\n : Exclude<RequiredParamKeys<P>, keyof PD>\n\n/**\n * The argument type for a call site (`@Foo({...})`, `.with({...})`).\n *\n * `Partial<P>` when every required field has a default, so adopters keep\n * overriding just the fields they care about. Otherwise `Partial<P>`\n * intersected with the undefaulted required fields, making those\n * mandatory at each call site.\n */\nexport type CallSiteParams<P, PD> = [MissingParamKeys<P, PD>] extends [never]\n ? Partial<P>\n : Partial<P> & Pick<P, MissingParamKeys<P, PD> & keyof P>\n\n/**\n * Spec passed to {@link defineContextDecorator}. Describes one entry in\n * the per-request contributor pipeline.\n *\n * @typeParam K - The {@link ContextMeta} key this contributor populates.\n * @typeParam D - DI dependency declaration shape (see {@link ResolvedDeps}).\n * @typeParam Ctx - Concrete execution-context type. Defaults to the abstract\n * {@link ExecutionContext}; HTTP contributors typically\n * default to `RequestContext` at the call site.\n * @typeParam PD - Inferred from `paramDefaults`. Drives which params each\n * call site is required to supply — see {@link CallSiteParams}.\n * Never spell this by hand.\n */\nexport interface ContextDecoratorSpec<\n K extends string = string,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Partial<P>,\n> {\n /** ContextMeta key the resolved value is written to. */\n key: K\n /**\n * DI dependencies resolved by the runner before `resolve()` runs.\n * Map entry keys become property names on the resolved-deps argument\n * passed to `resolve()`.\n */\n deps?: D\n /**\n * Other contributor keys that must populate before this one runs.\n * Enforced via topo-sort at startup; missing deps and cycles fail boot.\n *\n * Typed against {@link ContextMetaKey} — the union of `keyof ContextMeta`\n * and `keyof ContextKeys` — once the project augments either, so\n * `dependsOn: ['tenent']` (typo) is a TS error, not a boot-time\n * `MissingContributorError`. Falls back to plain `string[]` when both\n * registries are empty (no augmentation yet) so first-day projects keep\n * compiling. Cast escape hatch: `dependsOn: ['custom-key' as ContextMetaKey]`.\n */\n dependsOn?: readonly ContextMetaKey[]\n /**\n * If `true`, a thrown `resolve` skips the contributor instead of\n * forwarding to the request error handler. The key is left unset\n * and downstream consumers see `ctx.get(key) === undefined`.\n */\n optional?: boolean\n /**\n * Default per-call params merged with the call-site params; call-site\n * wins. When omitted, params default to `{}` and the resolver\n * receives an empty object — back-compat with today's zero-arg\n * decorators.\n *\n * Param shape is intentionally unconstrained: plain data, functions,\n * and closures are all valid. Adopters who want runtime validation\n * should perform it inside `resolve()` — keeps the framework\n * validator-agnostic so projects can plug Zod / Valibot / hand-rolled\n * checks without the framework caring.\n *\n * **Required fields are enforced at the call site.** A required field\n * of `P` that has no entry here must be supplied wherever the\n * decorator is applied — `@Foo({ action: 'audit:read' })`. The bare\n * `@Foo` form and `.registration` stop type-checking for such a\n * decorator, because neither can supply the value.\n *\n * This exists so nobody has to invent a meaningless default just to\n * satisfy the type (`action: 'settings:read'` on a permission\n * contributor every call site overrides anyway). A default that is\n * never correct is worse than no default: forget the argument at one\n * call site and the route silently gates on the placeholder instead\n * of failing to compile.\n *\n * Omit this entirely for the common zero-params case — `P` defaults\n * to `Record<string, never>`, which requires nothing.\n */\n paramDefaults?: PD\n /**\n * Runtime mirror of the compile-time requirement above: param names\n * that must be present after merging call-site params over\n * `paramDefaults`. A missing one throws `TypeError` at the point of\n * use (decoration time for `@Foo`, call time for `@Foo({...})` /\n * `.with({...})`), naming the decorator and the field.\n *\n * The type-level check already covers TypeScript call sites, so this\n * is for the paths types can't reach: plain-JS adopters, `as any`\n * escapes, and params assembled dynamically. Optional — list only\n * fields whose absence is a correctness bug rather than a default.\n */\n requiredParams?: readonly (keyof P & string)[]\n /**\n * Hook invoked when `resolve()` throws and `optional` is `false`.\n * Returning a value writes it to `ctx.set(key, …)`; returning\n * `undefined` / `void` skips. Throwing inside the hook forwards\n * the new error to the request error handler.\n *\n * Async-permitted by design — adopters frequently want to\n * `await auditService.log(err)` or `await cache.fallback(...)`.\n *\n * The third `params` argument carries the resolved per-call params\n * (call-site overrides merged on top of `paramDefaults`).\n */\n onError?: (err: unknown, ctx: Ctx, params: P) => MaybePromise<MetaValue<K> | undefined | void>\n /**\n * Compute and return the value to write into `ctx.set(key, …)`.\n *\n * The third `params` argument carries the resolved per-call params\n * (call-site overrides merged on top of `paramDefaults`). When the\n * decorator is applied zero-arg (`@Foo`), `params` equals\n * `paramDefaults` (or `{}` if defaults are omitted). When applied as\n * a factory (`@Foo({ ... })`), the call-site object is merged on top.\n */\n resolve: (ctx: Ctx, deps: ResolvedDeps<D>, params: P) => MaybePromise<MetaValue<K>>\n}\n\n/**\n * Internal, normalised registration produced by {@link defineContextDecorator}.\n *\n * Frozen and shared by every use site of the decorator (method, class,\n * module, adapter, global). The pipeline builder dedupes by `key` using\n * the precedence rule defined in §20.4 of `architecture.md`.\n */\nexport interface ContributorRegistration<\n K extends string = string,\n D extends Record<string, DepValue> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n> {\n readonly key: K\n readonly deps: D\n readonly dependsOn: readonly ContextMetaKey[]\n readonly optional: boolean\n /**\n * Per-call params from the spec are baked into the closure at\n * decorator-construction time, so the runner-facing `onError`\n * exposes only `(err, ctx)`.\n */\n readonly onError?: (err: unknown, ctx: Ctx) => MaybePromise<MetaValue<K> | undefined | void>\n /**\n * Per-call params from the spec are baked into the closure at\n * decorator-construction time, so the runner-facing `resolve`\n * exposes only `(ctx, deps)`. Adopters who write their own\n * registrations (without `defineContextDecorator`) match this\n * shape directly — params are an implementation detail of the\n * decorator factory, not of the runtime pipeline.\n */\n readonly resolve: (ctx: Ctx, deps: ResolvedDeps<D>) => MaybePromise<MetaValue<K>>\n /**\n * Stack snapshot captured the moment the decorator was defined.\n * Used by the contributor-pipeline builder to point boot-time\n * errors (`MissingContributorError`, `ContributorCycleError`,\n * `DuplicateContributorError`) at the adopter file that declared\n * the broken contributor — instead of forcing them to grep for\n * the key string.\n *\n * Optional because adopters who hand-roll a `ContributorRegistration`\n * (without `defineContextDecorator`) may not have a useful stack to\n * attach. Format is the raw `Error.stack` string; consumers slice\n * the first non-framework frame for display.\n */\n readonly definedAt?: string\n}\n\n/**\n * Type-erased registration shape used in the public collection slots\n * (`AppModule.contributors?()`, `AppAdapter.contributors?()`,\n * `KickPlugin.contributors?()`, `bootstrap({ contributors })`).\n *\n * The narrow `ContributorRegistration` carries a `Ctx` parameter so\n * `resolve(ctx, deps)` is typed at the *definition* site, but `Ctx`\n * sits in a contravariant position on `resolve` — meaning a\n * `ContributorRegistration<…, RequestContext>` is *not* assignable to\n * `ContributorRegistration<…, ExecutionContext>` even though every\n * `RequestContext` is-a `ExecutionContext`. That's TS-sound but\n * useless: the runner only ever calls a contributor with the same\n * concrete ctx the route mounts under, so an HTTP-typed contributor\n * never sees a non-HTTP ctx. Erasing `Ctx` to `any` in the collection\n * type lets adopters store contributors typed against `RequestContext`\n * (or any future transport-specific ctx) in the same array without\n * casting.\n */\nexport type AnyContributorRegistration = ContributorRegistration<\n string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any\n>\n\n/** Convenience alias for `AnyContributorRegistration[]` collections. */\nexport type ContributorRegistrations =\n | AnyContributorRegistration[]\n | readonly AnyContributorRegistration[]\n\n/**\n * The decorator function returned by either a zero-arg\n * {@link ContextDecorator} application or a factory call. Overloaded\n * to satisfy both class and method decorator shapes — adopters apply\n * the same decorator to either site without TS rejecting the call.\n */\nexport interface ContextDecoratorTarget {\n /** Class decorator usage. */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n (target: Function): void\n /** Method decorator usage. */\n (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void\n}\n\n/**\n * The value returned by {@link defineContextDecorator}.\n *\n * Resolves to one of two shapes depending on whether every required\n * field of `P` has a `paramDefaults` entry:\n *\n * - {@link ContextDecoratorWithDefaults} — the classic shape. Usable\n * bare (`@Foo`), as a factory (`@Foo({...})`), via `.with(params)`,\n * and via `.registration`.\n * - {@link ContextDecoratorRequiringParams} — when `P` has required\n * fields `paramDefaults` doesn't cover. Only the factory and\n * `.with(params)` forms exist, because the others have no way to\n * supply the missing values. Applying such a decorator bare is a\n * compile error rather than a silent `undefined` at request time.\n *\n * `PD` is inferred from the spec's `paramDefaults`; the default here\n * (`Partial<P>`) keeps hand-written `ContextDecorator<K, D, P>`\n * annotations resolving to the permissive shape as before.\n */\nexport type ContextDecorator<\n K extends string = string,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD = Partial<P>,\n> = [MissingParamKeys<P, PD>] extends [never]\n ? ContextDecoratorWithDefaults<K, D, P, Ctx>\n : ContextDecoratorRequiringParams<K, D, P, Ctx, PD>\n\n/**\n * Decorator whose params are fully defaulted (or which takes none).\n * Callable as a class or method decorator directly, as a factory, or\n * via `.with(params)` / `.registration` at non-decorator registration\n * sites.\n */\nexport interface ContextDecoratorWithDefaults<\n K extends string = string,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n> {\n /**\n * Class decorator usage with `paramDefaults` — `@Foo class C {}`.\n * The class constructor is a `Function`, so this overload only\n * matches actual classes (not plain object literals — which\n * fall through to the factory overload below).\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n (target: Function): void\n /**\n * Method decorator usage with `paramDefaults` — `@Foo` over a\n * method. The required `propertyKey` distinguishes this from the\n * factory overload's single-object-arg call shape.\n */\n (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void\n /**\n * Zero-arg factory usage — `@Foo()` returns a decorator with\n * `paramDefaults` applied. Equivalent in behaviour to the bare\n * `@Foo` form, exposed here so adopters who prefer \"always call the\n * factory\" don't need an `as any` cast.\n */\n (): ContextDecoratorTarget\n /**\n * Factory usage — `@Foo({ ... })` returns the actual decorator with\n * call-site params merged onto `paramDefaults`. Matches a single\n * params object; plain objects don't satisfy the `Function` /\n * 2-arg-minimum constraints above, so TS picks this overload.\n *\n * Accepts `Partial<P>` so adopters can override only the fields\n * they care about — anything missing falls back to `paramDefaults`\n * at runtime. Inside `resolve()`, `params` is the fully merged\n * `P`, never a `Partial<P>`.\n *\n * The returned decorator is overloaded for class AND method usage —\n * `@Foo({...}) class C {}` and `@Foo({...}) m() {}` both type-check.\n */\n (params: Partial<P>): ContextDecoratorTarget\n /**\n * Bare registration — uses `paramDefaults` only. Use this at non-\n * decorator registration sites (module / adapter / bootstrap) when\n * the call site doesn't need to override params.\n */\n readonly registration: ContributorRegistration<K, D, Ctx>\n /**\n * Build a registration with call-site params merged onto\n * `paramDefaults`. Use this at non-decorator registration sites\n * when the call site DOES want to override params:\n *\n * ```ts\n * bootstrap({\n * contributors: [LoadTenant.with({ source: 'subdomain' }).registration],\n * })\n * ```\n *\n * `Partial<P>` for the same reason as the factory overload — the\n * runtime merges with `paramDefaults`, so adopters only specify\n * fields they want to override.\n */\n with(params: Partial<P>): { readonly registration: ContributorRegistration<K, D, Ctx> }\n}\n\n/**\n * Decorator with at least one required param that `paramDefaults`\n * doesn't cover. Deliberately narrower than\n * {@link ContextDecoratorWithDefaults}: the bare `@Foo` form, the\n * zero-arg `@Foo()` form, and the `.registration` accessor are all\n * absent, because none of them can supply the missing value.\n *\n * ```ts\n * const OperatorPerm = defineContextDecorator.withParams<{ action: string }>()({\n * key: 'operatorPerm',\n * resolve: (ctx, _deps, { action }) => check(ctx, action),\n * })\n *\n * class Ops {\n * @OperatorPerm // ✗ compile error — `action` missing\n * @Get('/a')\n * a(ctx: RequestContext) {}\n *\n * @OperatorPerm({ action: 'read' }) // ✓\n * @Get('/b')\n * b(ctx: RequestContext) {}\n * }\n * ```\n *\n * If you want the bare form back, give `action` a default in\n * `paramDefaults` — but only when the default is genuinely correct for\n * an undecorated route, which for a permission string it usually isn't.\n */\nexport interface ContextDecoratorRequiringParams<\n K extends string = string,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD = Partial<P>,\n> {\n /**\n * Factory usage — the only decorator form available here. The\n * argument type carries every required field `paramDefaults` didn't\n * provide, so omitting one is a compile error at this call site.\n */\n (params: CallSiteParams<P, PD>): ContextDecoratorTarget\n /**\n * Build a registration for non-decorator registration sites. Same\n * required-field rule as the factory form.\n */\n with(params: CallSiteParams<P, PD>): {\n readonly registration: ContributorRegistration<K, D, Ctx>\n }\n}\n\n/**\n * Curried params-first helper for {@link defineContextDecorator}.\n *\n * Solves the partial-inference problem: TS generics are positional, so\n * the moment an adopter wants to specify the per-call params shape\n * `P`, they're forced to spell `K` and `D` by hand — losing the\n * automatic `deps` inference that drives the `(ctx, deps, params) =>`\n * resolver signature:\n *\n * ```ts\n * // ❌ Positional generics — must repeat the deps type, and `D` no\n * // longer flows from `spec.deps` into `resolve(ctx, deps, ...)`.\n * defineContextDecorator<'tenant', { repo: typeof TENANT_REPO }, MyParams>({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO },\n * resolve: (ctx, { repo }, params) => ...,\n * })\n *\n * // ✅ Curried form — only `P` is spelled; `K`, `D`, `Ctx` infer.\n * defineContextDecorator.withParams<MyParams>()({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO }, // D inferred\n * resolve: (ctx, { repo }, params) => ..., // deps + params both typed\n * })\n * ```\n *\n * Use this whenever the contributor takes call-site params; use the\n * positional form when there are no params (the common case).\n */\nexport interface DefineContextDecoratorWithParams<P extends Record<string, unknown>> {\n <\n K extends string,\n D extends Record<string, DepValue> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n // Inferred from `spec.paramDefaults`. The `Record<never, never>`\n // default (NOT `Partial<P>`) is what makes an omitted `paramDefaults`\n // mean \"nothing is defaulted\" — so every required field of `P` lands\n // on the call site. `Partial<P>` here would make `keyof PD` cover all\n // of `P` and silently defeat the whole check.\n PD extends Partial<P> = Record<never, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, Ctx, PD>,\n ): ContextDecorator<K, D, P, Ctx, PD>\n}\n\n/**\n * The public shape of {@link defineContextDecorator}. Combines the\n * original positional-generic call signature with the curried\n * `.withParams<P>()` helper that fixes partial-inference for the\n * parameterised case.\n */\nexport interface DefineContextDecoratorFn {\n <\n K extends string,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Record<never, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, Ctx, PD>,\n ): ContextDecorator<K, D, P, Ctx, PD>\n\n /**\n * Curried entry point. Spell only the per-call params shape `P`;\n * `K`, `D`, and `Ctx` are inferred from the spec passed to the\n * returned function. See {@link DefineContextDecoratorWithParams}\n * for the rationale.\n */\n withParams<P extends Record<string, unknown>>(): DefineContextDecoratorWithParams<P>\n}\n\n/**\n * Define a typed context contributor.\n *\n * The returned function works as a method or class decorator:\n *\n * ```ts\n * const LoadTenant = defineContextDecorator({\n * key: 'tenant',\n * deps: { tenants: TenantsRepo },\n * resolve: (ctx, { tenants }) => tenants.findByHost(ctx.requestId),\n * })\n *\n * @LoadTenant\n * @Get('/me')\n * me(ctx: RequestContext) {\n * ctx.get('tenant') // typed via ContextMeta\n * }\n * ```\n *\n * For non-decorator registration (module/adapter/bootstrap), pass\n * `LoadTenant.registration` into the appropriate contributors list.\n *\n * Boot-time validation: a missing `resolve`, an empty `key`, or a\n * non-array `dependsOn` throws `TypeError` here at definition time\n * (typically module-load) rather than waiting for the first request.\n * See `architecture.md` §20 for the full pipeline plan.\n *\n * **Parameterised contributors:** TS generics are positional, so to\n * specify `P` (the per-call params shape) with this signature you\n * also have to spell `K` and `D` — which loses automatic `deps`\n * inference. Use {@link DefineContextDecoratorFn.withParams} instead:\n * `defineContextDecorator.withParams<MyParams>()(spec)`.\n */\nfunction defineContextDecoratorImpl<\n K extends string,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Record<never, never>,\n>(spec: ContextDecoratorSpec<K, D, P, Ctx, PD>): ContextDecorator<K, D, P, Ctx, PD> {\n // ---- Boot-time validation --------------------------------------\n // Surface mistakes the moment `defineContextDecorator` is called\n // (typically module-load time) instead of letting them ride to\n // request time as cryptic ContextMeta misses or container failures.\n // Cheap, ~15 lines, catches a whole class of definition-time bugs.\n if (spec === null || typeof spec !== 'object') {\n throw new TypeError(\n 'defineContextDecorator: spec must be an object literal. ' +\n 'See architecture.md §20.4 for the spec shape.',\n )\n }\n if (typeof spec.key !== 'string' || spec.key.length === 0) {\n const got = typeof spec.key === 'string' ? '\"\"' : typeof spec.key\n throw new TypeError(\n `defineContextDecorator: spec.key must be a non-empty string (got ${got}). ` +\n 'Pick a name that matches a key declared on `ContextMeta` (or one of the framework keys).',\n )\n }\n if (typeof spec.resolve !== 'function') {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.resolve is required and must be a function ` +\n `(got ${typeof spec.resolve}). Each contributor needs a resolver that produces ` +\n `the value to write into ctx.set(${JSON.stringify(spec.key)}, …).`,\n )\n }\n if (spec.onError !== undefined && typeof spec.onError !== 'function') {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.onError must be a function when provided ` +\n `(got ${typeof spec.onError}).`,\n )\n }\n if (spec.dependsOn !== undefined && !Array.isArray(spec.dependsOn)) {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.dependsOn must be an array when provided ` +\n `(got ${typeof spec.dependsOn}).`,\n )\n }\n if (spec.requiredParams !== undefined && !Array.isArray(spec.requiredParams)) {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.requiredParams must be an array when provided ` +\n `(got ${typeof spec.requiredParams}).`,\n )\n }\n if (\n Array.isArray(spec.requiredParams) &&\n spec.requiredParams.some((p) => typeof p !== 'string' || p.length === 0)\n ) {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.requiredParams entries must be non-empty strings.`,\n )\n }\n if (\n Array.isArray(spec.dependsOn) &&\n spec.dependsOn.some((dep) => typeof dep !== 'string' || dep.length === 0)\n ) {\n // TS narrows dependsOn to `readonly ContextMetaKey[]` for typed\n // adopters, but JS callers (and TS callers who erase via `as any`)\n // can still slip through with `[42]` or `['']`. Surface it at\n // definition time as a TypeError instead of riding to first\n // request as a `MissingContributorError` on a phantom key.\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.dependsOn entries must be non-empty strings.`,\n )\n }\n\n // Source-location capture. `new Error().stack` here records the\n // adopter's call site so boot-time errors (cycles, missing deps,\n // duplicate keys) can point at the file that declared the broken\n // decorator. Bad boot errors are why frameworks get abandoned —\n // a single stack frame turns \"what's a 'tenent'?\" into \"edit\n // src/contributors/tenant.ts line 42\".\n const definedAt = new Error().stack\n\n // Snapshot + freeze paramDefaults so callers can't mutate the spec\n // object post-definition and shift the merged params under our feet.\n // Same immutability boundary applied to `deps` and `dependsOn`.\n const defaults = Object.freeze({\n ...(spec.paramDefaults ?? ({} as Partial<P>)),\n }) as Readonly<Partial<P>>\n\n // Shared deps + dependsOn — frozen once and reused across every\n // registration produced by this decorator (one per call site).\n // `Object.freeze` is shallow — the values inside (DI tokens,\n // constructors) are intentionally untouched; freezing them would\n // prevent the container from doing legitimate per-instance\n // bookkeeping.\n //\n // The `({} as D)` cast: `spec.deps` is optional (`deps?: D`), so\n // the fallback to `{}` is required. When `D` defaults to\n // `Record<string, never>`, the cast is sound (`{}` matches). When\n // `D` is non-empty AND adopter omitted `deps`, the cast is unsound\n // — but the runner errors loudly the moment it tries to resolve a\n // missing dep, so the mistake surfaces at first request rather\n // than producing wrong-but-silent behaviour. The alternative\n // (forcing `deps` non-optional in the spec) would break ergonomics\n // for the common zero-deps case.\n const sharedDeps = Object.freeze({ ...(spec.deps ?? ({} as D)) }) as D\n const sharedDependsOn = Object.freeze([...(spec.dependsOn ?? [])])\n const sharedOptional = spec.optional ?? false\n\n /**\n * Build a frozen registration that closes over a specific\n * resolved-params object. The runner sees a normal\n * `(ctx, deps) => value` registration — params are baked into the\n * closure, invisible to the pipeline.\n */\n const buildRegistration = (params: P): ContributorRegistration<K, D, Ctx> => {\n // Shallow-freeze the captured params so a misbehaving `resolve()` /\n // `onError()` can't mutate the closure and bleed into later\n // requests through the same registration. Adopters who really need\n // mutable per-request state should write to `ctx.set(...)` from\n // inside the resolver instead of mutating params.\n const frozenParams = Object.freeze({ ...params }) as P\n const onError: ContributorRegistration<K, D, Ctx>['onError'] = spec.onError\n ? (err: unknown, ctx: Ctx) => spec.onError!(err, ctx, frozenParams)\n : undefined\n const resolve: ContributorRegistration<K, D, Ctx>['resolve'] = (\n ctx: Ctx,\n deps: ResolvedDeps<D>,\n ) => spec.resolve(ctx, deps, frozenParams)\n return Object.freeze({\n key: spec.key,\n deps: sharedDeps,\n dependsOn: sharedDependsOn,\n optional: sharedOptional,\n onError,\n resolve,\n definedAt,\n })\n }\n\n // Default-params registration — used for zero-arg decorator\n // applications and for the `.registration` accessor. Cloning\n // `defaults` so the closure can't be mutated through the spec.\n const defaultRegistration = buildRegistration({ ...defaults } as P)\n\n /**\n * Decide whether the decorator was called as a decorator\n * (`@Foo` / `@Foo(target, key)`) or as a factory\n * (`@Foo()` / `@Foo({...})`).\n *\n * - Class decorator: `(target)` where target is a constructor function.\n * - Method decorator: `(target, propertyKey, descriptor?)` where\n * target is a prototype object and propertyKey is a string/symbol.\n * - Factory (defaults): `()` — adopter wrote `@Foo()` (rare; usually\n * they write the bare `@Foo`). Returns a decorator using\n * `paramDefaults`.\n * - Factory (params): `(params)` where params is a plain object.\n *\n * The legacy decorators that ship with KickJS (`@Get('/path')`,\n * `@Cron('* * * *')`, etc.) use the same heuristic, so adopters\n * already know to pass plain object literals as factory args.\n */\n const isDecoratorCall = (args: unknown[]): boolean => {\n // Zero args is a factory call (`@Foo()`), not a direct decorator\n // application — TS never invokes a decorator with zero args.\n if (args.length === 0) return false\n if (args.length >= 2) return true\n const first = args[0]\n // Functions are class constructors at decoration time.\n if (typeof first === 'function') return true\n // Anything else (plain object, undefined, primitive) → factory call.\n return false\n }\n\n const applyAsDecorator = (\n registration: ContributorRegistration<K, D, Ctx>,\n target: object,\n propertyKey?: string | symbol,\n ): void => {\n if (propertyKey === undefined) {\n // Class decorator — `target` is the constructor.\n pushClassMeta(METADATA.CLASS_CONTRIBUTORS, target, registration)\n } else {\n // Method decorator — `target` is the prototype; we write to its\n // constructor so reads can use the controller class as the\n // lookup key, matching the convention used by @Middleware and\n // consumed by router-builder.ts.\n const constructor = (target as { constructor: object }).constructor\n pushMethodMeta(METADATA.METHOD_CONTRIBUTORS, constructor, String(propertyKey), registration)\n }\n }\n\n /**\n * `true` for `{...}` and `Object.create(null)`. Rejects every\n * built-in subclass (Map / Set / Date / Error / RegExp / Promise),\n * adopter classes, and arrays.\n */\n const isPlainObject = (value: object): boolean => {\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n }\n\n /**\n * Merge call-site `Partial<P>` over `paramDefaults`. Strict guard:\n * accepts only **plain object literals** (or `Object.create(null)`).\n * Reject everything else with a descriptive `TypeError`.\n *\n * Why strict? Object spread copies enumerable own properties only.\n * `{ ...new Date() }` and `{ ...new Map([...])}` both produce `{}`\n * — the params silently drop. Catching this at the boundary\n * surfaces the mistake instead of routing the request with empty\n * config. Adopters who genuinely need a class-instance shape can\n * destructure it themselves: `Foo({ ...new MyParams() })`.\n */\n const mergeParams = (override: unknown): P => {\n if (override === undefined) return { ...defaults } as P\n if (\n override === null ||\n typeof override !== 'object' ||\n Array.isArray(override) ||\n !isPlainObject(override)\n ) {\n const got =\n override === null\n ? 'null'\n : Array.isArray(override)\n ? 'array'\n : typeof override === 'object'\n ? `instance of ${(override as object).constructor?.name ?? 'unknown class'}`\n : typeof override\n throw new TypeError(\n `defineContextDecorator(${spec.key}): factory call requires a plain object literal, got ${got}. ` +\n `Class instances and built-ins (Map, Date, etc) silently spread to empty objects — ` +\n `call the decorator with a spread object literal instead, e.g. ` +\n `\\`MyDecorator({ ...someInstance })\\`.`,\n )\n }\n return { ...defaults, ...(override as Partial<P>) } as P\n }\n\n const sharedRequiredParams = Object.freeze([...(spec.requiredParams ?? [])])\n\n /**\n * Runtime half of the required-params guarantee. The type system\n * already stops TS call sites from omitting a required field, so this\n * catches what types can't see: plain-JS adopters, `as any`, and\n * params built dynamically.\n *\n * `usage` names the form that failed so the message points at the fix\n * — a bare `@Foo` on a decorator with required params needs to become\n * `@Foo({ ... })`, which is a different edit from a factory call that\n * merely forgot one field.\n */\n const assertRequiredParams = (params: P, usage: string): void => {\n if (sharedRequiredParams.length === 0) return\n const missing = sharedRequiredParams.filter(\n (name) => (params as Record<string, unknown>)[name] === undefined,\n )\n if (missing.length === 0) return\n throw new TypeError(\n `defineContextDecorator(${spec.key}): ${usage} is missing required param(s) ` +\n `${missing.map((m) => `'${m}'`).join(', ')}. ` +\n `Supply them at the call site — e.g. \\`@${spec.key}({ ${missing[0]}: … })\\` — or ` +\n `add a default in \\`paramDefaults\\` if one is genuinely correct for every route.`,\n )\n }\n\n // Overloaded function signatures so `decoratorOrFactory`'s type\n // matches `ContextDecorator`'s call shapes directly — no\n // `as unknown as` double-cast needed. The implementation signature\n // (`...args: unknown[]`) is hidden from callers; only the four\n // declared overloads are reachable.\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n function decoratorOrFactory(target: Function): void\n function decoratorOrFactory(\n target: object,\n propertyKey: string | symbol,\n descriptor?: PropertyDescriptor,\n ): void\n function decoratorOrFactory(): ContextDecoratorTarget\n function decoratorOrFactory(params: Partial<P>): ContextDecoratorTarget\n function decoratorOrFactory(...args: unknown[]): unknown {\n if (isDecoratorCall(args)) {\n const [target, propertyKey] = args as [\n object,\n string | symbol | undefined,\n PropertyDescriptor?,\n ]\n // Bare `@Foo` — nothing can supply params here, so a required one\n // that isn't defaulted is a decoration-time (module-load) failure\n // rather than an undefined riding into the resolver.\n assertRequiredParams({ ...defaults } as P, 'bare `@decorator` usage')\n applyAsDecorator(defaultRegistration, target, propertyKey)\n return undefined\n }\n // Factory call — capture merged params and return a decorator.\n const params = mergeParams(args[0])\n assertRequiredParams(params, 'factory call')\n const registration = buildRegistration(params)\n return (target: object, propertyKey?: string | symbol) => {\n applyAsDecorator(registration, target, propertyKey)\n }\n }\n\n // Set a meaningful `.name` so `console.log(LoadTenant)` and stack\n // traces show `[Function: ContextDecorator(tenant)]` instead of\n // `[Function: decoratorOrFactory]`. Function.name is configurable\n // by default, so this works on the existing function reference.\n Object.defineProperty(decoratorOrFactory, 'name', {\n value: `ContextDecorator(${spec.key})`,\n writable: false,\n enumerable: false,\n configurable: true,\n })\n\n // `Object.assign` returns the intersection type, so the decorator\n // gets `.registration` + `.with` properties typed without the\n // previous `as unknown as ContextDecorator<...>` double cast.\n // Properties go on via assign (writable + enumerable defaults),\n // then `Object.freeze` locks the shape so adopters can't mutate\n // `decorator.registration = …` post-construction.\n const decorator = Object.assign(decoratorOrFactory, {\n with: (params: Partial<P>) => {\n const merged = mergeParams(params)\n assertRequiredParams(merged, '`.with()`')\n return { registration: buildRegistration(merged) }\n },\n })\n\n // `.registration` is a getter, not a plain property, so that reading it\n // on a decorator with undefaulted required params throws instead of\n // handing back a registration whose resolver will see `undefined`.\n // (`Object.assign` would have copied the value and lost the check.)\n Object.defineProperty(decorator, 'registration', {\n get() {\n assertRequiredParams({ ...defaults } as P, '`.registration`')\n return defaultRegistration\n },\n enumerable: true,\n configurable: false,\n })\n\n // `ContextDecorator` is a conditional type (permissive vs required-params\n // shape), and TS can't evaluate a conditional whose input generics are\n // still unresolved — so the assignment can't be checked structurally\n // here regardless of how `decorator` is built. The cast is the price of\n // the call-site guarantee; the two branches differ only in which call\n // forms they expose, and every one of them is implemented above.\n return Object.freeze(decorator) as unknown as ContextDecorator<K, D, P, Ctx, PD>\n}\n\n/**\n * Public export — the positional-generic implementation plus the\n * curried `.withParams<P>()` helper attached as a method. Adopters\n * pick whichever form fits the call site:\n *\n * - **Positional** — `defineContextDecorator(spec)` when there are\n * no per-call params (the common case). `K`, `D`, `Ctx` infer.\n * - **Curried** — `defineContextDecorator.withParams<P>()(spec)`\n * when the contributor takes params; spell only `P`, the rest\n * still infer.\n *\n * Frozen so adopters can't reassign `.withParams` at runtime.\n */\nexport const defineContextDecorator: DefineContextDecoratorFn = Object.freeze(\n Object.assign(defineContextDecoratorImpl, {\n withParams: <P extends Record<string, unknown>>(): DefineContextDecoratorWithParams<P> => {\n return <\n K extends string,\n D extends Record<string, DepValue> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Record<never, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, Ctx, PD>,\n ): ContextDecorator<K, D, P, Ctx, PD> => defineContextDecoratorImpl<K, D, P, Ctx, PD>(spec)\n },\n }),\n) as DefineContextDecoratorFn\n","/**\n * Topological sort for plugins and adapters with `dependsOn` declarations.\n *\n * Mirrors the algorithm used by the Context Contributor pipeline\n * (`contributor-pipeline.ts:topoSort`) but operates on a name-keyed\n * `MountSortItem` shape rather than a `ContributorRegistration`. Kept\n * standalone instead of factored generic so that:\n *\n * 1. The contributor pipeline doesn't need refactoring (risk reduction).\n * 2. The error classes can carry plugin/adapter-specific diagnostic\n * fields (`kind`, `dependent`) without bending the contributor types.\n */\n\n/**\n * A plugin or adapter that participates in dependency-ordered mounting.\n * Both `KickPlugin` and `AppAdapter` satisfy this shape via their `name`\n * and `dependsOn` fields.\n */\nexport interface MountSortItem {\n /** Unique mount name within the plugin/adapter set being sorted. */\n name: string\n /**\n * Other plugin/adapter names that must mount before this one. Cycles\n * and missing names fail at boot — same fail-fast contract as the\n * Context Contributor pipeline (architecture.md §20.4).\n */\n dependsOn?: readonly string[]\n}\n\n/** What kind of mount set the sort is operating on — surfaces in error messages. */\nexport type MountKind = 'plugin' | 'adapter'\n\n/**\n * Two items in the same mount set declared the same `name`. Plugin/adapter\n * names must be unique so `dependsOn` references resolve unambiguously.\n */\nexport class DuplicateMountNameError extends Error {\n readonly kind: MountKind\n readonly mountName: string\n\n constructor(kind: MountKind, mountName: string) {\n super(`Duplicate ${kind} name '${mountName}' — every ${kind} must declare a unique name`)\n this.name = 'DuplicateMountNameError'\n this.kind = kind\n this.mountName = mountName\n }\n}\n\n/**\n * A `dependsOn` entry references a plugin/adapter that isn't in the\n * current set. The dependency might not be installed, might be misspelled,\n * or might live in a different mount set (plugin referencing an adapter,\n * or vice versa — they sort independently).\n */\nexport class MissingMountDepError extends Error {\n readonly kind: MountKind\n readonly missing: string\n readonly dependent: string\n\n constructor(kind: MountKind, missing: string, dependent: string) {\n super(\n `Missing ${kind} dependency '${missing}' required by '${dependent}' — ` +\n `is the ${kind} installed and named correctly?`,\n )\n this.name = 'MissingMountDepError'\n this.kind = kind\n this.missing = missing\n this.dependent = dependent\n }\n}\n\n/**\n * The `dependsOn` declarations form a cycle. Boot fails so the cycle\n * can't poison live traffic. The `cycle` field reports one path through\n * the offending nodes for diagnostics — formatted `A → B → C → A`.\n */\nexport class MountCycleError extends Error {\n readonly kind: MountKind\n readonly cycle: readonly string[]\n\n constructor(kind: MountKind, cycle: readonly string[]) {\n super(`Cycle in ${kind} dependsOn declarations: ${cycle.join(' → ')}`)\n this.name = 'MountCycleError'\n this.kind = kind\n this.cycle = Object.freeze([...cycle])\n }\n}\n\n/**\n * Topologically sort `items` so every item with `dependsOn: ['X']` mounts\n * after `X`. Items without `dependsOn` keep their input order via Kahn's\n * algorithm with a FIFO queue seeded in input order — preserves the\n * historical \"plugins/adapters mount in declaration order\" behaviour for\n * any set that doesn't use `dependsOn`.\n *\n * @param items - mountable items, e.g. plugins or adapters\n * @param kind - 'plugin' | 'adapter', used in error messages\n * @returns a new array in valid mount order\n * @throws {DuplicateMountNameError} two items share a name\n * @throws {MissingMountDepError} a `dependsOn` references an unknown name\n * @throws {MountCycleError} the dep graph contains a cycle\n */\nexport function mountSort<T extends MountSortItem>(items: readonly T[], kind: MountKind): T[] {\n const byName = new Map<string, T>()\n for (const item of items) {\n if (byName.has(item.name)) {\n throw new DuplicateMountNameError(kind, item.name)\n }\n byName.set(item.name, item)\n }\n\n // indegree[name] = number of unresolved deps for that item\n const indegree = new Map<string, number>()\n // dependents[depName] = items that depend on depName\n const dependents = new Map<string, T[]>()\n\n for (const item of items) {\n indegree.set(item.name, item.dependsOn?.length ?? 0)\n }\n for (const item of items) {\n for (const dep of item.dependsOn ?? []) {\n if (!byName.has(dep)) {\n throw new MissingMountDepError(kind, dep, item.name)\n }\n const list = dependents.get(dep) ?? []\n list.push(item)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: items with zero indegree, preserving input order.\n const queue: T[] = []\n for (const item of items) {\n if (indegree.get(item.name) === 0) queue.push(item)\n }\n\n const sorted: T[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.name) ?? []) {\n const next = (indegree.get(dependent.name) ?? 0) - 1\n indegree.set(dependent.name, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== items.length) {\n const remaining = items.filter((it) => (indegree.get(it.name) ?? 0) > 0)\n throw new MountCycleError(kind, reconstructCycle(remaining, byName))\n }\n\n return sorted\n}\n\n/**\n * Walk the residual graph to find one cycle path for diagnostics.\n * Starts from the first remaining node and follows the first dep that's\n * still in the residual set until we revisit a node. Returns a path\n * starting and ending with the same name so the message reads naturally\n * (`A → B → C → A`).\n */\nfunction reconstructCycle<T extends MountSortItem>(\n residual: readonly T[],\n byName: ReadonlyMap<string, T>,\n): string[] {\n const residualNames = new Set(residual.map((r) => r.name))\n const start = residual[0].name\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor = start\n\n while (true) {\n const item = byName.get(cursor)!\n const nextDep = item.dependsOn?.find((d) => residualNames.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n const cycleStart = path.indexOf(nextDep)\n return [...path.slice(cycleStart), nextDep]\n }\n path.push(nextDep)\n seen.add(nextDep)\n cursor = nextDep\n }\n\n return path\n}\n","import type { Container } from './container'\nimport type { KickPlugin } from './plugin'\n\n/**\n * Options passed to {@link definePlugin}. Mirrors the spec in\n * `architecture.md` §21.3.1 + §21.2.2 (Phase A \"lite\").\n */\nexport interface DefinePluginOptions<TConfig> {\n /** Stable identity used for logging, `dependsOn` lookups, and `.scoped()` namespacing. */\n name: string\n\n /** Optional plugin version — surfaced to DevTools and `kick add` compatibility checks (Phase B). */\n version?: string\n\n /**\n * Peer-version ranges this plugin needs from the framework or other\n * plugins. Recorded as metadata; runtime enforcement lands in §21.4.4.\n */\n requires?: { kickjs?: string }\n\n /** Default config merged under any caller overrides. */\n defaults?: Partial<TConfig>\n\n /** Builds the underlying {@link KickPlugin} from the resolved config. */\n build(config: TConfig, ctx: BuildContext): Omit<KickPlugin, 'name'>\n}\n\n/**\n * Hook context handed to a plugin/adapter `build` function. `name` is the\n * resolved instance name (the bare definition name for the bare call,\n * or `${defName}:${scope}` for `.scoped()`). `scoped` is the discriminator\n * authors use to namespace DI tokens or resource keys.\n */\nexport interface BuildContext {\n name: string\n scoped: boolean\n}\n\n/**\n * Async-config form of a plugin or adapter — `inject` lists DI tokens to\n * resolve from the container, then `useFactory` produces the config.\n *\n * Plugins resolve this lazily inside `onReady`, so any module/adapter/\n * middleware/contributor a plugin contributes via `build` is **not\n * available** when `.async()` is used. Use the bare/`.scoped()` form for\n * plugins that need to register modules or middleware.\n */\nexport interface PluginAsyncOptions<TConfig> {\n inject?: ReadonlyArray<unknown>\n useFactory(...deps: any[]): TConfig | Promise<TConfig>\n}\n\n/**\n * Factory returned by {@link definePlugin}. Callable form is the singleton\n * (`AuthPlugin(config)`); `.scoped()` and `.async()` are the multi-instance\n * and deferred-config forms.\n */\nexport interface PluginFactory<TConfig> {\n (config?: Partial<TConfig>): KickPlugin\n scoped(scopeName: string, config?: Partial<TConfig>): KickPlugin\n async(opts: PluginAsyncOptions<TConfig>): KickPlugin\n /** Read-only access to the original definition — useful for DevTools introspection. */\n readonly definition: Readonly<DefinePluginOptions<TConfig>>\n}\n\nconst mergeConfig = <TConfig>(\n defaults: Partial<TConfig> = {},\n overrides: Partial<TConfig> = {},\n): TConfig => ({ ...defaults, ...overrides }) as TConfig\n\nconst composeName = (base: string, scope: string): string => `${base}:${scope}`\n\n/**\n * Build a {@link KickPlugin} factory from a typed definition. See\n * `architecture.md` §21.2.2 for the design rationale.\n *\n * @example\n * ```ts\n * const FlagsPlugin = definePlugin<FlagsConfig>({\n * name: 'FlagsPlugin',\n * defaults: { defaultTtl: 60_000 },\n * build(config) {\n * return {\n * register(container) {\n * container.registerInstance(FLAGS, makeProvider(config))\n * },\n * }\n * },\n * })\n *\n * bootstrap({ plugins: [FlagsPlugin({ provider: launchDarkly })] })\n * ```\n */\nexport function definePlugin<TConfig = Record<string, unknown>>(\n options: DefinePluginOptions<TConfig>,\n): PluginFactory<TConfig> {\n const buildSync = (\n instanceName: string,\n scoped: boolean,\n overrides?: Partial<TConfig>,\n ): KickPlugin => {\n const config = mergeConfig(options.defaults, overrides)\n // Mutate `name` on the build result instead of spreading — spread\n // strips prototype methods when `build()` returns a class instance.\n const built = options.build(config, { name: instanceName, scoped }) as KickPlugin\n built.name = instanceName\n return built\n }\n\n const buildAsync = (instanceName: string, opts: PluginAsyncOptions<TConfig>): KickPlugin => {\n let inner: KickPlugin | undefined\n\n const ensureInner = async (container: Container): Promise<KickPlugin> => {\n if (inner) return inner\n const deps = (opts.inject ?? []).map((tok) => container.resolve(tok as never))\n const config = await opts.useFactory(...deps)\n inner = buildSync(instanceName, false, config as Partial<TConfig>)\n return inner\n }\n\n return {\n name: instanceName,\n async onReady(container: Container) {\n const built = await ensureInner(container)\n // Run the inner plugin's register() now that we have the config —\n // late but better than never; `.async()` plugins skip the early\n // module/middleware contribution surface by design.\n built.register?.(container)\n await built.onReady?.(container)\n },\n async shutdown() {\n await inner?.shutdown?.()\n },\n }\n }\n\n const factory = ((config?: Partial<TConfig>) =>\n buildSync(options.name, false, config)) as PluginFactory<TConfig>\n\n factory.scoped = (scopeName: string, config?: Partial<TConfig>) =>\n buildSync(composeName(options.name, scopeName), true, config)\n\n factory.async = (opts: PluginAsyncOptions<TConfig>) => buildAsync(options.name, opts)\n\n Object.defineProperty(factory, 'definition', { value: Object.freeze({ ...options }) })\n\n return factory\n}\n","import type { AppAdapter, AdapterContext } from './adapter'\nimport type { BuildContext } from './define-plugin'\n\n/**\n * Options passed to {@link defineAdapter}. Mirrors {@link DefinePluginOptions}\n * exactly — see `architecture.md` §21.3.4 for the definePlugin/defineAdapter\n * symmetry rationale.\n *\n * The optional `TExtra` generic lets an adapter ship public methods\n * beyond the standard {@link AppAdapter} contract — they're preserved\n * on the returned adapter instance so external callers (tests, peer\n * adapters) can invoke them directly. See `OtelAdapter.applyRedaction`\n * for the canonical use case.\n */\nexport interface DefineAdapterOptions<TConfig, TExtra = unknown> {\n /**\n * Stable identity used for logging, `dependsOn` lookups, and `.scoped()`\n * namespacing. Required for adapters that participate in `dependsOn`\n * sort — error messages reference adapters by name.\n */\n name: string\n version?: string\n requires?: { kickjs?: string }\n defaults?: Partial<TConfig>\n build(config: TConfig, ctx: BuildContext): Omit<AppAdapter, 'name'> & TExtra\n}\n\n/**\n * Async-config form of an adapter. Identical shape to `PluginAsyncOptions`\n * but resolved during `beforeStart` (the earliest async hook in\n * `AppAdapter`) — meaning `middleware()` and `contributors()` from the inner\n * adapter are **not** picked up. Use the bare/`.scoped()` form when you need\n * those.\n */\nexport interface AdapterAsyncOptions<TConfig> {\n inject?: ReadonlyArray<unknown>\n useFactory(...deps: any[]): TConfig | Promise<TConfig>\n}\n\n/**\n * Factory returned by {@link defineAdapter}. Same surface as\n * {@link PluginFactory} (call / `.scoped()` / `.async()`) so adopters learn\n * one mental model for both primitives. Returned instances carry both\n * the standard {@link AppAdapter} contract and any `TExtra` extension\n * methods the `build` function exposed.\n */\nexport interface AdapterFactory<TConfig, TExtra = unknown> {\n (config?: Partial<TConfig>): AppAdapter & TExtra\n scoped(scopeName: string, config?: Partial<TConfig>): AppAdapter & TExtra\n async(opts: AdapterAsyncOptions<TConfig>): AppAdapter\n readonly definition: Readonly<DefineAdapterOptions<TConfig, TExtra>>\n}\n\nconst mergeConfig = <TConfig>(\n defaults: Partial<TConfig> = {},\n overrides: Partial<TConfig> = {},\n): TConfig => ({ ...defaults, ...overrides }) as TConfig\n\nconst composeName = (base: string, scope: string): string => `${base}:${scope}`\n\n/**\n * Build an {@link AppAdapter} factory from a typed definition. See\n * `architecture.md` §21.3.4 for migration guidance — most first-party\n * adapter packages collapse to a single `defineAdapter()` call after this\n * lands.\n *\n * @example\n * ```ts\n * const TenantAdapter = defineAdapter<TenantConfig>({\n * name: 'TenantAdapter',\n * defaults: { strategy: 'header', required: true },\n * build: (config) => ({\n * middleware: () => [tenantResolverMiddleware(config)],\n * beforeStart: ({ container }) => registerTenantFactory(container, config),\n * }),\n * })\n *\n * bootstrap({\n * adapters: [\n * TenantAdapter({ strategy: 'subdomain' }),\n * TenantAdapter.scoped('shard-eu', { headerName: 'x-eu-tenant' }),\n * ],\n * })\n * ```\n */\nexport function defineAdapter<TConfig = Record<string, unknown>, TExtra = unknown>(\n options: DefineAdapterOptions<TConfig, TExtra>,\n): AdapterFactory<TConfig, TExtra> {\n const buildSync = (\n instanceName: string,\n scoped: boolean,\n overrides?: Partial<TConfig>,\n ): AppAdapter & TExtra => {\n const config = mergeConfig(options.defaults, overrides)\n // Mutate `name` on the build result instead of spreading — spread\n // strips prototype methods when `build()` returns a class instance.\n // Extension methods declared by the build function (`TExtra`) survive\n // the mutation since they're own properties on the build result.\n const built = options.build(config, { name: instanceName, scoped }) as AppAdapter & TExtra\n ;(built as AppAdapter).name = instanceName\n return built\n }\n\n const buildAsync = (instanceName: string, opts: AdapterAsyncOptions<TConfig>): AppAdapter => {\n let inner: AppAdapter | undefined\n\n return {\n name: instanceName,\n async beforeStart(ctx: AdapterContext) {\n const deps = (opts.inject ?? []).map((tok) => ctx.container.resolve(tok as never))\n const config = await opts.useFactory(...deps)\n inner = buildSync(instanceName, false, config as Partial<TConfig>)\n await inner.beforeStart?.(ctx)\n },\n async afterStart(ctx: AdapterContext) {\n await inner?.afterStart?.(ctx)\n },\n async shutdown() {\n await inner?.shutdown?.()\n },\n async onHealthCheck() {\n return (await inner?.onHealthCheck?.()) ?? { name: instanceName, status: 'up' }\n },\n }\n }\n\n const factory = ((config?: Partial<TConfig>) =>\n buildSync(options.name, false, config)) as AdapterFactory<TConfig, TExtra>\n\n factory.scoped = (scopeName: string, config?: Partial<TConfig>) =>\n buildSync(composeName(options.name, scopeName), true, config)\n\n factory.async = (opts: AdapterAsyncOptions<TConfig>) => buildAsync(options.name, opts)\n\n Object.defineProperty(factory, 'definition', { value: Object.freeze({ ...options }) })\n\n return factory\n}\n","/**\n * Augmentation registry — runtime no-op + the canonical\n * `KickJsPluginRegistry` interface that `kick typegen` augments.\n *\n * See `architecture.md` §21.2.1 (typegen for `dependsOn`) and §21.3.3\n * (standardized augmentation registry) for the design rationale.\n *\n * @module @forinda/kickjs/core/augmentation\n */\n\n/**\n * Marker interface that lists every plugin/adapter in the project. The\n * keys are the literal `name` field passed to `defineAdapter` /\n * `definePlugin` (or declared on a `class implements AppAdapter`); the\n * values are the kind tag (`'plugin' | 'adapter'`).\n *\n * The framework ships this empty. `kick typegen` augments it from\n * source so `keyof KickJsPluginRegistry` resolves to a string-literal\n * union of every name in scope — and therefore so do `dependsOn`\n * declarations on plugins/adapters.\n */\nexport interface KickJsPluginRegistry {}\n\n/**\n * Resolves to a string-literal union of every registered plugin/adapter\n * name when the project's typegen has populated `KickJsPluginRegistry`,\n * or to `string` when the registry is empty (no typegen pass yet).\n *\n * Falling back to `string` keeps fresh projects compiling — the typegen\n * narrowing is a progressive enhancement, not a hard prerequisite.\n */\nexport type KickJsPluginName = keyof KickJsPluginRegistry extends never\n ? string\n : keyof KickJsPluginRegistry & string\n\n/**\n * Metadata attached to a `defineAugmentation` call. The fields are\n * documentation-only — `kick typegen` surfaces them in the generated\n * `.kickjs/types/augmentations.d.ts` so adopters can browse every\n * augmentable interface from one place.\n *\n * `description` and `example` may both be multi-line — typegen preserves\n * line breaks when rendering them as JSDoc. There is no upper limit on\n * the example size; entire shape definitions or worked snippets are fine.\n */\nexport interface AugmentationMeta {\n /**\n * Free-form description of what the augmentation customises. May be\n * multi-line — newlines are preserved in the generated JSDoc.\n */\n description?: string\n /**\n * TS snippet showing a typical shape. May be multi-line; rendered\n * inside a fenced code block in the generated catalogue. Use this\n * for anything from `'{ foo: string }'` up to a full interface body\n * — typegen no longer flattens newlines.\n */\n example?: string\n}\n\n/**\n * Advertise an augmentable interface so `kick typegen` can list it in\n * `.kickjs/types/augmentations.d.ts` for project-wide discovery.\n *\n * **This function is a runtime AND type-level no-op.** It does NOT\n * augment the interface for TypeScript. The actual augmentation is\n * the `declare module` block — `defineAugmentation` only adds an\n * entry to the typegen catalogue so other contributors / adopters\n * can find the interface and know its expected shape.\n *\n * Both calls are needed:\n *\n * ```ts\n * import { defineAugmentation } from '@forinda/kickjs'\n *\n * // (1) The actual TS augmentation — what `ctx.get('tenant')` reads.\n * declare module '@forinda/kickjs' {\n * interface ContextMeta {\n * tenant: { id: string; name: string }\n * }\n * }\n *\n * // (2) Catalogue entry — what `kick typegen` lists in\n * // `.kickjs/types/augmentations.d.ts` for browseability.\n * defineAugmentation('ContextMeta', {\n * description: 'Tenant resolved from x-tenant-id by TenantAdapter',\n * example: `{\n * tenant: {\n * id: string\n * name: string\n * plan: 'free' | 'pro' | 'enterprise'\n * featureFlags: Record<string, boolean>\n * }\n * }`,\n * })\n * ```\n *\n * Skip `defineAugmentation` if you don't care about the catalogue — the\n * `declare module` block alone is enough for runtime + type behaviour.\n *\n * @deprecated Low value in practice: it does nothing at runtime or at\n * the type level — the `declare module` block alone gives you the\n * augmentation, and the generated `.kickjs/types/kick__augmentations.d.ts`\n * catalogue it feeds is documentation-only. Prefer a plain\n * `declare module '@forinda/kickjs' { ... }` block and (optionally) a\n * JSDoc comment on your own interface. `defineAugmentation` and the\n * `kick/augmentations` typegen plugin will be removed in a future major.\n */\nexport function defineAugmentation(name: string, meta?: AugmentationMeta): void {\n // No-op at runtime — `kick typegen` discovers the call statically and\n // emits the augmentation catalogue. Touch the args so esbuild / tsc\n // don't drop the call as dead code under aggressive minification\n // (the marker has to survive the build for static scanners that look\n // at compiled output, e.g. third-party tools).\n void name\n void meta\n}\n","// Shared asset-key strategy — used by the build pipeline, the\n// runtime dev resolver, and the typegen emitter so all three layers\n// agree on what the manifest looks like.\n//\n// Three strategies:\n//\n// - `'strip'` — drop the final extension. `pages/index.pug` →\n// `pages/index`. Best for namespaces where every basename is\n// unique (mail templates, pdf invoices). Shortest autocomplete.\n//\n// - `'with-extension'` — keep the extension. `pages/index.pug` →\n// `pages/index.pug`. Required when extension siblings coexist.\n//\n// - `'auto'` (default) — strip when safe, keep extensions when not.\n// The grouping pass collects files by stripped basename; groups\n// of one keep the stripped key, groups of more than one keep the\n// full path. Backward compatible for projects with no\n// extension-siblings; lossless for projects that have them.\n//\n// The function returns the path / key pairs in walk order (input\n// order preserved) so callers can iterate without re-sorting. Sort\n// in the caller if a stable manifest order is needed.\n\nexport type AssetKeyStrategy = 'auto' | 'strip' | 'with-extension'\n\nexport interface AssetKeyPair {\n /** Source-relative path the file was discovered at. */\n rel: string\n /** Final logical key for the manifest, including the namespace prefix. */\n key: string\n}\n\nexport interface GroupAssetKeysOptions {\n /**\n * Strategy applied to this namespace. Default `'auto'`.\n *\n * Adopters override per-namespace via `assetMap.<ns>.keys` in\n * kick.config.ts when they want uniform behaviour (e.g. always\n * include the extension regardless of collisions).\n */\n strategy?: AssetKeyStrategy\n}\n\nexport interface GroupAssetKeysResult {\n pairs: AssetKeyPair[]\n /**\n * Number of collision groups resolved by keeping extensions.\n * Surfaces as an informational log (\"auto-resolved N collisions\")\n * so adopters know whether they're relying on auto-mode or not.\n * Always 0 for `'strip'` (overwrites) and `'with-extension'`\n * (extensions always preserved, no collision possible).\n */\n collisionGroupsResolved: number\n}\n\n/**\n * Compute final manifest keys for a namespace's discovered paths,\n * given the chosen strategy. Pure function — no fs / config / globals\n * involved.\n *\n * @param namespace assetMap key — e.g. `'mails'`, `'pages'`.\n * @param paths source-relative file paths (POSIX separators) discovered\n * under the namespace's `src` directory.\n * @param opts grouping options.\n */\nexport function groupAssetKeys(\n namespace: string,\n paths: readonly string[],\n opts: GroupAssetKeysOptions = {},\n): GroupAssetKeysResult {\n const strategy = opts.strategy ?? 'auto'\n\n if (strategy === 'with-extension') {\n return {\n pairs: paths.map((rel) => ({ rel, key: `${namespace}/${rel}` })),\n collisionGroupsResolved: 0,\n }\n }\n\n if (strategy === 'strip') {\n // Strict strip — last-walk-order wins on collision; no\n // collision tracking here because the docs document this as the\n // adopter's choice. The caller (build / runtime / typegen) can\n // still warn if it wants; this function just shapes keys.\n return {\n pairs: paths.map((rel) => ({ rel, key: `${namespace}/${stripExt(rel)}` })),\n collisionGroupsResolved: 0,\n }\n }\n\n // strategy === 'auto' — group by stripped basename, keep\n // extensions only on collision groups.\n const groups = new Map<string, string[]>()\n for (const rel of paths) {\n const strippedKey = `${namespace}/${stripExt(rel)}`\n const arr = groups.get(strippedKey)\n if (arr) {\n arr.push(rel)\n } else {\n groups.set(strippedKey, [rel])\n }\n }\n\n let collisionGroupsResolved = 0\n // Build a per-rel decision table so we preserve input ordering in\n // the output (manifest writers rely on stable ordering).\n const keyByRel = new Map<string, string>()\n for (const [strippedKey, rels] of groups) {\n if (rels.length === 1) {\n keyByRel.set(rels[0], strippedKey)\n } else {\n collisionGroupsResolved += 1\n for (const rel of rels) {\n keyByRel.set(rel, `${namespace}/${rel}`)\n }\n }\n }\n\n return {\n pairs: paths.map((rel) => ({\n rel,\n key: keyByRel.get(rel) ?? `${namespace}/${rel}`,\n })),\n collisionGroupsResolved,\n }\n}\n\n/** Strip the final extension from a path. `'mails/welcome.ejs'` → `'mails/welcome'`. */\nexport function stripExt(path: string): string {\n // Inline the logic instead of importing extname so this module\n // stays usable in pure-browser contexts. Same algorithm: find the\n // last dot AFTER the last separator, slice up to it.\n const lastSlash = path.lastIndexOf('/')\n const filename = lastSlash >= 0 ? path.slice(lastSlash + 1) : path\n const lastDot = filename.lastIndexOf('.')\n if (lastDot <= 0) return path // no ext, or hidden file like `.env`\n const dirPart = lastSlash >= 0 ? path.slice(0, lastSlash + 1) : ''\n return dirPart + filename.slice(0, lastDot)\n}\n","/**\n * Asset manager runtime (assets-plan.md PR 3).\n *\n * Three accessor surfaces over one resolver engine:\n *\n * - `assets.x.y()` — Proxy ambient, autocomplete-friendly default.\n * - `useAssets()` — hook returning the same Proxy; mockable per-call.\n * - `resolveAsset(ns, k)` — string resolver for dynamic dispatch.\n *\n * Resolution path:\n *\n * 1. Load the build-time manifest (`<outDir>/.kickjs-assets.json`)\n * when present. Manifest existence = \"we're running from a built\n * dist\". Fast path; mapped paths returned verbatim.\n * 2. Otherwise (dev / test) walk the assetMap src directories from\n * `kick.config.{json,js,cjs}` and synthesise the same manifest\n * shape in memory. First call pays the walk cost; subsequent\n * calls hit the cache.\n * 3. `KICK_ASSETS_ROOT` env var overrides everything — the resolver\n * treats the path as the manifest directory and skips discovery.\n *\n * @module @forinda/kickjs/core/assets\n */\n\nimport { existsSync, readFileSync, readdirSync } from 'node:fs'\nimport { isAbsolute, join, resolve, sep } from 'node:path'\nimport { createToken, type InjectionToken } from './token'\nimport { groupAssetKeys } from './asset-keys'\n\nexport const ASSET_MANIFEST_VERSION = 1 as const\n\ninterface AssetManifest {\n version: typeof ASSET_MANIFEST_VERSION\n entries: Record<string, string>\n}\n\ninterface ResolvedManifest {\n manifest: AssetManifest\n root: string\n}\n\n/**\n * Augmentable interface — `kick typegen` (assets-plan.md PR 4) emits\n * a `declare global` augmentation listing every namespace + key for\n * the project's `assetMap`. Adopters who haven't run typegen yet\n * still get a runtime-correct Proxy; only typed autocomplete waits\n * on the augmentation.\n */\nexport interface KickAssets {}\n\n/**\n * DI token for the `assets` Proxy. Adopter code that wants to\n * receive assets via constructor injection registers via\n * `container.registerInstance(ASSETS, useAssets())` in `bootstrap()`\n * and injects with `@Inject(ASSETS)`.\n */\nexport const ASSETS: InjectionToken<KickAssets> = createToken<KickAssets>('kick/assets/Map')\n\nexport class UnknownAssetError extends Error {\n readonly namespace: string\n readonly key: string\n constructor(namespace: string, key: string) {\n super(\n `Unknown asset '${namespace}/${key}'. ` +\n `Make sure the namespace exists in kick.config.ts assetMap and that the file is present in the source directory.`,\n )\n this.name = 'UnknownAssetError'\n this.namespace = namespace\n this.key = key\n }\n}\n\nlet manifestCache: ResolvedManifest | null | undefined = undefined\n\n/**\n * Convert a filesystem path to its posix (forward-slash) form. The\n * asset manifest and the public `resolveAsset` / `assets.x.y()` /\n * `useAssets()` contract returns posix paths on every platform so\n * adopters can:\n *\n * - pass the result to `express.static` / `res.sendFile` / `ejs.renderFile`\n * without worrying about Windows vs. posix separators\n * - splice the path into URLs (`href`, `src`, CDN keys) without\n * replacing `\\` with `/` by hand\n * - compare two paths from `resolveAsset` for equality on any host\n *\n * Node's `fs.*` and Express's path-handling APIs accept either\n * separator on Windows, so this normalisation is safe — the only\n * adopter code it could break is something explicitly parsing\n * Windows backslashes back out of the result, which would already be\n * brittle.\n */\nfunction toPosixPath(p: string): string {\n return p.replaceAll('\\\\', '/')\n}\n\nexport function resolveAsset(namespace: string, key: string): string {\n const resolved = loadManifest()\n if (!resolved) {\n throw new UnknownAssetError(namespace, key)\n }\n const entry = resolved.manifest.entries[`${namespace}/${key}`]\n if (!entry) {\n throw new UnknownAssetError(namespace, key)\n }\n const abs = isAbsolute(entry) ? entry : resolve(resolved.root, entry)\n return toPosixPath(abs)\n}\n\n/** Reset the manifest cache. Tests use this; production code shouldn't need it. */\nexport function clearAssetCache(): void {\n manifestCache = undefined\n}\n\nfunction loadManifest(): ResolvedManifest | null {\n // In development we deliberately skip the cache so adding or removing\n // a template file (e.g. dropping `welcome-back.ejs` into\n // `src/templates/mails/`) takes effect without a server restart. The\n // synthesise-dev-manifest path walks ~10 files; the cost is negligible\n // compared to the user-visible surprise of \"I added a file but\n // assets.x.y() throws\". Production keeps the cache because the built\n // manifest under `dist/.kickjs-assets.json` doesn't change at runtime.\n if (process.env.NODE_ENV !== 'production') {\n return discoverManifest()\n }\n if (manifestCache !== undefined) return manifestCache\n manifestCache = discoverManifest()\n return manifestCache\n}\n\nfunction discoverManifest(): ResolvedManifest | null {\n // `KICK_ASSETS_ROOT` is an explicit, deliberate override — it wins in\n // both dev and prod.\n const envRoot = process.env.KICK_ASSETS_ROOT\n if (envRoot) {\n const fromEnv = readBuiltManifest(envRoot)\n if (fromEnv) return fromEnv\n }\n\n const cwd = process.cwd()\n const config = loadConfigSync(cwd)\n\n // In dev, prefer a live walk of the source tree over any built\n // manifest left on disk by an earlier `kick build`. Otherwise a stale\n // `dist/.kickjs-assets.json` shadows the live `src/` tree: a freshly\n // added template file would resolve to the old map (or throw\n // UnknownAssetError) until the next rebuild — exactly the surprise the\n // cache-skip in `loadManifest` exists to avoid. `synthesiseDevManifest`\n // returns null only when there's no `assetMap` config at all, in which\n // case we fall through to the built-manifest probes below.\n if (process.env.NODE_ENV !== 'production') {\n const fresh = synthesiseDevManifest(cwd, config)\n if (fresh) return fresh\n }\n\n // Honour `kick.config.build.outDir` BEFORE the standard probe list\n // so adopters with non-default Vite outputs (e.g. `output/`,\n // `target/`, anything else) don't have to set KICK_ASSETS_ROOT\n // every time. Cached config load — same loader the dev fallback\n // uses. Skip when outDir matches one of the standard candidates;\n // it'd be probed redundantly otherwise.\n const configOutDir = config?.build?.outDir\n if (configOutDir && !(STANDARD_OUT_DIRS as readonly string[]).includes(configOutDir)) {\n const found = readBuiltManifest(resolve(cwd, configOutDir))\n if (found) return found\n }\n\n for (const candidate of STANDARD_OUT_DIRS) {\n const found = readBuiltManifest(join(cwd, candidate))\n if (found) return found\n }\n\n return synthesiseDevManifest(cwd, config)\n}\n\nconst STANDARD_OUT_DIRS = ['dist', 'build', 'out'] as const\n\nfunction readBuiltManifest(dir: string): ResolvedManifest | null {\n const path = join(dir, '.kickjs-assets.json')\n if (!existsSync(path)) return null\n try {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as Partial<AssetManifest>\n if (parsed.version !== ASSET_MANIFEST_VERSION) return null\n if (!parsed.entries || typeof parsed.entries !== 'object') return null\n return { manifest: parsed as AssetManifest, root: resolve(dir) }\n } catch {\n return null\n }\n}\n\ninterface AssetMapConfigShape {\n assetMap?: Record<\n string,\n {\n src: string\n dest?: string\n glob?: string\n keys?: 'auto' | 'strip' | 'with-extension'\n }\n >\n build?: { outDir?: string }\n}\n\nfunction synthesiseDevManifest(\n cwd: string,\n preloadedConfig?: AssetMapConfigShape | null,\n): ResolvedManifest | null {\n const config = preloadedConfig === undefined ? loadConfigSync(cwd) : preloadedConfig\n if (!config?.assetMap) return null\n\n const entries: Record<string, string> = {}\n // Walk order is filesystem-dependent on Linux/macOS, so we collect\n // every match per-namespace first, then sort + feed `groupAssetKeys`\n // for stable manifest output. Build pipeline does the same — both\n // layers agree on the resulting key set.\n for (const [namespace, entry] of Object.entries(config.assetMap)) {\n if (!entry || typeof entry.src !== 'string') continue\n const srcAbs = resolve(cwd, entry.src)\n if (!existsSync(srcAbs)) continue\n\n const collected: Array<{ rel: string; abs: string }> = []\n walkSync(srcAbs, srcAbs, (relPath, absPath) => {\n if (entry.glob && !matchesGlobLite(relPath, entry.glob, namespace)) return\n collected.push({ rel: relPath, abs: absPath })\n })\n collected.sort((a, b) => a.rel.localeCompare(b.rel))\n\n const { pairs, collisionGroupsResolved } = groupAssetKeys(\n namespace,\n collected.map((c) => c.rel),\n { strategy: entry.keys ?? 'auto' },\n )\n const absByRel = new Map(collected.map((c) => [c.rel, c.abs]))\n for (const { rel, key } of pairs) {\n const abs = absByRel.get(rel)\n if (abs) entries[key] = abs\n }\n if (collisionGroupsResolved > 0 && process.env.NODE_ENV !== 'production') {\n // Dev-only nudge — production runs read the build manifest where\n // the collision was already resolved at build time.\n console.log(\n `[kickjs/assets] '${namespace}': ${collisionGroupsResolved} basename collision(s) auto-resolved by keeping extensions ` +\n `(set assetMap.${namespace}.keys to 'strip' or 'with-extension' to override).`,\n )\n }\n }\n\n return { manifest: { version: ASSET_MANIFEST_VERSION, entries }, root: cwd }\n}\n\nconst warnedGlobs = new Set<string>()\n\n/**\n * Tiny glob matcher used only by dev manifest synthesis. Covers the\n * common assetMap glob patterns (`**\\/*`, `**\\/*.ext`,\n * `**\\/*.{a,b}`). Anything exotic prints a one-time warning + falls\n * through to `**\\/*` semantics rather than silently dropping files.\n *\n * The build pipeline (`packages/cli/src/asset-manager/build.ts`) uses\n * the full `glob` package; adopters who hit the dev-mode warning\n * should run `kick build:assets` to produce a real manifest.\n */\nfunction matchesGlobLite(relPath: string, pattern: string, namespace: string): boolean {\n if (pattern === '**/*' || pattern === '**') return true\n const singleExt = /^\\*\\*\\/\\*\\.(\\w+)$/.exec(pattern)\n if (singleExt) return relPath.endsWith(`.${singleExt[1]}`)\n const braceExt = /^\\*\\*\\/\\*\\.\\{([^}]+)\\}$/.exec(pattern)\n if (braceExt) {\n const exts = braceExt[1].split(',').map((e) => e.trim())\n return exts.some((ext) => relPath.endsWith(`.${ext}`))\n }\n const tag = `${namespace}::${pattern}`\n if (!warnedGlobs.has(tag)) {\n warnedGlobs.add(tag)\n console.warn(\n `[kickjs/assets] Dev-mode glob '${pattern}' on '${namespace}' isn't recognised by the lite matcher. ` +\n `Falling back to '**/*'. Run 'kick build:assets' to use the full glob engine.`,\n )\n }\n return true\n}\n\nfunction walkSync(root: string, dir: string, onFile: (rel: string, abs: string) => void): void {\n let entries\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n walkSync(root, full, onFile)\n } else if (entry.isFile()) {\n const rel = full\n .slice(root.length + 1)\n .split(sep)\n .join('/')\n onFile(rel, full)\n }\n }\n}\n\n/**\n * Sync `kick.config.{json,cjs,js}` loader for dev manifest\n * synthesis. Avoids dynamic `import()` (would force every assets\n * lookup to be async). TypeScript configs (`kick.config.ts`) are\n * skipped here — adopters who use them should run `kick build:assets`\n * to produce a real manifest the resolver can read.\n */\nfunction loadConfigSync(cwd: string): AssetMapConfigShape | null {\n const jsonPath = join(cwd, 'kick.config.json')\n if (existsSync(jsonPath)) {\n try {\n return JSON.parse(readFileSync(jsonPath, 'utf-8')) as AssetMapConfigShape\n } catch {\n return null\n }\n }\n for (const filename of ['kick.config.cjs', 'kick.config.js']) {\n const path = join(cwd, filename)\n if (!existsSync(path)) continue\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { createRequire } = require('node:module') as typeof import('node:module')\n const req = createRequire(join(cwd, 'package.json'))\n const mod = req(path) as { default?: AssetMapConfigShape } & AssetMapConfigShape\n return mod.default ?? mod ?? null\n } catch {\n continue\n }\n }\n\n // Fallback: the CLI snapshot of a transpiled `kick.config.ts`. We\n // can't transpile TS synchronously on the asset hot path, so the CLI\n // (`loadKickConfig`) mirrors the JSON-serialisable `assetMap` +\n // `build.outDir` into `.kickjs/kick.config.json` whenever it loads a\n // `.ts`/`.js` config. This is what makes `assets.x.y()` resolve in\n // dev for `.ts`-config projects before the first production build.\n const snapshotPath = join(cwd, '.kickjs', 'kick.config.json')\n if (existsSync(snapshotPath)) {\n try {\n return JSON.parse(readFileSync(snapshotPath, 'utf-8')) as AssetMapConfigShape\n } catch {\n return null\n }\n }\n return null\n}\n\n/**\n * Keys we MUST return `undefined` for: returning anything truthy\n * here causes Promise resolution to treat the Proxy as a thenable\n * and `await` it (resolving to whatever `then.call(...)` produces).\n * Two-element list deliberately — every other coercion-related key\n * goes through {@link PASSTHROUGH_KEYS} below so `String(assets)`,\n * template literals, and `util.inspect` keep working.\n */\nconst PROMISE_DETECTION_KEYS: ReadonlySet<string | symbol> = new Set<string | symbol>(['then'])\n\n/**\n * Keys we let fall through to the proxy target (a function-shaped\n * object). The default `Object.prototype` implementations of\n * `toString` / `valueOf` etc. are correct for these — returning\n * `undefined` here would break `String(assets)` and template\n * literals (`${assets}`) because `Symbol.toPrimitive` falling through\n * to a missing `toString` throws. Adopters that hit `assets.toString`\n * get `'function () {}'` back, which is fine — the contract is\n * \"asset access returns paths\"; everything else is incidental.\n */\nconst PASSTHROUGH_KEYS: ReadonlySet<string | symbol> = new Set<string | symbol>([\n 'toString',\n 'valueOf',\n 'constructor',\n 'prototype',\n 'name',\n 'length',\n 'asymmetricMatch',\n 'nodeType',\n Symbol.toPrimitive,\n Symbol.toStringTag,\n Symbol.iterator,\n Symbol.asyncIterator,\n])\n\nfunction createNamespaceProxy(parts: readonly string[]): unknown {\n const target = function () {} as unknown as Record<string, unknown>\n return new Proxy(target, {\n apply() {\n if (parts.length < 2) {\n throw new UnknownAssetError(parts[0] ?? '', '')\n }\n const [namespace, ...rest] = parts\n return resolveAsset(namespace, rest.join('/'))\n },\n get(t, prop) {\n if (PROMISE_DETECTION_KEYS.has(prop)) return undefined\n if (PASSTHROUGH_KEYS.has(prop)) return Reflect.get(t, prop)\n if (typeof prop === 'symbol') return undefined\n return createNamespaceProxy([...parts, prop])\n },\n })\n}\n\nfunction createAssetProxy(): KickAssets {\n return new Proxy({} as Record<string, unknown>, {\n get(t, prop) {\n if (PROMISE_DETECTION_KEYS.has(prop)) return undefined\n if (PASSTHROUGH_KEYS.has(prop)) return Reflect.get(t, prop)\n if (typeof prop === 'symbol') return undefined\n return createNamespaceProxy([prop])\n },\n }) as unknown as KickAssets\n}\n\n/**\n * Variant A — the ambient Proxy. Default accessor for static call\n * sites where the namespace + key are literals known at compile time.\n *\n * @example\n * ```ts\n * import { assets } from '@forinda/kickjs'\n * const path = assets.mails.welcome()\n * ```\n */\nexport const assets: KickAssets = createAssetProxy()\n\n/**\n * Variant B — the hook accessor. Returns the same Proxy as `assets`;\n * factory shape exists for testability + DI. Mock via\n * `vi.mock('@forinda/kickjs', () => ({ useAssets: () => fakeAssets }))`.\n *\n * @example\n * ```ts\n * import { useAssets } from '@forinda/kickjs'\n * class MailService {\n * private assets = useAssets()\n * send(name: string) {\n * return ejs.renderFile(this.assets.mails[name](), {})\n * }\n * }\n * ```\n */\nexport function useAssets(): KickAssets {\n return assets\n}\n\n// Install this module's resolver into the container's asset slot — the\n// container deliberately avoids importing this file (edge purity; see\n// asset-resolver-slot.ts). Runs at module scope so any node app that loads\n// the asset manager gets full @Asset injection.\nimport { _setAssetResolver } from './asset-resolver-slot'\n_setAssetResolver(resolveAsset)\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.4.0
2
+ * @forinda/kickjs v6.5.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -9,4 +9,4 @@
9
9
  * @license MIT
10
10
  */
11
11
  function randomHex(bytes){let buf=crypto.getRandomValues(new Uint8Array(bytes));return Array.from(buf,b=>b.toString(16).padStart(2,`0`)).join(``)}function csrf(options={}){let cookieName=options.cookie??`_csrf`,headerName=options.header??`x-csrf-token`,protectedMethods=new Set((options.methods??[`POST`,`PUT`,`PATCH`,`DELETE`]).map(m=>m.toUpperCase())),ignorePaths=new Set(options.ignorePaths??[]),tokenLength=options.tokenLength??32,cookieOpts={httpOnly:!0,sameSite:`strict`,secure:process.env.NODE_ENV===`production`,path:`/`,...options.cookieOptions};return(req,res,next)=>{let token=(req.cookies||{})[cookieName];if(token||(token=randomHex(tokenLength),res.cookie(cookieName,token,cookieOpts)),!protectedMethods.has(req.method.toUpperCase())||ignorePaths.has(req.path))return next();let headerToken=req.headers[headerName];if(!headerToken||headerToken!==token)return res.status(403).json({message:`CSRF token mismatch`});next()}}export{randomHex as n,csrf as t};
12
- //# sourceMappingURL=csrf-Dlo-P8Il.mjs.map
12
+ //# sourceMappingURL=csrf-DNh_7aOk.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"csrf-Dlo-P8Il.mjs","names":[],"sources":["../src/core/web-crypto.ts","../src/http/middleware/csrf.ts"],"sourcesContent":["/**\n * Web Crypto helpers — portable across node/bun/deno/workers via\n * `globalThis.crypto`. The Web Crypto migration lives here so the request\n * path never imports `node:crypto` (edge-runtime portability, see\n * `web-standards-edge-design.md` P0).\n */\n\n/** Random hex string — replacement for `randomBytes(n).toString('hex')`. */\nexport function randomHex(bytes: number): string {\n const buf = crypto.getRandomValues(new Uint8Array(bytes))\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n","import type { Request, Response, NextFunction } from 'express'\n\nimport { randomHex } from '../../core/web-crypto'\n\nexport interface CsrfOptions {\n /** Cookie name for the CSRF token (default: '_csrf') */\n cookie?: string\n /** Header name to check for the token (default: 'x-csrf-token') */\n header?: string\n /** HTTP methods that require CSRF validation (default: POST, PUT, PATCH, DELETE) */\n methods?: string[]\n /** Paths to exclude from CSRF checks (e.g. webhooks) */\n ignorePaths?: string[]\n /** Token byte length before hex encoding (default: 32 = 64 hex chars) */\n tokenLength?: number\n /** Cookie options */\n cookieOptions?: {\n httpOnly?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n secure?: boolean\n path?: string\n }\n}\n\n/**\n * Double-submit cookie CSRF protection middleware.\n *\n * On every request, sets a CSRF token cookie. For state-changing methods\n * (POST, PUT, PATCH, DELETE), validates that the request header matches\n * the cookie value.\n *\n * @example\n * ```ts\n * import { csrf } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * cookieParser(),\n * csrf(),\n * // ... other middleware\n * ],\n * })\n * ```\n *\n * Client usage:\n * 1. Read the `_csrf` cookie value\n * 2. Send it in the `x-csrf-token` header on every mutating request\n */\nexport function csrf(options: CsrfOptions = {}) {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const cookieOpts = {\n httpOnly: true,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Generate or reuse CSRF token\n const cookies = (req as any).cookies || {}\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomHex(tokenLength)\n res.cookie(cookieName, token, cookieOpts)\n }\n\n // Skip validation for safe methods and ignored paths\n if (!protectedMethods.has(req.method.toUpperCase())) {\n return next()\n }\n\n if (ignorePaths.has(req.path)) {\n return next()\n }\n\n // Validate: header token must match cookie token\n const headerToken = req.headers[headerName] as string | undefined\n\n if (!headerToken || headerToken !== token) {\n return res.status(403).json({\n message: 'CSRF token mismatch',\n })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,UAAU,MAAuB,CAC/C,IAAM,IAAM,OAAO,gBAAgB,IAAI,WAAW,KAAK,CAAC,EACxD,OAAO,MAAM,KAAK,IAAM,GAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CACxE,CCsCA,SAAgB,KAAK,QAAuB,CAAC,EAAG,CAC9C,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,QAAQ,EAAA,CAAG,IAAK,GAAM,EAAE,YAAY,CAAC,CACpF,EACM,YAAc,IAAI,IAAI,QAAQ,aAAe,CAAC,CAAC,EAC/C,YAAc,QAAQ,aAAe,GACrC,WAAa,CACjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,aACb,EAEA,OAAQ,IAAc,IAAe,OAAuB,CAG1D,IAAI,OADa,IAAY,SAAW,CAAC,EAAA,CACrB,YAYpB,GAVK,QACH,MAAQ,UAAU,WAAW,EAC7B,IAAI,OAAO,WAAY,MAAO,UAAU,GAItC,CAAC,iBAAiB,IAAI,IAAI,OAAO,YAAY,CAAC,GAI9C,YAAY,IAAI,IAAI,IAAI,EAC1B,OAAO,KAAK,EAId,IAAM,YAAc,IAAI,QAAQ,YAEhC,GAAI,CAAC,aAAe,cAAgB,MAClC,OAAO,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,CAC1B,QAAS,qBACX,CAAC,EAGH,KAAK,CACP,CACF"}
1
+ {"version":3,"file":"csrf-DNh_7aOk.mjs","names":[],"sources":["../src/core/web-crypto.ts","../src/http/middleware/csrf.ts"],"sourcesContent":["/**\n * Web Crypto helpers — portable across node/bun/deno/workers via\n * `globalThis.crypto`. The Web Crypto migration lives here so the request\n * path never imports `node:crypto` (edge-runtime portability, see\n * `web-standards-edge-design.md` P0).\n */\n\n/** Random hex string — replacement for `randomBytes(n).toString('hex')`. */\nexport function randomHex(bytes: number): string {\n const buf = crypto.getRandomValues(new Uint8Array(bytes))\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n","import type { Request, Response, NextFunction } from 'express'\n\nimport { randomHex } from '../../core/web-crypto'\n\nexport interface CsrfOptions {\n /** Cookie name for the CSRF token (default: '_csrf') */\n cookie?: string\n /** Header name to check for the token (default: 'x-csrf-token') */\n header?: string\n /** HTTP methods that require CSRF validation (default: POST, PUT, PATCH, DELETE) */\n methods?: string[]\n /** Paths to exclude from CSRF checks (e.g. webhooks) */\n ignorePaths?: string[]\n /** Token byte length before hex encoding (default: 32 = 64 hex chars) */\n tokenLength?: number\n /** Cookie options */\n cookieOptions?: {\n httpOnly?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n secure?: boolean\n path?: string\n }\n}\n\n/**\n * Double-submit cookie CSRF protection middleware.\n *\n * On every request, sets a CSRF token cookie. For state-changing methods\n * (POST, PUT, PATCH, DELETE), validates that the request header matches\n * the cookie value.\n *\n * @example\n * ```ts\n * import { csrf } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * cookieParser(),\n * csrf(),\n * // ... other middleware\n * ],\n * })\n * ```\n *\n * Client usage:\n * 1. Read the `_csrf` cookie value\n * 2. Send it in the `x-csrf-token` header on every mutating request\n */\nexport function csrf(options: CsrfOptions = {}) {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const cookieOpts = {\n httpOnly: true,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Generate or reuse CSRF token\n const cookies = (req as any).cookies || {}\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomHex(tokenLength)\n res.cookie(cookieName, token, cookieOpts)\n }\n\n // Skip validation for safe methods and ignored paths\n if (!protectedMethods.has(req.method.toUpperCase())) {\n return next()\n }\n\n if (ignorePaths.has(req.path)) {\n return next()\n }\n\n // Validate: header token must match cookie token\n const headerToken = req.headers[headerName] as string | undefined\n\n if (!headerToken || headerToken !== token) {\n return res.status(403).json({\n message: 'CSRF token mismatch',\n })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,UAAU,MAAuB,CAC/C,IAAM,IAAM,OAAO,gBAAgB,IAAI,WAAW,KAAK,CAAC,EACxD,OAAO,MAAM,KAAK,IAAM,GAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CACxE,CCsCA,SAAgB,KAAK,QAAuB,CAAC,EAAG,CAC9C,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,QAAQ,EAAA,CAAG,IAAK,GAAM,EAAE,YAAY,CAAC,CACpF,EACM,YAAc,IAAI,IAAI,QAAQ,aAAe,CAAC,CAAC,EAC/C,YAAc,QAAQ,aAAe,GACrC,WAAa,CACjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,aACb,EAEA,OAAQ,IAAc,IAAe,OAAuB,CAG1D,IAAI,OADa,IAAY,SAAW,CAAC,EAAA,CACrB,YAYpB,GAVK,QACH,MAAQ,UAAU,WAAW,EAC7B,IAAI,OAAO,WAAY,MAAO,UAAU,GAItC,CAAC,iBAAiB,IAAI,IAAI,OAAO,YAAY,CAAC,GAI9C,YAAY,IAAI,IAAI,IAAI,EAC1B,OAAO,KAAK,EAId,IAAM,YAAc,IAAI,QAAQ,YAEhC,GAAI,CAAC,aAAe,cAAgB,MAClC,OAAO,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,CAC1B,QAAS,qBACX,CAAC,EAGH,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.4.0
2
+ * @forinda/kickjs v6.5.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -9,4 +9,4 @@
9
9
  * @license MIT
10
10
  */
11
11
  const disposables=new Set;function registerDisposable(fn){return disposables.add(fn),()=>disposables.delete(fn)}function drainDisposables(){let pending=[...disposables];return disposables.clear(),pending}async function runDisposables(list){await Promise.allSettled(list.map(async fn=>fn()))}async function disposeAll(){await runDisposables(drainDisposables())}export{runDisposables as i,drainDisposables as n,registerDisposable as r,disposeAll as t};
12
- //# sourceMappingURL=disposables-rPSyh69r.mjs.map
12
+ //# sourceMappingURL=disposables-mnjJuaVx.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"disposables-rPSyh69r.mjs","names":[],"sources":["../src/core/disposables.ts"],"sourcesContent":["/**\n * Registry for framework-owned resources that hold timers or other handles\n * outside the adapter/plugin lifecycle — e.g. the in-memory session and\n * rate-limit stores' cleanup intervals. `Application.shutdown()` drains it so\n * startup and shutdown stay symmetric even for resources created lazily by\n * middleware factories.\n *\n * Intervals are `.unref()`'d so they never wedge process exit — this registry\n * exists for IN-PROCESS teardown (test suites, HMR, multi-app hosts) where\n * each un-disposed store would otherwise leak a live interval + backing Map\n * per app instance.\n */\n\nexport type Disposable = () => void | Promise<void>\n\nconst disposables = new Set<Disposable>()\n\n/** Register a cleanup callback. Returns an unregister function. */\nexport function registerDisposable(fn: Disposable): () => void {\n disposables.add(fn)\n return () => disposables.delete(fn)\n}\n\n/**\n * Remove and return all currently registered disposables WITHOUT running\n * them. Used by HMR rebuild to snapshot the outgoing app's resources before\n * `setup()` registers the incoming app's — so the stale set can be disposed\n * on success, or restored on failure, without touching the new one.\n */\nexport function drainDisposables(): Disposable[] {\n const pending = [...disposables]\n disposables.clear()\n return pending\n}\n\n/**\n * Run a list of disposables. Errors — including SYNCHRONOUS throws — are\n * swallowed per-entry: the `async` callback converts a sync throw into a\n * rejection that `allSettled` absorbs, so one bad disposable can't abort\n * the rest (a bare `Promise.resolve(fn())` would let the throw escape the\n * `.map()` before `allSettled` ever ran).\n */\nexport async function runDisposables(list: Disposable[]): Promise<void> {\n await Promise.allSettled(list.map(async (fn) => fn()))\n}\n\n/** Run and clear all registered disposables. Errors are swallowed per-entry. */\nexport async function disposeAll(): Promise<void> {\n await runDisposables(drainDisposables())\n}\n"],"mappings":";;;;;;;;;;AAeA,MAAM,YAAc,IAAI,IAGxB,SAAgB,mBAAmB,GAA4B,CAE7D,OADA,YAAY,IAAI,EAAE,MACL,YAAY,OAAO,EAAE,CACpC,CAQA,SAAgB,kBAAiC,CAC/C,IAAM,QAAU,CAAC,GAAG,WAAW,EAE/B,OADA,YAAY,MAAM,EACX,OACT,CASA,eAAsB,eAAe,KAAmC,CACtE,MAAM,QAAQ,WAAW,KAAK,IAAI,KAAO,KAAO,GAAG,CAAC,CAAC,CACvD,CAGA,eAAsB,YAA4B,CAChD,MAAM,eAAe,iBAAiB,CAAC,CACzC"}
1
+ {"version":3,"file":"disposables-mnjJuaVx.mjs","names":[],"sources":["../src/core/disposables.ts"],"sourcesContent":["/**\n * Registry for framework-owned resources that hold timers or other handles\n * outside the adapter/plugin lifecycle — e.g. the in-memory session and\n * rate-limit stores' cleanup intervals. `Application.shutdown()` drains it so\n * startup and shutdown stay symmetric even for resources created lazily by\n * middleware factories.\n *\n * Intervals are `.unref()`'d so they never wedge process exit — this registry\n * exists for IN-PROCESS teardown (test suites, HMR, multi-app hosts) where\n * each un-disposed store would otherwise leak a live interval + backing Map\n * per app instance.\n */\n\nexport type Disposable = () => void | Promise<void>\n\nconst disposables = new Set<Disposable>()\n\n/** Register a cleanup callback. Returns an unregister function. */\nexport function registerDisposable(fn: Disposable): () => void {\n disposables.add(fn)\n return () => disposables.delete(fn)\n}\n\n/**\n * Remove and return all currently registered disposables WITHOUT running\n * them. Used by HMR rebuild to snapshot the outgoing app's resources before\n * `setup()` registers the incoming app's — so the stale set can be disposed\n * on success, or restored on failure, without touching the new one.\n */\nexport function drainDisposables(): Disposable[] {\n const pending = [...disposables]\n disposables.clear()\n return pending\n}\n\n/**\n * Run a list of disposables. Errors — including SYNCHRONOUS throws — are\n * swallowed per-entry: the `async` callback converts a sync throw into a\n * rejection that `allSettled` absorbs, so one bad disposable can't abort\n * the rest (a bare `Promise.resolve(fn())` would let the throw escape the\n * `.map()` before `allSettled` ever ran).\n */\nexport async function runDisposables(list: Disposable[]): Promise<void> {\n await Promise.allSettled(list.map(async (fn) => fn()))\n}\n\n/** Run and clear all registered disposables. Errors are swallowed per-entry. */\nexport async function disposeAll(): Promise<void> {\n await runDisposables(drainDisposables())\n}\n"],"mappings":";;;;;;;;;;AAeA,MAAM,YAAc,IAAI,IAGxB,SAAgB,mBAAmB,GAA4B,CAE7D,OADA,YAAY,IAAI,EAAE,MACL,YAAY,OAAO,EAAE,CACpC,CAQA,SAAgB,kBAAiC,CAC/C,IAAM,QAAU,CAAC,GAAG,WAAW,EAE/B,OADA,YAAY,MAAM,EACX,OACT,CASA,eAAsB,eAAe,KAAmC,CACtE,MAAM,QAAQ,WAAW,KAAK,IAAI,KAAO,KAAO,GAAG,CAAC,CAAC,CACvD,CAGA,eAAsB,YAA4B,CAChD,MAAM,eAAe,iBAAiB,CAAC,CACzC"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.4.0
2
+ * @forinda/kickjs v6.5.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,6 +8,6 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{t as __require}from"./rolldown-runtime-Bzotz84Y.mjs";import{t as Container}from"./container-Da9BLMHy.mjs";import{detectSchema}from"@forinda/kickjs-schema";let _zod;function getZod(){if(_zod)return _zod;try{_zod=__require(`zod`)}catch{throw Error("@forinda/kickjs: the 'zod' peer dependency is required for baseEnvSchema / defineEnv / loadEnv but is not installed. Install it (`pnpm add zod`), or define your env with a non-Zod schema and call loadEnvFromSchema() instead — @forinda/kickjs-schema supports Valibot, Yup, and any Standard Schema validator without zod.")}return _zod}function tryLoadDotenv(){try{__require(`dotenv`).config({override:!1,quiet:!0})}catch{}}tryLoadDotenv();function buildBaseEnvSchema(){let z=getZod();return z.object({PORT:z.coerce.number().default(3e3),NODE_ENV:z.enum([`development`,`production`,`test`]).default(`development`),LOG_LEVEL:z.string().default(`info`)})}let _baseEnvSchema;function getBaseEnvSchema(){return _baseEnvSchema||=buildBaseEnvSchema(),_baseEnvSchema}const baseEnvSchema=new Proxy({},{get(_t,prop){let real=getBaseEnvSchema(),value=real[prop];return typeof value==`function`?value.bind(real):value},has(_t,prop){return prop in getBaseEnvSchema()}});let cachedEnv=null,cachedSchema=null;function defineEnv(extend){let base=getBaseEnvSchema(),userSchema=extend(base);return base.extend(userSchema.shape)}function loadEnv(schema){let s=schema??cachedSchema??getBaseEnvSchema();return cachedEnv&&cachedSchema===s?cachedEnv:(cachedSchema=s,cachedEnv=s.parse(process.env),Container._envResolver=key=>cachedEnv?.[key],cachedEnv)}function getEnv(key,schema){return(schema?loadEnv(schema):loadEnv())[key]}function reloadEnv(){try{__require(`dotenv`).config({override:!0,quiet:!0})}catch{}cachedEnv=null,cachedSchema&&(typeof cachedSchema.parse==`function`?loadEnv(cachedSchema):loadEnvFromSchema(cachedSchema))}function resetEnvCache(){cachedEnv=null,cachedSchema=null}function loadEnvFromSchema(schema){let result=detectSchema(schema).safeParse(process.env);if(!result.success){let details=result.issues.map(i=>` ${i.path.join(`.`)||`(root)`}: ${i.message}`).join(`
11
+ import{t as __require}from"./rolldown-runtime-CuwksmWW.mjs";import{t as Container}from"./container-CBaL5lus.mjs";import{detectSchema}from"@forinda/kickjs-schema";let _zod;function getZod(){if(_zod)return _zod;try{_zod=__require(`zod`)}catch{throw Error("@forinda/kickjs: the 'zod' peer dependency is required for baseEnvSchema / defineEnv / loadEnv but is not installed. Install it (`pnpm add zod`), or define your env with a non-Zod schema and call loadEnvFromSchema() instead — @forinda/kickjs-schema supports Valibot, Yup, and any Standard Schema validator without zod.")}return _zod}function tryLoadDotenv(){try{__require(`dotenv`).config({override:!1,quiet:!0})}catch{}}tryLoadDotenv();function buildBaseEnvSchema(){let z=getZod();return z.object({PORT:z.coerce.number().default(3e3),NODE_ENV:z.enum([`development`,`production`,`test`]).default(`development`),LOG_LEVEL:z.string().default(`info`)})}let _baseEnvSchema;function getBaseEnvSchema(){return _baseEnvSchema||=buildBaseEnvSchema(),_baseEnvSchema}const baseEnvSchema=new Proxy({},{get(_t,prop){let real=getBaseEnvSchema(),value=real[prop];return typeof value==`function`?value.bind(real):value},has(_t,prop){return prop in getBaseEnvSchema()}});let cachedEnv=null,cachedSchema=null;function defineEnv(extend){let base=getBaseEnvSchema(),userSchema=extend(base);return base.extend(userSchema.shape)}function loadEnv(schema){let s=schema??cachedSchema??getBaseEnvSchema();return cachedEnv&&cachedSchema===s?cachedEnv:(cachedSchema=s,cachedEnv=s.parse(process.env),Container._envResolver=key=>cachedEnv?.[key],cachedEnv)}function getEnv(key,schema){return(schema?loadEnv(schema):loadEnv())[key]}function reloadEnv(){try{__require(`dotenv`).config({override:!0,quiet:!0})}catch{}cachedEnv=null,cachedSchema&&(typeof cachedSchema.parse==`function`?loadEnv(cachedSchema):loadEnvFromSchema(cachedSchema))}function resetEnvCache(){cachedEnv=null,cachedSchema=null}function loadEnvFromSchema(schema){let result=detectSchema(schema).safeParse(process.env);if(!result.success){let details=result.issues.map(i=>` ${i.path.join(`.`)||`(root)`}: ${i.message}`).join(`
12
12
  `);throw Error(`Environment validation failed:\n${details}`)}return cachedEnv=result.data,cachedSchema=schema,Container._envResolver=key=>cachedEnv?.[key],result.data}export{loadEnvFromSchema as a,loadEnv as i,defineEnv as n,reloadEnv as o,getEnv as r,resetEnvCache as s,baseEnvSchema as t};
13
- //# sourceMappingURL=env-Ce-aYRxQ.mjs.map
13
+ //# sourceMappingURL=env-Dgtjncd5.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"env-Ce-aYRxQ.mjs","names":[],"sources":["../src/config/env.ts"],"sourcesContent":["import type { z } from 'zod'\nimport { detectSchema, type InferSchemaOutput } from '@forinda/kickjs-schema'\nimport { Container } from '../core/container'\nimport type { EnvKey } from '../core/decorators'\n\n/**\n * Lazy `zod` accessor. `zod` is an **optional** peer of\n * `@forinda/kickjs` — only the Zod-based env helpers (`baseEnvSchema`,\n * `defineEnv`, `loadEnv`) need it. Apps that validate env via Valibot /\n * Yup / Standard Schema go through `loadEnvFromSchema` (which only\n * touches the validator-agnostic `detectSchema`) and never load zod.\n *\n * Importing `zod` at module top-level made it a *hard* dependency of\n * the whole framework: `import { anything } from '@forinda/kickjs'`\n * eagerly evaluated this module, so a non-Zod app with no `zod`\n * installed crashed at module load / build time (\"Cannot find module\n * 'zod'\"). Deferring the require to first use keeps the import out of\n * the eager graph.\n */\nlet _zod: typeof import('zod') | undefined\nfunction getZod(): typeof import('zod') {\n if (_zod) return _zod\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n _zod = require('zod') as typeof import('zod')\n } catch {\n throw new Error(\n \"@forinda/kickjs: the 'zod' peer dependency is required for \" +\n 'baseEnvSchema / defineEnv / loadEnv but is not installed. ' +\n 'Install it (`pnpm add zod`), or define your env with a non-Zod ' +\n 'schema and call loadEnvFromSchema() instead — @forinda/kickjs-schema ' +\n 'supports Valibot, Yup, and any Standard Schema validator without zod.',\n )\n }\n return _zod\n}\n\n/**\n * Lazily load `.env` via dotenv if it is installed in the consumer app.\n * `dotenv` is an **optional peer dependency** of `@forinda/kickjs` —\n * apps that want `.env` file support install it themselves (the CLI\n * generators add it to every scaffolded `package.json`). Apps that\n * inject env via the shell, Docker, or a secret manager can skip it\n * entirely and `process.env` is read as-is.\n *\n * We call `dotenv.config()` directly rather than `require('dotenv/config')`\n * so that a re-evaluation of this module (e.g. Vite SSR full-reload after\n * editing `.env`) can re-read the file. `dotenv/config` is a side-effect\n * shim that CJS caches aggressively, so the second `require` is a no-op\n * and new keys never reach `process.env` — the root cause of the\n * \"DATABASE_URL undefined until restart\" bug on Windows dev.\n */\nfunction tryLoadDotenv(): void {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const dotenv = require('dotenv')\n // `quiet: true` suppresses dotenv 17's tip banner that goes to stdout.\n // Required for CLI tools whose stdout is parsed by callers (`kick explain\n // --json`, etc) — the banner would otherwise corrupt the JSON output.\n dotenv.config({ override: false, quiet: true })\n } catch {\n // dotenv not installed — fall through, env vars must come from\n // the environment directly. This is a valid deployment style.\n }\n}\ntryLoadDotenv()\n\n/**\n * Build the base env schema. Memoized so repeated calls reuse one\n * schema instance (matters for the `cachedSchema === s` identity check\n * in {@link loadEnv}). Loads zod lazily via {@link getZod}.\n */\n// Build the concrete base schema. This function is the single source of\n// the *type* of `baseEnvSchema` (via `ReturnType<typeof buildBaseEnvSchema>`)\n// AND its runtime value (via `getBaseEnvSchema`). It is NEVER called at\n// module load — only lazily through `getBaseEnvSchema()` — so referencing\n// its return type stays compile-time-only and pulls no eager zod import.\n// Spelling the type this way (rather than `z.ZodObject<any>`) preserves the\n// precise PORT/NODE_ENV/LOG_LEVEL shape, which `defineEnv` composition and\n// the `kick typegen` `KickEnv` inference depend on.\nfunction buildBaseEnvSchema() {\n const z = getZod()\n return z.object({\n // Server\n PORT: z.coerce.number().default(3000),\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n LOG_LEVEL: z.string().default('info'),\n })\n}\n\n/** Precise type of the base env schema — no runtime value, no eager zod. */\nexport type BaseEnvSchema = ReturnType<typeof buildBaseEnvSchema>\n\n/**\n * Memoized base schema. Reuses one instance so the `cachedSchema === s`\n * identity check in {@link loadEnv} holds. Loads zod lazily.\n */\nlet _baseEnvSchema: BaseEnvSchema | undefined\nexport function getBaseEnvSchema(): BaseEnvSchema {\n if (!_baseEnvSchema) _baseEnvSchema = buildBaseEnvSchema()\n return _baseEnvSchema\n}\n\n/**\n * Base environment schema with common server variables.\n * Users extend this with their own application-specific vars.\n *\n * Back-compat lazy view over {@link getBaseEnvSchema}: constructing the\n * Proxy does NOT load zod — zod is only required the moment a property\n * (`.extend`, `.parse`, `.shape`, …) is actually accessed. This keeps\n * `import { baseEnvSchema } from '@forinda/kickjs'` zero-cost for apps\n * that don't use the Zod env path, while keeping the precise schema type.\n */\nexport const baseEnvSchema: BaseEnvSchema = new Proxy({} as BaseEnvSchema, {\n get(_t, prop) {\n const real = getBaseEnvSchema() as unknown as Record<string | symbol, unknown>\n const value = real[prop]\n // Bind methods to the real schema so zod's internal `this` works.\n return typeof value === 'function' ? (value as (...a: unknown[]) => unknown).bind(real) : value\n },\n has(_t, prop) {\n return prop in (getBaseEnvSchema() as object)\n },\n}) as BaseEnvSchema\n\n/** Cached env config to avoid re-parsing — keyed by schema reference */\nlet cachedEnv: any = null\nlet cachedSchema: any = null\n\n/**\n * Define a custom env schema. The base schema (`PORT`, `NODE_ENV`,\n * `LOG_LEVEL`) is always merged in, so users never need to remember to\n * extend it themselves — both styles below produce the same result and\n * both surface the base keys in `KickEnv`.\n *\n * Returns a Zod schema that validates `process.env`.\n *\n * @example\n * ```ts\n * // Style A — return a fresh object; base fields are merged automatically\n * export default defineEnv(() =>\n * z.object({\n * DATABASE_URL: z.string().url(),\n * JWT_SECRET: z.string().min(32),\n * })\n * )\n *\n * // Style B — explicitly extend `base` (still works, identical result)\n * export default defineEnv((base) =>\n * base.extend({\n * DATABASE_URL: z.string().url(),\n * })\n * )\n * ```\n *\n * User-defined keys override base keys when they collide, so a project\n * can re-type `PORT` or tighten `NODE_ENV` without losing the rest of\n * the base shape.\n */\nexport function defineEnv<T extends z.ZodRawShape>(\n extend: (base: typeof baseEnvSchema) => z.ZodObject<T>,\n): z.ZodObject<typeof baseEnvSchema.shape & T> {\n const base = getBaseEnvSchema()\n const userSchema = extend(base as typeof baseEnvSchema)\n // Always merge the base in. `extend` lets the user's shape override\n // any colliding base keys, which preserves the escape hatch for\n // projects that want to re-type `PORT` etc.\n return base.extend(userSchema.shape) as z.ZodObject<typeof baseEnvSchema.shape & T>\n}\n\n/**\n * Parse and validate `process.env` against a Zod schema. Caches result per schema.\n *\n * **No-arg behaviour is sticky.** The first time `loadEnv(schema)` is called\n * with an extended schema, the cache holds that schema. Subsequent\n * `loadEnv()` calls (no arg) will reuse the cached extended schema rather\n * than falling back to `baseEnvSchema`.\n *\n * Stickiness matters because `ConfigService` and the `@Value()` decorator\n * both read through the no-arg form internally. Without it, resolving\n * either of them after a `loadEnv(extendedSchema)` call would silently\n * downgrade the env to the base shape and drop every user-defined key.\n *\n * To force a re-parse with a different schema, pass the new schema explicitly.\n * To start fresh, call `resetEnvCache()` (full reset) or `reloadEnv()`\n * (re-parse against the same schema after a `.env` file change).\n *\n * **Typing:**\n * - With an explicit schema → returns `z.infer<typeof schema>`.\n * - With no schema and `KickEnv` populated by typegen → returns\n * `KickEnv` (the project-wide typed env shape).\n * - With no schema and `KickEnv` empty → returns the base `Env`.\n */\nexport function loadEnv<T extends z.ZodObject<any>>(schema: T): z.infer<T>\nexport function loadEnv(): [EnvKey] extends [never] ? Env : KickEnv\nexport function loadEnv<T extends z.ZodObject<any>>(schema?: T): any {\n // Sticky: when called with no arg, prefer the most recently cached\n // schema over re-parsing with the base schema.\n const s = schema ?? cachedSchema ?? getBaseEnvSchema()\n // Re-parse if schema changed or no cache yet\n if (cachedEnv && cachedSchema === s) return cachedEnv\n cachedSchema = s\n cachedEnv = s.parse(process.env)\n\n // Register env resolver so @Value() reads validated values\n Container._envResolver = (key: string) => cachedEnv?.[key]\n\n return cachedEnv\n}\n\n/**\n * Get a single typed environment variable value.\n *\n * Three forms:\n * - **No-arg, KickEnv populated** (`kick typegen` has run) → key is\n * constrained to known `KickEnv` keys; return type inferred from\n * the schema.\n * - **With explicit schema** → key constrained to that schema's keys;\n * return type is `z.infer<typeof schema>[K]`.\n * - **No-arg, KickEnv empty** → loose `string` key, `any` return.\n *\n * @example\n * ```ts\n * // After `kick typegen` (KickEnv populated):\n * const port = getEnv('PORT') // typed as number\n * const url = getEnv('DATABASE_URL') // typed as string\n *\n * // With an inline schema (legacy / one-off):\n * const dbUrl = getEnv('DATABASE_URL', envSchema)\n * ```\n */\nexport function getEnv<K extends EnvKey>(key: K): KickEnv[K]\nexport function getEnv<T extends z.ZodObject<any>, K extends string & keyof z.infer<T>>(\n key: K,\n schema: T,\n): z.infer<T>[K]\nexport function getEnv(key: string, schema?: z.ZodObject<any>): any {\n const env = schema ? loadEnv(schema) : (loadEnv() as Record<string, any>)\n return env[key]\n}\n\n/**\n * Reload env from `process.env`. Re-reads `.env` via dotenv (if\n * installed), then re-parses the result against the **same** schema\n * the project registered at startup. Called during HMR rebuild and by\n * `envWatchPlugin` to pick up `.env` file changes mid-session.\n *\n * Crucially, this preserves the registered schema. Wiping it would\n * silently downgrade `ConfigService` and `@Value()` back to the base\n * schema on the next read, so every user-defined key would start\n * resolving as `undefined` after the first `.env` save — which was the\n * original \"config disappears in dev\" bug.\n *\n * If you need to fully forget the schema (e.g. tests that swap schemas\n * between cases), call {@link resetEnvCache} instead.\n */\nexport function reloadEnv(): void {\n // Re-read .env file into process.env if dotenv is installed.\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require('dotenv').config({ override: true, quiet: true })\n } catch {\n // dotenv not installed — nothing to reload, env comes from the\n // environment directly.\n }\n\n // Drop the parsed snapshot but keep the schema. Re-parse eagerly so\n // existing consumers (including ConfigService getters and @Value\n // resolvers that may already hold references to the env state) see\n // the new values on their very next read instead of having to\n // re-trigger any cache-population step.\n cachedEnv = null\n if (cachedSchema) {\n if (typeof cachedSchema.parse === 'function') {\n loadEnv(cachedSchema)\n } else {\n loadEnvFromSchema(cachedSchema)\n }\n }\n}\n\n/**\n * Fully reset the env cache, including the registered schema. Use this\n * in tests that need to swap schemas between cases — `reloadEnv()`\n * deliberately preserves the schema to avoid the \"user keys vanish on\n * `.env` reload\" footgun.\n */\nexport function resetEnvCache(): void {\n cachedEnv = null\n cachedSchema = null\n}\n\n/**\n * Schema-agnostic env loader. Accepts any schema supported by\n * `@forinda/kickjs-schema` (Zod, Valibot, Yup, Joi, Standard Schema,\n * KickSchema adapters, or plain validator functions).\n *\n * Unlike `loadEnv()` which uses Zod's `.parse()` and throws a\n * `ZodError`, this function uses `detectSchema().safeParse()` and\n * throws a plain `Error` with structured issue messages on failure.\n *\n * @example\n * ```ts\n * import * as v from 'valibot'\n * import { fromValibot } from '@forinda/kickjs-schema/valibot'\n *\n * const envSchema = fromValibot(v.object({\n * PORT: v.pipe(v.string(), v.transform(Number)),\n * NODE_ENV: v.picklist(['development', 'production', 'test']),\n * }))\n *\n * export const env = loadEnvFromSchema(envSchema)\n * ```\n */\n/**\n * Single inferring signature. `TSchema` is inferred from the call site;\n * the conditional return type maps the unknown-input case (TSchema\n * resolves to `unknown` only when the argument is `unknown`-typed\n * because nothing on the value side carried a richer brand) to\n * `Record<string, unknown>` rather than letting `InferSchemaOutput`\n * fall through to `unknown`. The previous two-overload form had the\n * generic overload win for `unknown`-typed inputs (`unknown` is\n * assignable to any `TSchema`), so the documented \"Record\" fallback\n * never actually applied.\n */\nexport function loadEnvFromSchema<TSchema>(\n schema: TSchema,\n): unknown extends TSchema ? Record<string, unknown> : InferSchemaOutput<TSchema>\nexport function loadEnvFromSchema(schema: unknown): unknown {\n const wrapped = detectSchema(schema)\n const result = wrapped.safeParse(process.env)\n if (!result.success) {\n const details = result.issues\n .map((i) => ` ${i.path.join('.') || '(root)'}: ${i.message}`)\n .join('\\n')\n throw new Error(`Environment validation failed:\\n${details}`)\n }\n cachedEnv = result.data\n cachedSchema = schema\n Container._envResolver = (key: string) => cachedEnv?.[key]\n return result.data\n}\n\n/**\n * Output type of {@link baseEnvSchema}. Hand-written (rather than\n * `z.infer<typeof baseEnvSchema>`) so the type is available without the\n * eager `zod` value import — `baseEnvSchema` is now a lazy Proxy typed\n * as `z.ZodObject<any>`, from which `z.infer` could not recover the\n * precise shape anyway.\n */\nexport type Env = {\n PORT: number\n NODE_ENV: 'development' | 'production' | 'test'\n LOG_LEVEL: string\n}\n"],"mappings":";;;;;;;;;;kKAmBA,IAAI,KACJ,SAAS,QAA+B,CACtC,GAAI,KAAM,OAAO,KACjB,GAAI,CAEF,KAAA,UAAe,KAAK,CACtB,MAAQ,CACN,MAAU,MACR,gUAKF,CACF,CACA,OAAO,IACT,CAiBA,SAAS,eAAsB,CAC7B,GAAI,CAMF,UAJuB,QAIlB,CAAC,CAAC,OAAO,CAAE,SAAU,GAAO,MAAO,EAAK,CAAC,CAChD,MAAQ,CAGR,CACF,CACA,cAAc,EAed,SAAS,oBAAqB,CAC5B,IAAM,EAAI,OAAO,EACjB,OAAO,EAAE,OAAO,CAEd,KAAM,EAAE,OAAO,OAAO,CAAC,CAAC,QAAQ,GAAI,EACpC,SAAU,EAAE,KAAK,CAAC,cAAe,aAAc,MAAM,CAAC,CAAC,CAAC,QAAQ,aAAa,EAC7E,UAAW,EAAE,OAAO,CAAC,CAAC,QAAQ,MAAM,CACtC,CAAC,CACH,CASA,IAAI,eACJ,SAAgB,kBAAkC,CAEhD,MADA,CAAqB,iBAAiB,mBAAmB,EAClD,cACT,CAYA,MAAa,cAA+B,IAAI,MAAM,CAAC,EAAoB,CACzE,IAAI,GAAI,KAAM,CACZ,IAAM,KAAO,iBAAiB,EACxB,MAAQ,KAAK,MAEnB,OAAO,OAAO,OAAU,WAAc,MAAuC,KAAK,IAAI,EAAI,KAC5F,EACA,IAAI,GAAI,KAAM,CACZ,OAAO,QAAS,iBAAiB,CACnC,CACF,CAAC,EAGD,IAAI,UAAiB,KACjB,aAAoB,KAgCxB,SAAgB,UACd,OAC6C,CAC7C,IAAM,KAAO,iBAAiB,EACxB,WAAa,OAAO,IAA4B,EAItD,OAAO,KAAK,OAAO,WAAW,KAAK,CACrC,CA2BA,SAAgB,QAAoC,OAAiB,CAGnE,IAAM,EAAI,QAAU,cAAgB,iBAAiB,EASrD,OAPI,WAAa,eAAiB,EAAU,WAC5C,aAAe,EACf,UAAY,EAAE,MAAM,QAAQ,GAAG,EAG/B,UAAU,aAAgB,KAAgB,YAAY,KAE/C,UACT,CA4BA,SAAgB,OAAO,IAAa,OAAgC,CAElE,OADY,OAAS,QAAQ,MAAM,EAAK,QAAQ,EAAA,CACrC,IACb,CAiBA,SAAgB,WAAkB,CAEhC,GAAI,CAEF,UAAQ,QAAQ,CAAC,CAAC,OAAO,CAAE,SAAU,GAAM,MAAO,EAAK,CAAC,CAC1D,MAAQ,CAGR,CAOA,UAAY,KACR,eACE,OAAO,aAAa,OAAU,WAChC,QAAQ,YAAY,EAEpB,kBAAkB,YAAY,EAGpC,CAQA,SAAgB,eAAsB,CACpC,UAAY,KACZ,aAAe,IACjB,CAsCA,SAAgB,kBAAkB,OAA0B,CAE1D,IAAM,OADU,aAAa,MACR,CAAC,CAAC,UAAU,QAAQ,GAAG,EAC5C,GAAI,CAAC,OAAO,QAAS,CACnB,IAAM,QAAU,OAAO,OACpB,IAAK,GAAM,KAAK,EAAE,KAAK,KAAK,GAAG,GAAK,SAAS,IAAI,EAAE,SAAS,CAAC,CAC7D,KAAK;CAAI,EACZ,MAAU,MAAM,mCAAmC,SAAS,CAC9D,CAIA,MAHA,WAAY,OAAO,KACnB,aAAe,OACf,UAAU,aAAgB,KAAgB,YAAY,KAC/C,OAAO,IAChB"}
1
+ {"version":3,"file":"env-Dgtjncd5.mjs","names":[],"sources":["../src/config/env.ts"],"sourcesContent":["import type { z } from 'zod'\nimport { detectSchema, type InferSchemaOutput } from '@forinda/kickjs-schema'\nimport { Container } from '../core/container'\nimport type { EnvKey } from '../core/decorators'\n\n/**\n * Lazy `zod` accessor. `zod` is an **optional** peer of\n * `@forinda/kickjs` — only the Zod-based env helpers (`baseEnvSchema`,\n * `defineEnv`, `loadEnv`) need it. Apps that validate env via Valibot /\n * Yup / Standard Schema go through `loadEnvFromSchema` (which only\n * touches the validator-agnostic `detectSchema`) and never load zod.\n *\n * Importing `zod` at module top-level made it a *hard* dependency of\n * the whole framework: `import { anything } from '@forinda/kickjs'`\n * eagerly evaluated this module, so a non-Zod app with no `zod`\n * installed crashed at module load / build time (\"Cannot find module\n * 'zod'\"). Deferring the require to first use keeps the import out of\n * the eager graph.\n */\nlet _zod: typeof import('zod') | undefined\nfunction getZod(): typeof import('zod') {\n if (_zod) return _zod\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n _zod = require('zod') as typeof import('zod')\n } catch {\n throw new Error(\n \"@forinda/kickjs: the 'zod' peer dependency is required for \" +\n 'baseEnvSchema / defineEnv / loadEnv but is not installed. ' +\n 'Install it (`pnpm add zod`), or define your env with a non-Zod ' +\n 'schema and call loadEnvFromSchema() instead — @forinda/kickjs-schema ' +\n 'supports Valibot, Yup, and any Standard Schema validator without zod.',\n )\n }\n return _zod\n}\n\n/**\n * Lazily load `.env` via dotenv if it is installed in the consumer app.\n * `dotenv` is an **optional peer dependency** of `@forinda/kickjs` —\n * apps that want `.env` file support install it themselves (the CLI\n * generators add it to every scaffolded `package.json`). Apps that\n * inject env via the shell, Docker, or a secret manager can skip it\n * entirely and `process.env` is read as-is.\n *\n * We call `dotenv.config()` directly rather than `require('dotenv/config')`\n * so that a re-evaluation of this module (e.g. Vite SSR full-reload after\n * editing `.env`) can re-read the file. `dotenv/config` is a side-effect\n * shim that CJS caches aggressively, so the second `require` is a no-op\n * and new keys never reach `process.env` — the root cause of the\n * \"DATABASE_URL undefined until restart\" bug on Windows dev.\n */\nfunction tryLoadDotenv(): void {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const dotenv = require('dotenv')\n // `quiet: true` suppresses dotenv 17's tip banner that goes to stdout.\n // Required for CLI tools whose stdout is parsed by callers (`kick explain\n // --json`, etc) — the banner would otherwise corrupt the JSON output.\n dotenv.config({ override: false, quiet: true })\n } catch {\n // dotenv not installed — fall through, env vars must come from\n // the environment directly. This is a valid deployment style.\n }\n}\ntryLoadDotenv()\n\n/**\n * Build the base env schema. Memoized so repeated calls reuse one\n * schema instance (matters for the `cachedSchema === s` identity check\n * in {@link loadEnv}). Loads zod lazily via {@link getZod}.\n */\n// Build the concrete base schema. This function is the single source of\n// the *type* of `baseEnvSchema` (via `ReturnType<typeof buildBaseEnvSchema>`)\n// AND its runtime value (via `getBaseEnvSchema`). It is NEVER called at\n// module load — only lazily through `getBaseEnvSchema()` — so referencing\n// its return type stays compile-time-only and pulls no eager zod import.\n// Spelling the type this way (rather than `z.ZodObject<any>`) preserves the\n// precise PORT/NODE_ENV/LOG_LEVEL shape, which `defineEnv` composition and\n// the `kick typegen` `KickEnv` inference depend on.\nfunction buildBaseEnvSchema() {\n const z = getZod()\n return z.object({\n // Server\n PORT: z.coerce.number().default(3000),\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n LOG_LEVEL: z.string().default('info'),\n })\n}\n\n/** Precise type of the base env schema — no runtime value, no eager zod. */\nexport type BaseEnvSchema = ReturnType<typeof buildBaseEnvSchema>\n\n/**\n * Memoized base schema. Reuses one instance so the `cachedSchema === s`\n * identity check in {@link loadEnv} holds. Loads zod lazily.\n */\nlet _baseEnvSchema: BaseEnvSchema | undefined\nexport function getBaseEnvSchema(): BaseEnvSchema {\n if (!_baseEnvSchema) _baseEnvSchema = buildBaseEnvSchema()\n return _baseEnvSchema\n}\n\n/**\n * Base environment schema with common server variables.\n * Users extend this with their own application-specific vars.\n *\n * Back-compat lazy view over {@link getBaseEnvSchema}: constructing the\n * Proxy does NOT load zod — zod is only required the moment a property\n * (`.extend`, `.parse`, `.shape`, …) is actually accessed. This keeps\n * `import { baseEnvSchema } from '@forinda/kickjs'` zero-cost for apps\n * that don't use the Zod env path, while keeping the precise schema type.\n */\nexport const baseEnvSchema: BaseEnvSchema = new Proxy({} as BaseEnvSchema, {\n get(_t, prop) {\n const real = getBaseEnvSchema() as unknown as Record<string | symbol, unknown>\n const value = real[prop]\n // Bind methods to the real schema so zod's internal `this` works.\n return typeof value === 'function' ? (value as (...a: unknown[]) => unknown).bind(real) : value\n },\n has(_t, prop) {\n return prop in (getBaseEnvSchema() as object)\n },\n}) as BaseEnvSchema\n\n/** Cached env config to avoid re-parsing — keyed by schema reference */\nlet cachedEnv: any = null\nlet cachedSchema: any = null\n\n/**\n * Define a custom env schema. The base schema (`PORT`, `NODE_ENV`,\n * `LOG_LEVEL`) is always merged in, so users never need to remember to\n * extend it themselves — both styles below produce the same result and\n * both surface the base keys in `KickEnv`.\n *\n * Returns a Zod schema that validates `process.env`.\n *\n * @example\n * ```ts\n * // Style A — return a fresh object; base fields are merged automatically\n * export default defineEnv(() =>\n * z.object({\n * DATABASE_URL: z.string().url(),\n * JWT_SECRET: z.string().min(32),\n * })\n * )\n *\n * // Style B — explicitly extend `base` (still works, identical result)\n * export default defineEnv((base) =>\n * base.extend({\n * DATABASE_URL: z.string().url(),\n * })\n * )\n * ```\n *\n * User-defined keys override base keys when they collide, so a project\n * can re-type `PORT` or tighten `NODE_ENV` without losing the rest of\n * the base shape.\n */\nexport function defineEnv<T extends z.ZodRawShape>(\n extend: (base: typeof baseEnvSchema) => z.ZodObject<T>,\n): z.ZodObject<typeof baseEnvSchema.shape & T> {\n const base = getBaseEnvSchema()\n const userSchema = extend(base as typeof baseEnvSchema)\n // Always merge the base in. `extend` lets the user's shape override\n // any colliding base keys, which preserves the escape hatch for\n // projects that want to re-type `PORT` etc.\n return base.extend(userSchema.shape) as z.ZodObject<typeof baseEnvSchema.shape & T>\n}\n\n/**\n * Parse and validate `process.env` against a Zod schema. Caches result per schema.\n *\n * **No-arg behaviour is sticky.** The first time `loadEnv(schema)` is called\n * with an extended schema, the cache holds that schema. Subsequent\n * `loadEnv()` calls (no arg) will reuse the cached extended schema rather\n * than falling back to `baseEnvSchema`.\n *\n * Stickiness matters because `ConfigService` and the `@Value()` decorator\n * both read through the no-arg form internally. Without it, resolving\n * either of them after a `loadEnv(extendedSchema)` call would silently\n * downgrade the env to the base shape and drop every user-defined key.\n *\n * To force a re-parse with a different schema, pass the new schema explicitly.\n * To start fresh, call `resetEnvCache()` (full reset) or `reloadEnv()`\n * (re-parse against the same schema after a `.env` file change).\n *\n * **Typing:**\n * - With an explicit schema → returns `z.infer<typeof schema>`.\n * - With no schema and `KickEnv` populated by typegen → returns\n * `KickEnv` (the project-wide typed env shape).\n * - With no schema and `KickEnv` empty → returns the base `Env`.\n */\nexport function loadEnv<T extends z.ZodObject<any>>(schema: T): z.infer<T>\nexport function loadEnv(): [EnvKey] extends [never] ? Env : KickEnv\nexport function loadEnv<T extends z.ZodObject<any>>(schema?: T): any {\n // Sticky: when called with no arg, prefer the most recently cached\n // schema over re-parsing with the base schema.\n const s = schema ?? cachedSchema ?? getBaseEnvSchema()\n // Re-parse if schema changed or no cache yet\n if (cachedEnv && cachedSchema === s) return cachedEnv\n cachedSchema = s\n cachedEnv = s.parse(process.env)\n\n // Register env resolver so @Value() reads validated values\n Container._envResolver = (key: string) => cachedEnv?.[key]\n\n return cachedEnv\n}\n\n/**\n * Get a single typed environment variable value.\n *\n * Three forms:\n * - **No-arg, KickEnv populated** (`kick typegen` has run) → key is\n * constrained to known `KickEnv` keys; return type inferred from\n * the schema.\n * - **With explicit schema** → key constrained to that schema's keys;\n * return type is `z.infer<typeof schema>[K]`.\n * - **No-arg, KickEnv empty** → loose `string` key, `any` return.\n *\n * @example\n * ```ts\n * // After `kick typegen` (KickEnv populated):\n * const port = getEnv('PORT') // typed as number\n * const url = getEnv('DATABASE_URL') // typed as string\n *\n * // With an inline schema (legacy / one-off):\n * const dbUrl = getEnv('DATABASE_URL', envSchema)\n * ```\n */\nexport function getEnv<K extends EnvKey>(key: K): KickEnv[K]\nexport function getEnv<T extends z.ZodObject<any>, K extends string & keyof z.infer<T>>(\n key: K,\n schema: T,\n): z.infer<T>[K]\nexport function getEnv(key: string, schema?: z.ZodObject<any>): any {\n const env = schema ? loadEnv(schema) : (loadEnv() as Record<string, any>)\n return env[key]\n}\n\n/**\n * Reload env from `process.env`. Re-reads `.env` via dotenv (if\n * installed), then re-parses the result against the **same** schema\n * the project registered at startup. Called during HMR rebuild and by\n * `envWatchPlugin` to pick up `.env` file changes mid-session.\n *\n * Crucially, this preserves the registered schema. Wiping it would\n * silently downgrade `ConfigService` and `@Value()` back to the base\n * schema on the next read, so every user-defined key would start\n * resolving as `undefined` after the first `.env` save — which was the\n * original \"config disappears in dev\" bug.\n *\n * If you need to fully forget the schema (e.g. tests that swap schemas\n * between cases), call {@link resetEnvCache} instead.\n */\nexport function reloadEnv(): void {\n // Re-read .env file into process.env if dotenv is installed.\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require('dotenv').config({ override: true, quiet: true })\n } catch {\n // dotenv not installed — nothing to reload, env comes from the\n // environment directly.\n }\n\n // Drop the parsed snapshot but keep the schema. Re-parse eagerly so\n // existing consumers (including ConfigService getters and @Value\n // resolvers that may already hold references to the env state) see\n // the new values on their very next read instead of having to\n // re-trigger any cache-population step.\n cachedEnv = null\n if (cachedSchema) {\n if (typeof cachedSchema.parse === 'function') {\n loadEnv(cachedSchema)\n } else {\n loadEnvFromSchema(cachedSchema)\n }\n }\n}\n\n/**\n * Fully reset the env cache, including the registered schema. Use this\n * in tests that need to swap schemas between cases — `reloadEnv()`\n * deliberately preserves the schema to avoid the \"user keys vanish on\n * `.env` reload\" footgun.\n */\nexport function resetEnvCache(): void {\n cachedEnv = null\n cachedSchema = null\n}\n\n/**\n * Schema-agnostic env loader. Accepts any schema supported by\n * `@forinda/kickjs-schema` (Zod, Valibot, Yup, Joi, Standard Schema,\n * KickSchema adapters, or plain validator functions).\n *\n * Unlike `loadEnv()` which uses Zod's `.parse()` and throws a\n * `ZodError`, this function uses `detectSchema().safeParse()` and\n * throws a plain `Error` with structured issue messages on failure.\n *\n * @example\n * ```ts\n * import * as v from 'valibot'\n * import { fromValibot } from '@forinda/kickjs-schema/valibot'\n *\n * const envSchema = fromValibot(v.object({\n * PORT: v.pipe(v.string(), v.transform(Number)),\n * NODE_ENV: v.picklist(['development', 'production', 'test']),\n * }))\n *\n * export const env = loadEnvFromSchema(envSchema)\n * ```\n */\n/**\n * Single inferring signature. `TSchema` is inferred from the call site;\n * the conditional return type maps the unknown-input case (TSchema\n * resolves to `unknown` only when the argument is `unknown`-typed\n * because nothing on the value side carried a richer brand) to\n * `Record<string, unknown>` rather than letting `InferSchemaOutput`\n * fall through to `unknown`. The previous two-overload form had the\n * generic overload win for `unknown`-typed inputs (`unknown` is\n * assignable to any `TSchema`), so the documented \"Record\" fallback\n * never actually applied.\n */\nexport function loadEnvFromSchema<TSchema>(\n schema: TSchema,\n): unknown extends TSchema ? Record<string, unknown> : InferSchemaOutput<TSchema>\nexport function loadEnvFromSchema(schema: unknown): unknown {\n const wrapped = detectSchema(schema)\n const result = wrapped.safeParse(process.env)\n if (!result.success) {\n const details = result.issues\n .map((i) => ` ${i.path.join('.') || '(root)'}: ${i.message}`)\n .join('\\n')\n throw new Error(`Environment validation failed:\\n${details}`)\n }\n cachedEnv = result.data\n cachedSchema = schema\n Container._envResolver = (key: string) => cachedEnv?.[key]\n return result.data\n}\n\n/**\n * Output type of {@link baseEnvSchema}. Hand-written (rather than\n * `z.infer<typeof baseEnvSchema>`) so the type is available without the\n * eager `zod` value import — `baseEnvSchema` is now a lazy Proxy typed\n * as `z.ZodObject<any>`, from which `z.infer` could not recover the\n * precise shape anyway.\n */\nexport type Env = {\n PORT: number\n NODE_ENV: 'development' | 'production' | 'test'\n LOG_LEVEL: string\n}\n"],"mappings":";;;;;;;;;;kKAmBA,IAAI,KACJ,SAAS,QAA+B,CACtC,GAAI,KAAM,OAAO,KACjB,GAAI,CAEF,KAAA,UAAe,KAAK,CACtB,MAAQ,CACN,MAAU,MACR,gUAKF,CACF,CACA,OAAO,IACT,CAiBA,SAAS,eAAsB,CAC7B,GAAI,CAMF,UAJuB,QAIlB,CAAC,CAAC,OAAO,CAAE,SAAU,GAAO,MAAO,EAAK,CAAC,CAChD,MAAQ,CAGR,CACF,CACA,cAAc,EAed,SAAS,oBAAqB,CAC5B,IAAM,EAAI,OAAO,EACjB,OAAO,EAAE,OAAO,CAEd,KAAM,EAAE,OAAO,OAAO,CAAC,CAAC,QAAQ,GAAI,EACpC,SAAU,EAAE,KAAK,CAAC,cAAe,aAAc,MAAM,CAAC,CAAC,CAAC,QAAQ,aAAa,EAC7E,UAAW,EAAE,OAAO,CAAC,CAAC,QAAQ,MAAM,CACtC,CAAC,CACH,CASA,IAAI,eACJ,SAAgB,kBAAkC,CAEhD,MADA,CAAqB,iBAAiB,mBAAmB,EAClD,cACT,CAYA,MAAa,cAA+B,IAAI,MAAM,CAAC,EAAoB,CACzE,IAAI,GAAI,KAAM,CACZ,IAAM,KAAO,iBAAiB,EACxB,MAAQ,KAAK,MAEnB,OAAO,OAAO,OAAU,WAAc,MAAuC,KAAK,IAAI,EAAI,KAC5F,EACA,IAAI,GAAI,KAAM,CACZ,OAAO,QAAS,iBAAiB,CACnC,CACF,CAAC,EAGD,IAAI,UAAiB,KACjB,aAAoB,KAgCxB,SAAgB,UACd,OAC6C,CAC7C,IAAM,KAAO,iBAAiB,EACxB,WAAa,OAAO,IAA4B,EAItD,OAAO,KAAK,OAAO,WAAW,KAAK,CACrC,CA2BA,SAAgB,QAAoC,OAAiB,CAGnE,IAAM,EAAI,QAAU,cAAgB,iBAAiB,EASrD,OAPI,WAAa,eAAiB,EAAU,WAC5C,aAAe,EACf,UAAY,EAAE,MAAM,QAAQ,GAAG,EAG/B,UAAU,aAAgB,KAAgB,YAAY,KAE/C,UACT,CA4BA,SAAgB,OAAO,IAAa,OAAgC,CAElE,OADY,OAAS,QAAQ,MAAM,EAAK,QAAQ,EAAA,CACrC,IACb,CAiBA,SAAgB,WAAkB,CAEhC,GAAI,CAEF,UAAQ,QAAQ,CAAC,CAAC,OAAO,CAAE,SAAU,GAAM,MAAO,EAAK,CAAC,CAC1D,MAAQ,CAGR,CAOA,UAAY,KACR,eACE,OAAO,aAAa,OAAU,WAChC,QAAQ,YAAY,EAEpB,kBAAkB,YAAY,EAGpC,CAQA,SAAgB,eAAsB,CACpC,UAAY,KACZ,aAAe,IACjB,CAsCA,SAAgB,kBAAkB,OAA0B,CAE1D,IAAM,OADU,aAAa,MACR,CAAC,CAAC,UAAU,QAAQ,GAAG,EAC5C,GAAI,CAAC,OAAO,QAAS,CACnB,IAAM,QAAU,OAAO,OACpB,IAAK,GAAM,KAAK,EAAE,KAAK,KAAK,GAAG,GAAK,SAAS,IAAI,EAAE,SAAS,CAAC,CAC7D,KAAK;CAAI,EACZ,MAAU,MAAM,mCAAmC,SAAS,CAC9D,CAIA,MAHA,WAAY,OAAO,KACnB,aAAe,OACf,UAAU,aAAgB,KAAgB,YAAY,KAC/C,OAAO,IAChB"}
@@ -14,6 +14,10 @@ declare function notFoundHandler(): (_req: Request, res: Response, _next: NextFu
14
14
  * for example — should pass their own `onError` to `bootstrap()` and decide
15
15
  * the policy explicitly.
16
16
  *
17
+ * Unexpected 500s carry `requestId` in every environment (the correlation
18
+ * handle back to the log line) and, outside production, the error summary
19
+ * plus stack. Production bodies stay opaque.
20
+ *
17
21
  * {@link ProblemException} is dispatched first and emits an
18
22
  * `application/problem+json` response per RFC 9457. Plain
19
23
  * {@link HttpException} keeps the existing `{ message, errors? }` shape
@@ -22,4 +26,4 @@ declare function notFoundHandler(): (_req: Request, res: Response, _next: NextFu
22
26
  declare function errorHandler(): (err: any, req: Request, res: Response, _next: NextFunction) => Response<any, Record<string, any>> | undefined;
23
27
  //#endregion
24
28
  export { notFoundHandler as n, errorHandler as t };
25
- //# sourceMappingURL=error-handler-3ih4E-QZ.d.mts.map
29
+ //# sourceMappingURL=error-handler-DR01K_rI.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-handler-DR01K_rI.d.mts","names":[],"sources":["../src/http/middleware/error-handler.ts"],"mappings":";;;;;iBAYgB,eAAA,KAAe,IAAA,EACf,OAAA,EAAO,GAAA,EAAO,QAAA,EAAQ,KAAA,EAAS,YAAA;AAD/C;;;;;;;;;;;;;;;AAC2D;AAwB3D;;;AAzBA,iBAyBgB,YAAA,KAAY,GAAA,OAEV,GAAA,EAAO,OAAA,EAAO,GAAA,EAAO,QAAA,EAAQ,KAAA,EAAS,YAAA,KAAY,QAAA,MAAA,MAAA"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @forinda/kickjs v6.5.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,describeError}from"./core/logger.mjs";import{i as requestStore,t as RequestContext}from"./context-BS5o1IuU.mjs";import{a as disposeRequestStore,i as createRequestStore,t as applyHandlerResult}from"./reply-BMBvs-FB.mjs";import{validate}from"./http/middleware/validate.mjs";import{t as applyUploadConfig}from"./upload-config-CFfHvoHF.mjs";var WebRequestShim=class{method;url;headers;body;params={};query={};requestId;file;files;signal;constructor(request,url){this.method=request.method,this.url=url.pathname+url.search,this.signal=request.signal;let headers={};request.headers.forEach((value,key)=>{headers[key]=value}),this.headers=headers;let query={};for(let key of url.searchParams.keys()){let all=url.searchParams.getAll(key);query[key]=all.length>1?all:all[0]}this.query=query}once(event,listener){return event===`close`&&this.signal.addEventListener(`abort`,listener,{once:!0}),this}on(event,listener){return this.once(event,listener)}};const encoder=new TextEncoder;function toBytes(chunk){return chunk instanceof Uint8Array?chunk:encoder.encode(String(chunk))}var WebResponseDriver=class{signal;ready;settled=!1;_status=200;headers=new Headers;_headersSent=!1;writer;resolveReady;constructor(signal){this.signal=signal,this.ready=new Promise(resolve=>{this.resolveReady=resolve})}status(code){return this._status=code,this}json(data){return this.headers.has(`content-type`)||this.headers.set(`content-type`,`application/json; charset=utf-8`),this.finish(JSON.stringify(data))}send(data){return typeof data==`object`&&data&&!(data instanceof Uint8Array)?this.json(data):this.finish(data)}type(contentType){return this.headers.set(`content-type`,contentType),this}setHeader(name,value){if(Array.isArray(value)){this.headers.delete(name);for(let v of value)this.headers.append(name,String(v))}else this.headers.set(name,String(value));return this}render(){throw Error(`ctx.render() is not supported on the web runtime (no view engine).`)}writeHead(statusCode,headers){if(this._status=statusCode,headers)for(let[name,value]of Object.entries(headers))this.setHeader(name,value);return this.startStream(),this}flushHeaders(){return this.startStream(),this}write(chunk){return this.startStream(),this.writer.write(toBytes(chunk)).catch(()=>{}),!0}end(data){return this.writer?(data!==void 0&&this.write(data),this.writer.close().catch(()=>{}),this.writer=void 0,this):this.finish(data)}once(event,listener){return event===`close`&&this.signal&&this.signal.addEventListener(`abort`,listener,{once:!0}),this}get headersSent(){return this._headersSent}finish(body){if(this.settled)return this;this.settled=!0,this._headersSent=!0;let bodyAllowed=this._status!==204&&this._status!==304;return this.resolveReady(new Response(bodyAllowed?body??null:null,{status:this._status,headers:this.headers})),this}startStream(){if(this.writer||this.settled)return;let{readable,writable}=new TransformStream;this.writer=writable.getWriter(),this.settled=!0,this._headersSent=!0,this.signal?.addEventListener(`abort`,()=>{this.writer?.abort().catch(()=>{}),this.writer=void 0},{once:!0}),this.resolveReady(new Response(readable,{status:this._status,headers:this.headers}))}};const log=createLogger(`WebPipeline`),NOOP_NEXT=()=>{},BODY_METHODS=new Set([`POST`,`PUT`,`PATCH`,`DELETE`]);function defaultErrorResponse(err,driver){let status=typeof err?.status==`number`?err.status:typeof err?.statusCode==`number`?err.statusCode:500;if(status>=500){log.error(err,`Unhandled error in web pipeline — ${describeError(err)}`);let stack=err?.stack;driver.status(status).json({error:`Internal Server Error`,...isProduction()?{}:{message:describeError(err),...typeof stack==`string`?{stack:stack.split(`
12
+ `)}:{}}});return}driver.status(status).json({error:err?.message??`Request failed`})}function isProduction(){return typeof process<`u`&&process.env?.NODE_ENV===`production`}function compileWebRoute(entry,hooks){let validator=entry.meta.validation?validate(entry.meta.validation):void 0,upload=entry.meta.upload&&entry.meta.upload.mode!==`none`?entry.meta.upload:null;return async({request,url,params})=>{let req=new WebRequestShim(request,url);if(req.params=params,upload){let form=await request.formData().catch(()=>void 0);if(form){let rawParts=[],fields={};for(let[name,value]of form.entries())typeof value==`string`?fields[name]=value:rawParts.push({fieldname:name,filename:value.name??``,mimetype:value.type||`application/octet-stream`,buffer:new Uint8Array(await value.arrayBuffer())});let{file,files}=applyUploadConfig(rawParts,entry.meta.upload);req.file=file,req.files=files,req.body=fields}}else if(BODY_METHODS.has(request.method)){let contentType=req.headers[`content-type`]??``;if(contentType.includes(`application/json`))req.body=await request.json().catch(()=>void 0);else if(contentType.includes(`application/x-www-form-urlencoded`)){let text=await request.text().catch(()=>``);req.body=Object.fromEntries(new URLSearchParams(text))}else contentType.startsWith(`text/`)&&(req.body=await request.text().catch(()=>void 0))}let store=createRequestStore(req.headers[`x-request-id`]);req.requestId=store.requestId;let driver=new WebResponseDriver(request.signal);driver.setHeader(`x-request-id`,store.requestId);let pipeline=requestStore.run(store,async()=>{let ctx=new RequestContext(req,driver,NOOP_NEXT,driver);validator&&await new Promise((resolve,reject)=>{validator(req,void 0,err=>err?reject(err):resolve())});for(let mw of entry.middlewares){let advanced=!1;if(await new Promise((resolve,reject)=>{Promise.resolve(mw(ctx,err=>{advanced=!0,err?reject(err):resolve()})).catch(reject)}),!advanced||driver.settled)return}if(entry.contributorRunner&&await entry.contributorRunner(ctx),driver.settled)return;let result=await entry.handler(ctx);driver.settled||applyHandlerResult(ctx,result)});try{try{await Promise.race([pipeline,driver.ready])}catch(err){driver.settled||(hooks?.onError&&await hooks.onError(err,req,driver),driver.settled||defaultErrorResponse(err,driver))}return driver.settled||driver.status(404).json({error:`Not Found`}),await driver.ready}finally{Promise.resolve(pipeline).catch(()=>{}).then(()=>disposeRequestStore(store))}}}export{WebRequestShim as n,WebResponseDriver as r,compileWebRoute as t};
13
+ //# sourceMappingURL=handler-UvmjQGYc.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-UvmjQGYc.mjs","names":[],"sources":["../src/http/web/driver.ts","../src/http/web/handler.ts"],"sourcesContent":["// Web-standard driver pair — the request/response bridge between web\n// primitives (WHATWG Request/Response) and the KickJS RequestContext surface.\n//\n// Deliberately imports NOTHING from node: this module is the edge-safe core\n// shared by the h3 v2 runtime (`runtimes/h3-web.ts`, node bootstrap) and the\n// `@forinda/kickjs/web` fetch entry (edge/Bun/Deno). See\n// `web-standards-edge-design.md` §3.1.\n\nimport type { RuntimeResponse } from '../runtime'\n\n/**\n * Presents a web `Request` as the express-shaped `req` object that\n * `RequestContext` (and route validation) reads: plain-object `headers`,\n * assignable `body`/`params`/`query`, and `on/once('close')` bridged from\n * the request's `AbortSignal` — the only EventEmitter surface the context\n * layer uses (`ctx.signal`, SSE close).\n */\nexport class WebRequestShim {\n readonly method: string\n readonly url: string\n /** Lower-cased plain header object (RequestContext indexes it directly). */\n readonly headers: Record<string, string>\n body: unknown\n params: Record<string, string> = {}\n query: Record<string, unknown> = {}\n requestId?: string\n file?: unknown\n files?: unknown;\n // Ad-hoc props middleware may set (session, user, ...) — index signature\n // keeps assignment open like a node req object.\n [key: string]: unknown\n\n private readonly signal: AbortSignal\n\n constructor(request: Request, url: URL) {\n this.method = request.method\n // Express-shaped `req.url` is path + search, not the absolute URL.\n this.url = url.pathname + url.search\n this.signal = request.signal\n const headers: Record<string, string> = {}\n request.headers.forEach((value, key) => {\n headers[key] = value\n })\n this.headers = headers\n const query: Record<string, unknown> = {}\n for (const key of url.searchParams.keys()) {\n const all = url.searchParams.getAll(key)\n query[key] = all.length > 1 ? all : all[0]\n }\n this.query = query\n }\n\n /** `req.once('close')` — abort of the underlying request signal. */\n once(event: string, listener: () => void): this {\n if (event === 'close') this.signal.addEventListener('abort', listener, { once: true })\n return this\n }\n\n /** `req.on('close')` — same bridge; signals only abort once anyway. */\n on(event: string, listener: () => void): this {\n return this.once(event, listener)\n }\n}\n\nconst encoder = new TextEncoder()\n\nfunction toBytes(chunk: unknown): Uint8Array {\n if (chunk instanceof Uint8Array) return chunk\n return encoder.encode(String(chunk))\n}\n\n/**\n * `RuntimeResponse` over a web `Response`.\n *\n * Two modes:\n * - **Buffered** (default): `status/json/send/type/setHeader` record state;\n * the terminal call resolves {@link ready} with a complete `Response`.\n * - **Streaming**: the first `writeHead`/`flushHeaders`/`write` call switches\n * to a `TransformStream` and resolves {@link ready} IMMEDIATELY with a\n * streamed `Response` — SSE works on edge this way; `write` feeds the\n * writer, `end` closes it.\n *\n * A class so the method table lives on a shared prototype (one allocation\n * per request — same rationale as the Fastify/h3 driver classes).\n */\nexport class WebResponseDriver implements RuntimeResponse {\n /** Resolves with the final (or streamed) Response. */\n readonly ready: Promise<Response>\n /** True once a terminal/streaming action produced the Response. */\n settled = false\n\n private _status = 200\n private readonly headers = new Headers()\n private _headersSent = false\n private writer?: WritableStreamDefaultWriter<Uint8Array>\n private resolveReady!: (response: Response) => void\n\n constructor(private readonly signal?: AbortSignal) {\n this.ready = new Promise<Response>((resolve) => {\n this.resolveReady = resolve\n })\n }\n\n status(code: number): this {\n this._status = code\n return this\n }\n\n json(data: unknown): this {\n if (!this.headers.has('content-type'))\n this.headers.set('content-type', 'application/json; charset=utf-8')\n return this.finish(JSON.stringify(data))\n }\n\n send(data: unknown): this {\n if (\n data !== undefined &&\n data !== null &&\n typeof data === 'object' &&\n !(data instanceof Uint8Array)\n ) {\n return this.json(data)\n }\n return this.finish(data as string | Uint8Array | null | undefined)\n }\n\n type(contentType: string): this {\n this.headers.set('content-type', contentType)\n return this\n }\n\n setHeader(name: string, value: unknown): this {\n if (Array.isArray(value)) {\n this.headers.delete(name)\n for (const v of value) this.headers.append(name, String(v))\n } else {\n this.headers.set(name, String(value))\n }\n return this\n }\n\n render(): never {\n throw new Error('ctx.render() is not supported on the web runtime (no view engine).')\n }\n\n writeHead(statusCode: number, headers?: Record<string, string | number | string[]>): this {\n this._status = statusCode\n if (headers) {\n for (const [name, value] of Object.entries(headers)) this.setHeader(name, value)\n }\n this.startStream()\n return this\n }\n\n flushHeaders(): this {\n this.startStream()\n return this\n }\n\n write(chunk: unknown): boolean {\n this.startStream()\n // Fire-and-forget: backpressure is absorbed by the stream's queue. Write\n // failures (client gone) surface via the abort listener, not here.\n void this.writer!.write(toBytes(chunk)).catch(() => {})\n return true\n }\n\n end(data?: unknown): this {\n if (this.writer) {\n if (data !== undefined) this.write(data)\n void this.writer.close().catch(() => {})\n this.writer = undefined\n return this\n }\n return this.finish(data as string | Uint8Array | null | undefined)\n }\n\n once(event: string, listener: (...args: unknown[]) => void): this {\n if (event === 'close' && this.signal)\n this.signal.addEventListener('abort', listener as () => void, { once: true })\n return this\n }\n\n get headersSent(): boolean {\n return this._headersSent\n }\n\n /** Buffered terminal: build the final Response exactly once. */\n private finish(body?: string | Uint8Array | null): this {\n if (this.settled) return this\n this.settled = true\n this._headersSent = true\n // 204/304 must not carry a body per the Response constructor contract.\n const bodyAllowed = this._status !== 204 && this._status !== 304\n this.resolveReady(\n new Response(bodyAllowed ? (body ?? null) : null, {\n status: this._status,\n headers: this.headers,\n }),\n )\n return this\n }\n\n /** Switch to streaming mode and resolve `ready` with the streamed Response. */\n private startStream(): void {\n if (this.writer || this.settled) return\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()\n this.writer = writable.getWriter()\n this.settled = true\n this._headersSent = true\n // Abort from the client tears the stream down so the writer stops queueing.\n this.signal?.addEventListener(\n 'abort',\n () => {\n void this.writer?.abort().catch(() => {})\n this.writer = undefined\n },\n { once: true },\n )\n this.resolveReady(new Response(readable, { status: this._status, headers: this.headers }))\n }\n}\n","// Engine-neutral web pipeline: run one KickJS RouteEntry against a WHATWG\n// Request and produce a WHATWG Response. Shared by the h3 v2 runtime\n// (node bootstrap) and the `@forinda/kickjs/web` fetch entry (edge/Bun/Deno).\n// Edge-safe: no node imports beyond the sanctioned ALS request store.\n\nimport { RequestContext } from '../context'\nimport { applyHandlerResult } from '../reply'\nimport { requestStore } from '../request-store'\nimport { createRequestStore, disposeRequestStore } from '../middleware/request-scope'\nimport { validate } from '../middleware/validate'\nimport { applyUploadConfig, type RawUploadPart } from '../middleware/upload-config'\nimport type { RouteEntry, RuntimeResponse } from '../runtime'\nimport { WebRequestShim, WebResponseDriver } from './driver'\nimport { createLogger, describeError } from '../../core/logger'\n\nconst log = createLogger('WebPipeline')\nconst NOOP_NEXT = (): void => {}\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nexport interface WebRouteInvocation {\n request: Request\n url: URL\n params: Record<string, string>\n}\n\nexport interface WebRouteHooks {\n /**\n * Error bridge — receives pipeline rejections (validation failures,\n * handler throws) with the shim + driver so an engine can dispatch the\n * framework's connect-style error handler. When absent (or when it\n * doesn't settle the driver), a minimal JSON error is produced from\n * `err.status`/`err.statusCode` (500 default).\n */\n onError?: (err: unknown, req: WebRequestShim, res: WebResponseDriver) => void | Promise<void>\n}\n\n/**\n * Fallback error shape when no engine error handler settles the response.\n *\n * This path used to be completely silent: a bare\n * `{ error: 'Internal Server Error' }` with no log line anywhere, so a\n * failure that reached here left no trace on either side. It is a\n * last-resort branch, which is exactly when diagnostics matter most.\n */\nfunction defaultErrorResponse(err: unknown, driver: WebResponseDriver): void {\n const status =\n typeof (err as { status?: number })?.status === 'number'\n ? (err as { status: number }).status\n : typeof (err as { statusCode?: number })?.statusCode === 'number'\n ? (err as { statusCode: number }).statusCode\n : 500\n\n if (status >= 500) {\n log.error(err, `Unhandled error in web pipeline — ${describeError(err)}`)\n const stack = (err as Error)?.stack\n driver.status(status).json({\n error: 'Internal Server Error',\n ...(isProduction()\n ? {}\n : {\n message: describeError(err),\n ...(typeof stack === 'string' ? { stack: stack.split('\\n') } : {}),\n }),\n })\n return\n }\n\n driver.status(status).json({ error: (err as Error)?.message ?? 'Request failed' })\n}\n\n/**\n * Read per-call, not at module scope: the web entry is imported once and\n * reused across invocations, and edge runtimes may not expose `process`.\n */\nfunction isProduction(): boolean {\n return typeof process !== 'undefined' && process.env?.NODE_ENV === 'production'\n}\n\n/**\n * Compile a RouteEntry into a `(invocation) => Promise<Response>` function.\n * Everything derivable at route-build time (validator, upload config) is\n * hoisted here — per-request work is shim + store + pipeline only.\n */\nexport function compileWebRoute(\n entry: RouteEntry,\n hooks?: WebRouteHooks,\n): (invocation: WebRouteInvocation) => Promise<Response> {\n const validator = entry.meta.validation ? validate(entry.meta.validation) : undefined\n const upload = entry.meta.upload && entry.meta.upload.mode !== 'none' ? entry.meta.upload : null\n\n return async ({ request, url, params }) => {\n const req = new WebRequestShim(request, url)\n req.params = params\n\n // Body: uploads consume the stream as FormData; everything else parses\n // by content-type. A failed parse leaves body undefined (validation\n // rejects it downstream if the route demands a shape).\n if (upload) {\n const form = await request.formData().catch(() => undefined)\n if (form) {\n const rawParts: RawUploadPart[] = []\n const fields: Record<string, unknown> = {}\n for (const [name, value] of form.entries()) {\n if (typeof value === 'string') {\n fields[name] = value\n } else {\n rawParts.push({\n fieldname: name,\n filename: value.name ?? '',\n mimetype: value.type || 'application/octet-stream',\n buffer: new Uint8Array(await value.arrayBuffer()) as unknown as Buffer,\n })\n }\n }\n const { file, files } = applyUploadConfig(rawParts, entry.meta.upload!)\n req.file = file\n req.files = files\n req.body = fields\n }\n } else if (BODY_METHODS.has(request.method)) {\n const contentType = req.headers['content-type'] ?? ''\n if (contentType.includes('application/json')) {\n req.body = await request.json().catch(() => undefined)\n } else if (contentType.includes('application/x-www-form-urlencoded')) {\n const text = await request.text().catch(() => '')\n req.body = Object.fromEntries(new URLSearchParams(text))\n } else if (contentType.startsWith('text/')) {\n req.body = await request.text().catch(() => undefined)\n }\n }\n\n const store = createRequestStore(req.headers['x-request-id'])\n req.requestId = store.requestId\n\n const driver = new WebResponseDriver(request.signal)\n driver.setHeader('x-request-id', store.requestId)\n\n const pipeline = requestStore.run(store, async () => {\n const ctx = new RequestContext(\n req as never,\n driver as never,\n NOOP_NEXT,\n driver as RuntimeResponse,\n )\n\n if (validator) {\n await new Promise<void>((resolve, reject) => {\n validator(req as never, undefined as never, (err?: unknown) =>\n err ? reject(err) : resolve(),\n )\n })\n }\n\n for (const mw of entry.middlewares) {\n let advanced = false\n await new Promise<void>((resolve, reject) => {\n const next = (err?: unknown): void => {\n advanced = true\n if (err) reject(err)\n else resolve()\n }\n Promise.resolve(mw(ctx, next)).catch(reject)\n })\n if (!advanced || driver.settled) return\n }\n\n if (entry.contributorRunner) await entry.contributorRunner(ctx)\n if (driver.settled) return\n const result = await entry.handler(ctx)\n // Return-value handlers (reply.ts): auto-send when nothing was written.\n // `undefined` falls through to the canonical 404 below.\n if (!driver.settled) applyHandlerResult(ctx, result)\n })\n\n try {\n // Streaming responses resolve `ready` mid-pipeline (SSE); buffered ones\n // resolve at the terminal ctx call. Race so both shapes return promptly.\n try {\n await Promise.race([pipeline, driver.ready])\n } catch (err) {\n if (!driver.settled) {\n if (hooks?.onError) await hooks.onError(err, req, driver)\n if (!driver.settled) defaultErrorResponse(err, driver)\n }\n }\n if (!driver.settled) {\n // Pipeline finished without responding — canonical 404 shape.\n driver.status(404).json({ error: 'Not Found' })\n }\n return await driver.ready\n } finally {\n // @PreDestroy teardown. For buffered responses this runs post-response;\n // for streams it runs when the pipeline (not the stream) completes —\n // stream lifetime is governed by the client abort signal.\n void Promise.resolve(pipeline)\n .catch(() => {})\n .then(() => disposeRequestStore(store))\n }\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-Bo3jSfmL.mjs";
2
+ import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-Bzdzl5wP.mjs";
3
3
  export { Application, ApplicationOptions, MiddlewareEntry };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.4.0
2
+ * @forinda/kickjs v6.5.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,4 +8,4 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{t as Application}from"../application-TiHVcGRL.mjs";export{Application};
11
+ import{t as Application}from"../application-C-r79XqD.mjs";export{Application};