@forinda/kickjs 6.7.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapter-diFqgxGR.d.mts → adapter-NstPNaFV.d.mts} +36 -3
- package/dist/{adapter-diFqgxGR.d.mts.map → adapter-NstPNaFV.d.mts.map} +1 -1
- package/dist/{application-VxxP88FH.mjs → application-BDtFj66k.mjs} +3 -3
- package/dist/application-BDtFj66k.mjs.map +1 -0
- package/dist/{application-D_mReBi_.d.mts → application-CnJh2Bry.d.mts} +55 -15
- package/dist/application-CnJh2Bry.d.mts.map +1 -0
- package/dist/{bootstrap-F6IShJ1f.mjs → bootstrap-CJbP4DXN.mjs} +3 -3
- package/dist/{bootstrap-F6IShJ1f.mjs.map → bootstrap-CJbP4DXN.mjs.map} +1 -1
- package/dist/{bootstrap-CytCvAnr.d.mts → bootstrap-Iq_WR08c.d.mts} +2 -2
- package/dist/bootstrap-Iq_WR08c.d.mts.map +1 -0
- package/dist/client-ip-Di2MrjCQ.mjs +12 -0
- package/dist/client-ip-Di2MrjCQ.mjs.map +1 -0
- package/dist/config/index.mjs +2 -2
- package/dist/{config-D035b21X.mjs → config-BQgwhAlJ.mjs} +3 -3
- package/dist/{config-D035b21X.mjs.map → config-BQgwhAlJ.mjs.map} +1 -1
- package/dist/{container-CFVAdwXa.mjs → container-BITnQkhU.mjs} +3 -3
- package/dist/{container-CFVAdwXa.mjs.map → container-BITnQkhU.mjs.map} +1 -1
- package/dist/context-DYOcPcg8.mjs +12 -0
- package/dist/context-DYOcPcg8.mjs.map +1 -0
- package/dist/{context-errors-Ce44238b.mjs → context-errors-Bf2NqTS3.mjs} +2 -2
- package/dist/{context-errors-Ce44238b.mjs.map → context-errors-Bf2NqTS3.mjs.map} +1 -1
- package/dist/core/adapter.d.mts +1 -1
- package/dist/core/adapter.mjs +1 -1
- package/dist/core/app-module.mjs +1 -1
- package/dist/core/cache.mjs +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/errors.mjs +1 -1
- package/dist/core/interfaces.mjs +1 -1
- package/dist/core/logger.mjs +1 -1
- package/dist/core/path.mjs +1 -1
- package/dist/core/plugin.d.mts +1 -1
- package/dist/core/plugin.mjs +1 -1
- package/dist/core/reactivity.mjs +1 -1
- package/dist/{core-DW5uoNhO.mjs → core-MI5Y1nGg.mjs} +3 -3
- package/dist/{core-DW5uoNhO.mjs.map → core-MI5Y1nGg.mjs.map} +1 -1
- package/dist/csrf-BAnc9Ww7.mjs +12 -0
- package/dist/csrf-BAnc9Ww7.mjs.map +1 -0
- package/dist/csrf-vR23CJas.d.mts.map +1 -1
- package/dist/define-adapter-Cz-bj4Xk.mjs +12 -0
- package/dist/define-adapter-Cz-bj4Xk.mjs.map +1 -0
- package/dist/{disposables-QYOU5_Eg.mjs → disposables-0gJffTe_.mjs} +2 -2
- package/dist/{disposables-QYOU5_Eg.mjs.map → disposables-0gJffTe_.mjs.map} +1 -1
- package/dist/{env-DPEhlEC4.mjs → env-IfTg0Rv1.mjs} +3 -3
- package/dist/{env-DPEhlEC4.mjs.map → env-IfTg0Rv1.mjs.map} +1 -1
- package/dist/{error-handler-Cr6Yt9as.d.mts → error-handler-p_T3hhiD.d.mts} +19 -4
- package/dist/error-handler-p_T3hhiD.d.mts.map +1 -0
- package/dist/{handler-DcxBY95X.mjs → handler-mLCYXfTQ.mjs} +3 -3
- package/dist/{handler-DcxBY95X.mjs.map → handler-mLCYXfTQ.mjs.map} +1 -1
- package/dist/http/application.d.mts +2 -2
- package/dist/http/application.mjs +2 -2
- package/dist/http/bootstrap.d.mts +1 -1
- package/dist/http/bootstrap.mjs +2 -2
- package/dist/http/context.d.mts +1 -1
- package/dist/http/context.mjs +2 -2
- package/dist/http/middleware/cors.mjs +1 -1
- package/dist/http/middleware/csrf.mjs +2 -2
- package/dist/http/middleware/error-handler.d.mts +1 -1
- package/dist/http/middleware/error-handler.mjs +2 -2
- package/dist/http/middleware/error-handler.mjs.map +1 -1
- package/dist/http/middleware/helmet.mjs +1 -1
- package/dist/http/middleware/rate-limit.mjs +2 -2
- package/dist/http/middleware/rate-limit.mjs.map +1 -1
- package/dist/http/middleware/request-id.mjs +1 -1
- package/dist/http/middleware/request-logger.d.mts +1 -1
- package/dist/http/middleware/request-logger.mjs +2 -2
- package/dist/http/middleware/request-logger.mjs.map +1 -1
- package/dist/http/middleware/session.mjs +2 -2
- package/dist/http/middleware/spa.d.mts +2 -2
- package/dist/http/middleware/spa.mjs +2 -2
- package/dist/http/middleware/spa.mjs.map +1 -1
- package/dist/http/middleware/upload.d.mts +1 -1
- package/dist/http/middleware/upload.mjs +2 -2
- package/dist/http/middleware/validate.mjs +1 -1
- package/dist/http/middleware/views.d.mts +1 -1
- package/dist/http/middleware/views.mjs +3 -2
- package/dist/http/middleware/views.mjs.map +1 -0
- package/dist/http/query/index.mjs +2 -2
- package/dist/http/router-builder.d.mts +1 -1
- package/dist/http/router-builder.mjs +2 -2
- package/dist/http/runtimes/fastify.d.mts +1 -1
- package/dist/http/runtimes/fastify.mjs +2 -2
- package/dist/http/runtimes/h3-web.d.mts +1 -1
- package/dist/http/runtimes/h3-web.mjs +2 -2
- package/dist/http/runtimes/h3.d.mts +1 -1
- package/dist/http/runtimes/h3.mjs +2 -2
- package/dist/index.d.mts +9 -10
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/kv-stores-DNeEYG-J.mjs +12 -0
- package/dist/kv-stores-DNeEYG-J.mjs.map +1 -0
- package/dist/{kv-stores-D2kSUAEP.d.mts → kv-stores-pWdw8RKf.d.mts} +3 -3
- package/dist/kv-stores-pWdw8RKf.d.mts.map +1 -0
- package/dist/{metadata-DjW8JOQZ.mjs → metadata-CyyYO_qa.mjs} +2 -2
- package/dist/{metadata-DjW8JOQZ.mjs.map → metadata-CyyYO_qa.mjs.map} +1 -1
- package/dist/{module-registry-cbndPnlY.mjs → module-registry-BfOa58-E.mjs} +2 -2
- package/dist/{module-registry-cbndPnlY.mjs.map → module-registry-BfOa58-E.mjs.map} +1 -1
- package/dist/{query-B_YnIZ2-.mjs → query-BYUCKdu5.mjs} +2 -2
- package/dist/{query-B_YnIZ2-.mjs.map → query-BYUCKdu5.mjs.map} +1 -1
- package/dist/rate-limit-Dme747JJ.d.mts.map +1 -1
- package/dist/{reply-Y50qUtDy.mjs → reply-Bo950wuK.mjs} +3 -3
- package/dist/{reply-Y50qUtDy.mjs.map → reply-Bo950wuK.mjs.map} +1 -1
- package/dist/{request-logger-BU4ulKLp.d.mts → request-logger-D_pf4xzE.d.mts} +21 -3
- package/dist/request-logger-D_pf4xzE.d.mts.map +1 -0
- package/dist/{router-builder-C6E11Grf.d.mts → router-builder-BYDUIiZW.d.mts} +2 -2
- package/dist/{router-builder-C6E11Grf.d.mts.map → router-builder-BYDUIiZW.d.mts.map} +1 -1
- package/dist/{router-builder-DtonR20n.mjs → router-builder-Bv175XoR.mjs} +3 -3
- package/dist/{router-builder-DtonR20n.mjs.map → router-builder-Bv175XoR.mjs.map} +1 -1
- package/dist/{views-BBY9_PUV.d.mts → spa-CDQ74RGA.d.mts} +136 -11
- package/dist/spa-CDQ74RGA.d.mts.map +1 -0
- package/dist/{upload-Wck12jE5.d.mts → upload-CUtcjc-n.d.mts} +2 -2
- package/dist/{upload-Wck12jE5.d.mts.map → upload-CUtcjc-n.d.mts.map} +1 -1
- package/dist/{upload-config-Dxr2BxTi.mjs → upload-config-C9V9tIR3.mjs} +2 -2
- package/dist/{upload-config-Dxr2BxTi.mjs.map → upload-config-C9V9tIR3.mjs.map} +1 -1
- package/dist/web.d.mts +2 -2
- package/dist/web.mjs +2 -2
- package/package.json +5 -5
- package/dist/application-D_mReBi_.d.mts.map +0 -1
- package/dist/application-VxxP88FH.mjs.map +0 -1
- package/dist/bootstrap-CytCvAnr.d.mts.map +0 -1
- package/dist/context-CAN3t2jg.mjs +0 -12
- package/dist/context-CAN3t2jg.mjs.map +0 -1
- package/dist/csrf-MbsM7-58.mjs +0 -12
- package/dist/csrf-MbsM7-58.mjs.map +0 -1
- package/dist/error-handler-Cr6Yt9as.d.mts.map +0 -1
- package/dist/kv-stores-D2kSUAEP.d.mts.map +0 -1
- package/dist/kv-stores-VfJbg9Gs.mjs +0 -12
- package/dist/kv-stores-VfJbg9Gs.mjs.map +0 -1
- package/dist/request-logger-BU4ulKLp.d.mts.map +0 -1
- package/dist/spa-CCi-hC2M.d.mts +0 -97
- package/dist/spa-CCi-hC2M.d.mts.map +0 -1
- package/dist/views-3TYTcdyF.mjs +0 -12
- package/dist/views-3TYTcdyF.mjs.map +0 -1
- package/dist/views-BBY9_PUV.d.mts.map +0 -1
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["mergeConfig","composeName"],"sources":["../src/core/define-module.ts","../src/core/define-modules.ts","../src/core/circuit-breaker.ts","../src/core/define-plugin.ts","../src/core/augmentation.ts","../src/http/define-http-context-decorator.ts","../src/http/middleware/trace-context.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 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","/**\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","/**\n * HTTP-flavoured wrapper around {@link defineContextDecorator}.\n *\n * The core factory leaves `Ctx` defaulted to {@link ExecutionContext} so the\n * Context Contributor pipeline stays transport-agnostic — future WebSocket,\n * queue, and cron contexts share the same primitive. The cost is that the\n * vast majority of contributors (which are HTTP) have to spell the fourth\n * generic by hand to get `ctx.req` / `ctx.headers` typing:\n *\n * ```ts\n * // Verbose: spell every generic, including the params shape `P`.\n * defineContextDecorator<'tenant', Record<string, never>, Record<string, never>, RequestContext>({\n * key: 'tenant',\n * resolve: (ctx) => ({ id: ctx.req.headers['x-tenant-id'] as string }),\n * })\n * ```\n *\n * `defineHttpContextDecorator` removes that ceremony — the spec is typed\n * against `RequestContext` from the start, so resolvers can read `ctx.req`,\n * `ctx.headers`, `ctx.params`, etc. without a cast or generic dance:\n *\n * ```ts\n * import { defineHttpContextDecorator } from '@forinda/kickjs'\n *\n * const LoadTenant = defineHttpContextDecorator({\n * key: 'tenant',\n * resolve: (ctx) => ({ id: ctx.req.headers['x-tenant-id'] as string }),\n * })\n *\n * // Parameterised — the positional form forces you to also spell `K`\n * // and `D`, which loses automatic `deps` inference. Prefer the curried\n * // `.withParams<P>()` entry point below.\n * const LoadTenantParameterised = defineHttpContextDecorator<\n * 'tenant',\n * Record<string, never>,\n * { source: 'header' | 'subdomain' }\n * >({\n * key: 'tenant',\n * paramDefaults: { source: 'header' },\n * resolve: (ctx, _deps, params) => {\n * // params.source narrows inside `if` branches.\n * },\n * })\n *\n * // Parameterised + deps — only `P` is spelled; `K` and `D` infer from\n * // the spec, so `deps.repo` is fully typed in the resolver.\n * const LoadTenantWithRepo = defineHttpContextDecorator.withParams<{\n * source: 'header' | 'subdomain'\n * }>()({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO },\n * paramDefaults: { source: 'header' },\n * resolve: (ctx, { repo }, params) =>\n * repo.findFor(ctx.req.headers['x-tenant-id'] as string, params.source),\n * })\n * ```\n *\n * Use {@link defineContextDecorator} directly when authoring contributors\n * for non-HTTP transports or when you want to share one contributor across\n * HTTP + WS + queue (the resolver then has to stick to the\n * `ExecutionContext` surface — `ctx.get` / `ctx.set` / `ctx.requestId`).\n */\n\nimport {\n defineContextDecorator,\n type ContextDecorator,\n type ContextDecoratorSpec,\n type ContextMetaKey,\n type DepValue,\n} from '../core/context-decorator'\nimport type { RequestContext } from './context'\n\n/**\n * Curried form returned by {@link DefineHttpContextDecoratorFn.withParams}.\n * Spell only the per-call params shape `P`; `K` and `D` are inferred\n * from the spec, and `Ctx` is locked to `RequestContext`.\n */\nexport interface DefineHttpContextDecoratorWithParams<P extends Record<string, unknown>> {\n <K extends ContextMetaKey, D extends Record<string, DepValue> = Record<string, never>>(\n spec: ContextDecoratorSpec<K, D, P, RequestContext>,\n ): ContextDecorator<K, D, P, RequestContext>\n}\n\n/**\n * Public shape of {@link defineHttpContextDecorator} — the positional\n * call signature plus the curried `.withParams<P>()` helper that fixes\n * partial-inference for the parameterised case.\n */\nexport interface DefineHttpContextDecoratorFn {\n <\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, RequestContext>,\n ): ContextDecorator<K, D, P, RequestContext>\n\n /**\n * Curried entry point. Spell only `P`; `K` and `D` infer from the\n * spec. `Ctx` is locked to `RequestContext`.\n *\n * ```ts\n * const LoadTenant = defineHttpContextDecorator.withParams<{\n * source: 'header' | 'subdomain'\n * }>()({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO }, // D inferred\n * paramDefaults: { source: 'header' },\n * resolve: (ctx, { repo }, params) => repo.findFor(ctx, params),\n * })\n * ```\n */\n withParams<P extends Record<string, unknown>>(): DefineHttpContextDecoratorWithParams<P>\n}\n\nfunction defineHttpContextDecoratorImpl<\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n>(spec: ContextDecoratorSpec<K, D, P, RequestContext>): ContextDecorator<K, D, P, RequestContext> {\n return defineContextDecorator<K, D, P, RequestContext>(spec)\n}\n\nexport const defineHttpContextDecorator: DefineHttpContextDecoratorFn = Object.freeze(\n Object.assign(defineHttpContextDecoratorImpl, {\n withParams: <P extends Record<string, unknown>>(): DefineHttpContextDecoratorWithParams<P> => {\n return <K extends ContextMetaKey, D extends Record<string, DepValue> = Record<string, never>>(\n spec: ContextDecoratorSpec<K, D, P, RequestContext>,\n ): ContextDecorator<K, D, P, RequestContext> => defineHttpContextDecoratorImpl<K, D, P>(spec)\n },\n }),\n) as DefineHttpContextDecoratorFn\n","import type { Request, Response, NextFunction } from 'express'\nimport { requestStore } from '../request-store'\n\nimport { randomHex } from '../../core/web-crypto'\n\n/**\n * W3C Trace Context header name.\n * @see https://www.w3.org/TR/trace-context/#traceparent-header\n */\nexport const TRACEPARENT_HEADER = 'traceparent'\n\n/** Regex for a valid W3C traceparent: {version}-{trace-id}-{parent-id}-{flags} */\nconst TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/\n\nexport interface TraceContextOptions {\n /**\n * When true, respond with a `traceresponse` header containing the trace ID.\n * Useful for debugging. Default: false.\n */\n propagateResponse?: boolean\n}\n\n/**\n * Parsed W3C trace context fields.\n */\nexport interface TraceContext {\n /** W3C traceparent version (usually '00') */\n version: string\n /** 32-hex-char distributed trace ID */\n traceId: string\n /** 16-hex-char parent span ID */\n parentSpanId: string\n /** 2-hex-char trace flags */\n flags: string\n}\n\n/**\n * Generate a random 32-hex-character trace ID.\n */\nfunction generateTraceId(): string {\n return randomHex(16)\n}\n\n/**\n * Generate a random 16-hex-character span ID.\n */\nfunction generateSpanId(): string {\n return randomHex(8)\n}\n\n/**\n * Parse a W3C traceparent header value.\n * Returns null if the format is invalid.\n */\nexport function parseTraceparent(header: string): TraceContext | null {\n const match = TRACEPARENT_RE.exec(header.trim().toLowerCase())\n if (!match) return null\n\n const version = match[1]\n const traceId = match[2]\n const parentSpanId = match[3]\n const flags = match[4]\n\n // W3C spec: version ff is invalid, all-zero trace-id and parent-id are invalid\n if (\n version === 'ff' ||\n traceId === '00000000000000000000000000000000' ||\n parentSpanId === '0000000000000000'\n ) {\n return null\n }\n\n return { version, traceId, parentSpanId, flags }\n}\n\n/**\n * Middleware that extracts W3C `traceparent` header and stores trace context\n * in the request's AsyncLocalStorage store.\n *\n * If no valid `traceparent` header is present, a new traceId is generated\n * so that every request always has a trace ID available for correlation.\n *\n * Must be mounted **after** `requestScopeMiddleware()` (which creates the store).\n *\n * @example\n * ```ts\n * bootstrap({\n * middleware: [\n * requestScopeMiddleware(),\n * traceContext(), // extracts or generates traceId\n * requestLogger(),\n * express.json(),\n * ],\n * })\n * ```\n */\nexport function traceContext(options: TraceContextOptions = {}) {\n return (req: Request, res: Response, next: NextFunction) => {\n const store = requestStore.getStore()\n\n const traceparentHeader = req.headers[TRACEPARENT_HEADER]\n const raw = Array.isArray(traceparentHeader) ? traceparentHeader[0] : traceparentHeader\n const parsed = raw ? parseTraceparent(raw) : null\n\n const traceId = parsed?.traceId ?? generateTraceId()\n const spanId = generateSpanId()\n\n // Attach to the request store so logger + downstream code can access it\n if (store) {\n store.values.set('traceId', traceId)\n store.values.set('spanId', spanId)\n if (parsed) {\n store.values.set('parentSpanId', parsed.parentSpanId)\n store.values.set('traceFlags', parsed.flags)\n store.values.set('traceVersion', parsed.version)\n }\n }\n\n // Also expose on req for convenience\n ;(req as any).traceId = traceId\n ;(req as any).spanId = spanId\n\n if (options.propagateResponse) {\n res.setHeader('traceresponse', traceId)\n }\n\n next()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["mergeConfig","composeName"],"sources":["../src/core/define-module.ts","../src/core/define-modules.ts","../src/core/circuit-breaker.ts","../src/core/define-plugin.ts","../src/core/augmentation.ts","../src/http/define-http-context-decorator.ts","../src/http/middleware/trace-context.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 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","/**\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","/**\n * HTTP-flavoured wrapper around {@link defineContextDecorator}.\n *\n * The core factory leaves `Ctx` defaulted to {@link ExecutionContext} so the\n * Context Contributor pipeline stays transport-agnostic — future WebSocket,\n * queue, and cron contexts share the same primitive. The cost is that the\n * vast majority of contributors (which are HTTP) have to spell the fourth\n * generic by hand to get `ctx.req` / `ctx.headers` typing:\n *\n * ```ts\n * // Verbose: spell every generic, including the params shape `P`.\n * defineContextDecorator<'tenant', Record<string, never>, Record<string, never>, RequestContext>({\n * key: 'tenant',\n * resolve: (ctx) => ({ id: ctx.req.headers['x-tenant-id'] as string }),\n * })\n * ```\n *\n * `defineHttpContextDecorator` removes that ceremony — the spec is typed\n * against `RequestContext` from the start, so resolvers can read `ctx.req`,\n * `ctx.headers`, `ctx.params`, etc. without a cast or generic dance:\n *\n * ```ts\n * import { defineHttpContextDecorator } from '@forinda/kickjs'\n *\n * const LoadTenant = defineHttpContextDecorator({\n * key: 'tenant',\n * resolve: (ctx) => ({ id: ctx.req.headers['x-tenant-id'] as string }),\n * })\n *\n * // Parameterised — the positional form forces you to also spell `K`\n * // and `D`, which loses automatic `deps` inference. Prefer the curried\n * // `.withParams<P>()` entry point below.\n * const LoadTenantParameterised = defineHttpContextDecorator<\n * 'tenant',\n * Record<string, never>,\n * { source: 'header' | 'subdomain' }\n * >({\n * key: 'tenant',\n * paramDefaults: { source: 'header' },\n * resolve: (ctx, _deps, params) => {\n * // params.source narrows inside `if` branches.\n * },\n * })\n *\n * // Parameterised + deps — only `P` is spelled; `K` and `D` infer from\n * // the spec, so `deps.repo` is fully typed in the resolver.\n * const LoadTenantWithRepo = defineHttpContextDecorator.withParams<{\n * source: 'header' | 'subdomain'\n * }>()({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO },\n * paramDefaults: { source: 'header' },\n * resolve: (ctx, { repo }, params) =>\n * repo.findFor(ctx.req.headers['x-tenant-id'] as string, params.source),\n * })\n * ```\n *\n * Use {@link defineContextDecorator} directly when authoring contributors\n * for non-HTTP transports or when you want to share one contributor across\n * HTTP + WS + queue (the resolver then has to stick to the\n * `ExecutionContext` surface — `ctx.get` / `ctx.set` / `ctx.requestId`).\n */\n\nimport {\n defineContextDecorator,\n type ContextDecorator,\n type ContextDecoratorSpec,\n type ContextMetaKey,\n type DepValue,\n} from '../core/context-decorator'\nimport type { RequestContext } from './context'\n\n/**\n * Curried form returned by {@link DefineHttpContextDecoratorFn.withParams}.\n * Spell only the per-call params shape `P`; `K` and `D` are inferred\n * from the spec, and `Ctx` is locked to `RequestContext`.\n */\nexport interface DefineHttpContextDecoratorWithParams<P extends Record<string, unknown>> {\n <K extends ContextMetaKey, D extends Record<string, DepValue> = Record<string, never>>(\n spec: ContextDecoratorSpec<K, D, P, RequestContext>,\n ): ContextDecorator<K, D, P, RequestContext>\n}\n\n/**\n * Public shape of {@link defineHttpContextDecorator} — the positional\n * call signature plus the curried `.withParams<P>()` helper that fixes\n * partial-inference for the parameterised case.\n */\nexport interface DefineHttpContextDecoratorFn {\n <\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, RequestContext>,\n ): ContextDecorator<K, D, P, RequestContext>\n\n /**\n * Curried entry point. Spell only `P`; `K` and `D` infer from the\n * spec. `Ctx` is locked to `RequestContext`.\n *\n * ```ts\n * const LoadTenant = defineHttpContextDecorator.withParams<{\n * source: 'header' | 'subdomain'\n * }>()({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO }, // D inferred\n * paramDefaults: { source: 'header' },\n * resolve: (ctx, { repo }, params) => repo.findFor(ctx, params),\n * })\n * ```\n */\n withParams<P extends Record<string, unknown>>(): DefineHttpContextDecoratorWithParams<P>\n}\n\nfunction defineHttpContextDecoratorImpl<\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n>(spec: ContextDecoratorSpec<K, D, P, RequestContext>): ContextDecorator<K, D, P, RequestContext> {\n return defineContextDecorator<K, D, P, RequestContext>(spec)\n}\n\nexport const defineHttpContextDecorator: DefineHttpContextDecoratorFn = Object.freeze(\n Object.assign(defineHttpContextDecoratorImpl, {\n withParams: <P extends Record<string, unknown>>(): DefineHttpContextDecoratorWithParams<P> => {\n return <K extends ContextMetaKey, D extends Record<string, DepValue> = Record<string, never>>(\n spec: ContextDecoratorSpec<K, D, P, RequestContext>,\n ): ContextDecorator<K, D, P, RequestContext> => defineHttpContextDecoratorImpl<K, D, P>(spec)\n },\n }),\n) as DefineHttpContextDecoratorFn\n","import type { Request, Response, NextFunction } from 'express'\nimport { requestStore } from '../request-store'\n\nimport { randomHex } from '../../core/web-crypto'\n\n/**\n * W3C Trace Context header name.\n * @see https://www.w3.org/TR/trace-context/#traceparent-header\n */\nexport const TRACEPARENT_HEADER = 'traceparent'\n\n/** Regex for a valid W3C traceparent: {version}-{trace-id}-{parent-id}-{flags} */\nconst TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/\n\nexport interface TraceContextOptions {\n /**\n * When true, respond with a `traceresponse` header containing the trace ID.\n * Useful for debugging. Default: false.\n */\n propagateResponse?: boolean\n}\n\n/**\n * Parsed W3C trace context fields.\n */\nexport interface TraceContext {\n /** W3C traceparent version (usually '00') */\n version: string\n /** 32-hex-char distributed trace ID */\n traceId: string\n /** 16-hex-char parent span ID */\n parentSpanId: string\n /** 2-hex-char trace flags */\n flags: string\n}\n\n/**\n * Generate a random 32-hex-character trace ID.\n */\nfunction generateTraceId(): string {\n return randomHex(16)\n}\n\n/**\n * Generate a random 16-hex-character span ID.\n */\nfunction generateSpanId(): string {\n return randomHex(8)\n}\n\n/**\n * Parse a W3C traceparent header value.\n * Returns null if the format is invalid.\n */\nexport function parseTraceparent(header: string): TraceContext | null {\n const match = TRACEPARENT_RE.exec(header.trim().toLowerCase())\n if (!match) return null\n\n const version = match[1]\n const traceId = match[2]\n const parentSpanId = match[3]\n const flags = match[4]\n\n // W3C spec: version ff is invalid, all-zero trace-id and parent-id are invalid\n if (\n version === 'ff' ||\n traceId === '00000000000000000000000000000000' ||\n parentSpanId === '0000000000000000'\n ) {\n return null\n }\n\n return { version, traceId, parentSpanId, flags }\n}\n\n/**\n * Middleware that extracts W3C `traceparent` header and stores trace context\n * in the request's AsyncLocalStorage store.\n *\n * If no valid `traceparent` header is present, a new traceId is generated\n * so that every request always has a trace ID available for correlation.\n *\n * Must be mounted **after** `requestScopeMiddleware()` (which creates the store).\n *\n * @example\n * ```ts\n * bootstrap({\n * middleware: [\n * requestScopeMiddleware(),\n * traceContext(), // extracts or generates traceId\n * requestLogger(),\n * express.json(),\n * ],\n * })\n * ```\n */\nexport function traceContext(options: TraceContextOptions = {}) {\n return (req: Request, res: Response, next: NextFunction) => {\n const store = requestStore.getStore()\n\n const traceparentHeader = req.headers[TRACEPARENT_HEADER]\n const raw = Array.isArray(traceparentHeader) ? traceparentHeader[0] : traceparentHeader\n const parsed = raw ? parseTraceparent(raw) : null\n\n const traceId = parsed?.traceId ?? generateTraceId()\n const spanId = generateSpanId()\n\n // Attach to the request store so logger + downstream code can access it\n if (store) {\n store.values.set('traceId', traceId)\n store.values.set('spanId', spanId)\n if (parsed) {\n store.values.set('parentSpanId', parsed.parentSpanId)\n store.values.set('traceFlags', parsed.flags)\n store.values.set('traceVersion', parsed.version)\n }\n }\n\n // Also expose on req for convenience\n ;(req as any).traceId = traceId\n ;(req as any).spanId = spanId\n\n if (options.propagateResponse) {\n res.setHeader('traceresponse', traceId)\n }\n\n next()\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs v7.1.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Felix Orinda
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
var LazyMemoryStore=class{windowMs;hits=new Map;constructor(windowMs){this.windowMs=windowMs}async increment(key){let now=Date.now(),entry=this.hits.get(key);if(entry&&entry.reset>now)return entry.hits++,{totalHits:entry.hits,resetTime:new Date(entry.reset)};if(this.hits.size>1e4)for(let[k,e]of this.hits)e.reset<=now&&this.hits.delete(k);let fresh={hits:1,reset:now+this.windowMs};return this.hits.set(key,fresh),{totalHits:1,resetTime:new Date(fresh.reset)}}async decrement(key){let entry=this.hits.get(key);entry&&entry.hits>0&&entry.hits--}async reset(key){this.hits.delete(key)}};function defaultKey(ctx){return ctx.ip??`global`}function rateLimitGuard(options={}){let max=options.max??100,windowMs=options.windowMs??6e4,message=options.message??`Too Many Requests`,statusCode=options.statusCode??429,keyGenerator=options.keyGenerator??defaultKey,sendHeaders=options.headers??!0,store=options.store??new LazyMemoryStore(windowMs);return async(ctx,next)=>{if(options.skip?.(ctx))return next();let{totalHits,resetTime}=await store.increment(keyGenerator(ctx)),remaining=Math.max(0,max-totalHits);if(sendHeaders&&(ctx.setHeader(`X-RateLimit-Limit`,String(max)),ctx.setHeader(`X-RateLimit-Remaining`,String(remaining)),ctx.setHeader(`X-RateLimit-Reset`,String(Math.ceil(resetTime.getTime()/1e3)))),totalHits>max){let retryAfter=Math.max(1,Math.ceil((resetTime.getTime()-Date.now())/1e3));sendHeaders&&ctx.setHeader(`Retry-After`,String(retryAfter)),ctx.json({message},statusCode);return}next()}}function kvTtlSeconds(ms){return Math.max(60,Math.ceil(ms/1e3))}var KvRateLimitStore=class{kv;prefix;windowMs;constructor(kv,options){this.kv=kv,this.windowMs=options.windowMs,this.prefix=options.prefix??`rl:`}async increment(key){let now=Date.now(),entry=await this.read(key);if(entry&&entry.reset>now)return entry.hits++,await this.write(key,entry),{totalHits:entry.hits,resetTime:new Date(entry.reset)};let fresh={hits:1,reset:now+this.windowMs};return await this.write(key,fresh),{totalHits:1,resetTime:new Date(fresh.reset)}}async decrement(key){let entry=await this.read(key);entry&&entry.hits>0&&(entry.hits--,await this.write(key,entry))}async reset(key){await this.kv.delete(this.prefix+key)}async read(key){let raw=await this.kv.get(this.prefix+key);if(raw===null)return null;try{return JSON.parse(raw)}catch{return null}}async write(key,entry){await this.kv.put(this.prefix+key,JSON.stringify(entry),{expirationTtl:kvTtlSeconds(Math.max(entry.reset-Date.now(),1e3))})}},KvSessionStore=class{kv;prefix;constructor(kv,options={}){this.kv=kv,this.prefix=options.prefix??`sess:`}async get(sid){let raw=await this.kv.get(this.prefix+sid);if(raw===null)return null;try{return JSON.parse(raw)}catch{return null}}async set(sid,data,maxAge){await this.kv.put(this.prefix+sid,JSON.stringify(data),{expirationTtl:kvTtlSeconds(maxAge)})}async destroy(sid){await this.kv.delete(this.prefix+sid)}async touch(sid,maxAge){let data=await this.get(sid);data!==null&&await this.set(sid,data,maxAge)}};export{KvSessionStore as n,rateLimitGuard as r,KvRateLimitStore as t};
|
|
12
|
+
//# sourceMappingURL=kv-stores-DNeEYG-J.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv-stores-DNeEYG-J.mjs","names":[],"sources":["../src/http/middleware/rate-limit-guard.ts","../src/http/middleware/kv-stores.ts"],"sourcesContent":["// Ctx-style rate limiter — runs on BOTH pipelines: the node runtimes (via\n// `@Middleware()` / class middleware) and the `@forinda/kickjs/web` fetch\n// entry (via `@Middleware()` or `createWebApp({ middleware })`), unlike the\n// connect-style `rateLimit()` which is node-only. Zero runtime imports —\n// part of the edge purity graph.\nimport type { MiddlewareHandler } from '../../core/decorators'\nimport type { RequestContext } from '../context'\nimport type { RateLimitStore } from './rate-limit'\n\nexport interface RateLimitGuardOptions {\n /** Maximum number of requests per window (default: 100). */\n max?: number\n /** Time window in milliseconds (default: 60_000). */\n windowMs?: number\n /** Response message when the limit is exceeded (default: 'Too Many Requests'). */\n message?: string\n /** HTTP status code when the limit is exceeded (default: 429). */\n statusCode?: number\n /**\n * Rate-limit key per request. Default: `cf-connecting-ip`, then the first\n * hop of `x-forwarded-for`, then `x-real-ip`, then `'global'`. Return a\n * user/tenant id here for authenticated quotas.\n */\n keyGenerator?: (ctx: RequestContext) => string\n /** Send `X-RateLimit-*` / `Retry-After` headers (default: true). */\n headers?: boolean\n /**\n * Counter backend. Defaults to a per-isolate in-memory map — fine for a\n * single node process, useless on edge where isolates recycle: pass a\n * `KvRateLimitStore` (or Redis-backed store) there.\n */\n store?: RateLimitStore\n /** Skip limiting for a request (health checks, allowlists). */\n skip?: (ctx: RequestContext) => boolean\n}\n\n/** Timer-free in-memory store: sweeps expired entries when the map grows. */\nclass LazyMemoryStore implements RateLimitStore {\n private readonly hits = new Map<string, { hits: number; reset: number }>()\n\n constructor(private readonly windowMs: number) {}\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n if (entry && entry.reset > now) {\n entry.hits++\n return { totalHits: entry.hits, resetTime: new Date(entry.reset) }\n }\n // ponytail: sweep-on-growth instead of a cleanup interval — no timer to\n // dispose, edge-safe. Bound is 10k live keys before a full sweep.\n if (this.hits.size > 10_000) {\n for (const [k, e] of this.hits) if (e.reset <= now) this.hits.delete(k)\n }\n const fresh = { hits: 1, reset: now + this.windowMs }\n this.hits.set(key, fresh)\n return { totalHits: 1, resetTime: new Date(fresh.reset) }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.hits > 0) entry.hits--\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n}\n\nfunction defaultKey(ctx: RequestContext): string {\n // `ctx.ip` carries the resolution that used to live here: prefer the\n // runtime-computed address (Express derives it from `trust proxy`), fall\n // back to forwarded headers only for runtimes that compute none.\n return ctx.ip ?? 'global'\n}\n\n/**\n * Rate limiting as a `(ctx, next)` middleware.\n *\n * ```ts\n * // Edge (Cloudflare Workers) — app-wide:\n * const app = createWebApp({\n * h3, modules,\n * middleware: [rateLimitGuard({ max: 60, windowMs: 60_000, store: new KvRateLimitStore(env.KV, { windowMs: 60_000 }) })],\n * })\n *\n * // Any runtime — per controller/route:\n * @Middleware(rateLimitGuard({ max: 10 }))\n * @Post('/login')\n * ```\n */\nexport function rateLimitGuard(options: RateLimitGuardOptions = {}): MiddlewareHandler {\n const max = options.max ?? 100\n const windowMs = options.windowMs ?? 60_000\n const message = options.message ?? 'Too Many Requests'\n const statusCode = options.statusCode ?? 429\n const keyGenerator = options.keyGenerator ?? defaultKey\n const sendHeaders = options.headers ?? true\n const store = options.store ?? new LazyMemoryStore(windowMs)\n\n return async (ctx: RequestContext, next: () => void): Promise<void> => {\n if (options.skip?.(ctx)) return next()\n\n const { totalHits, resetTime } = await store.increment(keyGenerator(ctx))\n const remaining = Math.max(0, max - totalHits)\n\n if (sendHeaders) {\n ctx.setHeader('X-RateLimit-Limit', String(max))\n ctx.setHeader('X-RateLimit-Remaining', String(remaining))\n ctx.setHeader('X-RateLimit-Reset', String(Math.ceil(resetTime.getTime() / 1000)))\n }\n\n if (totalHits > max) {\n const retryAfter = Math.max(1, Math.ceil((resetTime.getTime() - Date.now()) / 1000))\n if (sendHeaders) ctx.setHeader('Retry-After', String(retryAfter))\n ctx.json({ message }, statusCode)\n return\n }\n\n next()\n }\n}\n","// Edge-safe store adapters over a minimal KV surface. Zero runtime imports —\n// this file must stay importable from the `@forinda/kickjs/web` purity graph.\nimport type { RateLimitStore } from './rate-limit'\nimport type { SessionData, SessionStore } from './session'\n\n/**\n * The minimal async KV surface both stores need. Structurally compatible\n * with a Cloudflare Workers `KVNamespace` binding — pass it straight in:\n *\n * ```ts\n * export default {\n * fetch(req: Request, env: Env) {\n * const store = new KvRateLimitStore(env.MY_KV, { windowMs: 60_000 })\n * // ...\n * },\n * }\n * ```\n *\n * Anything with the same shape works too (Deno KV wrapper, Upstash Redis\n * REST client adapter, a Map-backed fake in tests).\n */\nexport interface KvLike {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/** Cloudflare KV rejects TTLs under 60 seconds — clamp up, never down. */\nfunction kvTtlSeconds(ms: number): number {\n return Math.max(60, Math.ceil(ms / 1000))\n}\n\nexport interface KvRateLimitStoreOptions {\n /** Window length in ms — must match the `windowMs` given to the limiter. */\n windowMs: number\n /** Key prefix in the KV namespace (default `'rl:'`). */\n prefix?: string\n}\n\n/**\n * {@link RateLimitStore} over a {@link KvLike} namespace, for edge deployments\n * where in-memory counters reset on every isolate.\n *\n * KV is eventually consistent and this is a read-modify-write counter, so\n * limiting is **approximate** under concurrency — bursts racing across\n * isolates can each read the same count. That is the accepted trade-off for\n * abuse throttling; use a Durable Object / Redis store when you need an\n * exact ceiling (billing quotas, strict per-key locks).\n */\nexport class KvRateLimitStore implements RateLimitStore {\n private readonly prefix: string\n private readonly windowMs: number\n\n constructor(\n private readonly kv: KvLike,\n options: KvRateLimitStoreOptions,\n ) {\n this.windowMs = options.windowMs\n this.prefix = options.prefix ?? 'rl:'\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = await this.read(key)\n if (entry && entry.reset > now) {\n entry.hits++\n await this.write(key, entry)\n return { totalHits: entry.hits, resetTime: new Date(entry.reset) }\n }\n const fresh = { hits: 1, reset: now + this.windowMs }\n await this.write(key, fresh)\n return { totalHits: 1, resetTime: new Date(fresh.reset) }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = await this.read(key)\n if (entry && entry.hits > 0) {\n entry.hits--\n await this.write(key, entry)\n }\n }\n\n async reset(key: string): Promise<void> {\n await this.kv.delete(this.prefix + key)\n }\n\n private async read(key: string): Promise<{ hits: number; reset: number } | null> {\n const raw = await this.kv.get(this.prefix + key)\n if (raw === null) return null\n try {\n return JSON.parse(raw) as { hits: number; reset: number }\n } catch {\n return null\n }\n }\n\n private async write(key: string, entry: { hits: number; reset: number }): Promise<void> {\n await this.kv.put(this.prefix + key, JSON.stringify(entry), {\n expirationTtl: kvTtlSeconds(Math.max(entry.reset - Date.now(), 1000)),\n })\n }\n}\n\nexport interface KvSessionStoreOptions {\n /** Key prefix in the KV namespace (default `'sess:'`). */\n prefix?: string\n}\n\n/**\n * {@link SessionStore} over a {@link KvLike} namespace. Plug into the node\n * `session()` middleware today (e.g. backed by Upstash/Cloudflare KV via\n * REST) so sessions survive restarts and horizontal scaling; TTL expiry is\n * delegated to the KV layer via `expirationTtl`.\n *\n * Cost note: `touch()` is a get + put round-trip (KvLike has no TTL-only\n * refresh), so `session({ rolling: true })` doubles KV operations per\n * request — per-operation-billed platforms may prefer `rolling: false`.\n */\nexport class KvSessionStore implements SessionStore {\n private readonly prefix: string\n\n constructor(\n private readonly kv: KvLike,\n options: KvSessionStoreOptions = {},\n ) {\n this.prefix = options.prefix ?? 'sess:'\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const raw = await this.kv.get(this.prefix + sid)\n if (raw === null) return null\n try {\n return JSON.parse(raw) as SessionData\n } catch {\n return null\n }\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n await this.kv.put(this.prefix + sid, JSON.stringify(data), {\n expirationTtl: kvTtlSeconds(maxAge),\n })\n }\n\n async destroy(sid: string): Promise<void> {\n await this.kv.delete(this.prefix + sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const data = await this.get(sid)\n if (data !== null) await this.set(sid, data, maxAge)\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { x as MiddlewareHandler } from "./decorators-Bks7cgBJ.mjs";
|
|
3
|
-
import { T as RequestContext } from "./adapter-
|
|
3
|
+
import { T as RequestContext, b as RuntimeResponse } from "./adapter-NstPNaFV.mjs";
|
|
4
4
|
import { n as RateLimitStore } from "./rate-limit-Dme747JJ.mjs";
|
|
5
5
|
import { i as SessionStore, n as SessionData } from "./session-z9iewOXA.mjs";
|
|
6
6
|
//#region src/http/reply.d.ts
|
|
@@ -46,7 +46,7 @@ declare namespace reply {
|
|
|
46
46
|
* ```
|
|
47
47
|
*/
|
|
48
48
|
type InferHandlerResponse<H> = H extends ((...args: never[]) => infer R) ? [Exclude<Awaited<R>, void | undefined>] extends [never] ? unknown : UnwrapReply<Exclude<Awaited<R>, void | undefined>> : unknown;
|
|
49
|
-
type UnwrapReply<A> = A extends Reply<number, infer B> ? B : A;
|
|
49
|
+
type UnwrapReply<A> = A extends Reply<number, infer B> ? B : A extends RuntimeResponse ? unknown : A;
|
|
50
50
|
declare function isReply(value: unknown): value is Reply;
|
|
51
51
|
/**
|
|
52
52
|
* Send a handler's RETURN value when the pipeline finished without anything
|
|
@@ -184,4 +184,4 @@ declare class KvSessionStore implements SessionStore {
|
|
|
184
184
|
}
|
|
185
185
|
//#endregion
|
|
186
186
|
export { KvSessionStoreOptions as a, InferHandlerResponse as c, isReply as d, reply as f, KvSessionStore as i, Reply as l, KvRateLimitStore as n, RateLimitGuardOptions as o, KvRateLimitStoreOptions as r, rateLimitGuard as s, KvLike as t, applyHandlerResult as u };
|
|
187
|
-
//# sourceMappingURL=kv-stores-
|
|
187
|
+
//# sourceMappingURL=kv-stores-pWdw8RKf.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv-stores-pWdw8RKf.d.mts","names":[],"sources":["../src/http/reply.ts","../src/http/middleware/rate-limit-guard.ts","../src/http/middleware/kv-stores.ts"],"mappings":";;;;;;cAgBM;;;;;;UAOW,MAAM,2BAA2B;YACtC;WACD,QAAQ;WACR,MAAM;;;;;;;;;;;;iBAaD,MAAM,kBAAkB,GAAG,QAAQ,GAAG,MAAM,IAAI,MAAM,GAAG;kBAAzD;MAKC,UAAA,GAAC,MAAQ,MAAI,WAAW;MAEvB,WAAA,GAAC,MAAQ,MAAI,WAAW;MAEpB,iBAAA;;;;;;;;;;;;;;;;KAgBV,qBAAqB,KAAK,eAAc,wBAAwB,MACvE,QAAQ,QAAQ,mDAEf,YAAY,QAAQ,QAAQ;KAG7B,YAAY,KACf,UAAU,oBAAoB,KAC1B,IACA,UAAU,4BAWR;iBAEQ,QAAQ,iBAAiB,SAAS;;;;;;;;;;;;iBAmBlC,mBAAmB,KAAK,gBAAgB;;;UChGvC;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;EAMA,gBAAgB,KAAK;;EAErB;;;;;;EAMA,QAAQ;;EAER,QAAQ,KAAK;;;;;;;;;;;;;;;;;iBA0DC,eAAe,UAAS,wBAA6B;;;;;;;;;;;;;;;;;;;UCtEpD;EACf,IAAI,cAAc;EAClB,IAAI,aAAa,eAAe;IAAY;MAA2B;EACvE,OAAO,cAAc;;UAQN;;EAEf;;EAEA;;;;;;;;;;;;cAaW,4BAA4B;mBAKpB;mBAJF;mBACA;EAEjB,YACmB,IAAI,QACrB,SAAS;EAML,UAAU,cAAc;IAAU;IAAmB,WAAW;;EAahE,UAAU,cAAc;EAQxB,MAAM,cAAc;UAIZ;UAUA;;UAOC;;EAEf;;;;;;;;;;;;cAaW,0BAA0B;mBAIlB;mBAHF;EAEjB,YACmB,IAAI,QACrB,UAAS;EAKL,IAAI,cAAc,QAAQ;EAU1B,IAAI,aAAa,MAAM,aAAa,iBAAiB;EAMrD,QAAQ,cAAc;EAItB,MAAM,aAAa,iBAAiB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v7.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
11
|
import"reflect-metadata";function setClassMeta(key,value,target){Reflect.defineMetadata(key,value,target)}function setMethodMeta(key,value,target,method){Reflect.defineMetadata(key,value,target,method)}function getClassMeta(key,target,fallback){return Reflect.getMetadata(key,target)??fallback}function getMethodMeta(key,target,method,fallback){return Reflect.getMetadata(key,target,method)??fallback}function hasClassMeta(key,target){return Reflect.hasMetadata(key,target)}function getClassMetaOrUndefined(key,target){return Reflect.getMetadata(key,target)}function getMethodMetaOrUndefined(key,target,method){return Reflect.getMetadata(key,target,method)}function pushClassMeta(key,target,...items){let existing=Reflect.getOwnMetadata(key,target)??[];Reflect.defineMetadata(key,[...existing,...items],target)}function pushMethodMeta(key,target,method,...items){let existing=Reflect.getOwnMetadata(key,target,method)??[];Reflect.defineMetadata(key,[...existing,...items],target,method)}function setInMetaMap(key,target,mapKey,mapValue){let inherited=Reflect.getOwnMetadata(key,target),map=inherited?new Map(inherited):new Map;map.set(mapKey,mapValue),Reflect.defineMetadata(key,map,target)}function getMetaMap(key,target){return Reflect.getMetadata(key,target)??new Map}function setInMetaRecord(key,target,recKey,recValue){let inherited=Reflect.getOwnMetadata(key,target),rec=inherited?{...inherited}:{};rec[recKey]=recValue,Reflect.defineMetadata(key,rec,target)}function getMetaRecord(key,target){return Reflect.getMetadata(key,target)??{}}export{getMethodMeta as a,pushClassMeta as c,setInMetaMap as d,setInMetaRecord as f,getMetaRecord as i,pushMethodMeta as l,getClassMetaOrUndefined as n,getMethodMetaOrUndefined as o,setMethodMeta as p,getMetaMap as r,hasClassMeta as s,getClassMeta as t,setClassMeta as u};
|
|
12
|
-
//# sourceMappingURL=metadata-
|
|
12
|
+
//# sourceMappingURL=metadata-CyyYO_qa.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-
|
|
1
|
+
{"version":3,"file":"metadata-CyyYO_qa.mjs","names":[],"sources":["../src/core/metadata.ts"],"sourcesContent":["import 'reflect-metadata'\nimport type { InjectionToken } from './token'\n\n/**\n * Typed utilities for reflect-metadata operations.\n *\n * Replaces scattered Reflect.defineMetadata/getMetadata calls with a single\n * source of truth. If we later move to Stage 3 decorators or a WeakMap-based\n * store, only this file changes — all 24+ consumer files stay untouched.\n *\n * Keys may be symbols, strings, or `InjectionToken<T>` objects produced by\n * `createToken()`. Using `createToken` is the preferred pattern because the\n * token's phantom type flows through the helpers, enabling type-safe reads.\n */\n\ntype MetaKey = symbol | string | InjectionToken<unknown>\n\n// ── Setters ─────────────────────────────────────────────────────\n\n/** Set metadata on a class (or prototype for property decorators) */\nexport function setClassMeta<T>(key: MetaKey, value: T, target: object): void {\n Reflect.defineMetadata(key, value, target)\n}\n\n/** Set metadata on a specific method */\nexport function setMethodMeta<T>(key: MetaKey, value: T, target: object, method: string): void {\n Reflect.defineMetadata(key, value, target, method)\n}\n\n// ── Getters ─────────────────────────────────────────────────────\n\n/** Get metadata from a class, returning `fallback` if not set */\nexport function getClassMeta<T>(key: MetaKey, target: object, fallback: T): T {\n return (Reflect.getMetadata(key, target) as T) ?? fallback\n}\n\n/** Get metadata from a specific method, returning `fallback` if not set */\nexport function getMethodMeta<T>(key: MetaKey, target: object, method: string, fallback: T): T {\n return (Reflect.getMetadata(key, target, method) as T) ?? fallback\n}\n\n/** Check if metadata exists on a class */\nexport function hasClassMeta(key: MetaKey, target: object): boolean {\n return Reflect.hasMetadata(key, target)\n}\n\n/** Get metadata from a class with no default (returns undefined if absent) */\nexport function getClassMetaOrUndefined<T>(key: MetaKey, target: object): T | undefined {\n return Reflect.getMetadata(key, target) as T | undefined\n}\n\n/** Get metadata from a method with no default (returns undefined if absent) */\nexport function getMethodMetaOrUndefined<T>(\n key: MetaKey,\n target: object,\n method: string,\n): T | undefined {\n return Reflect.getMetadata(key, target, method) as T | undefined\n}\n\n// ── Accumulate: Arrays ──────────────────────────────────────────\n\n/** Append items to an array stored in class metadata */\nexport function pushClassMeta<T>(key: MetaKey, target: object, ...items: T[]): void {\n const existing: T[] = Reflect.getOwnMetadata(key, target) ?? []\n Reflect.defineMetadata(key, [...existing, ...items], target)\n}\n\n/** Append items to an array stored in method metadata */\nexport function pushMethodMeta<T>(\n key: MetaKey,\n target: object,\n method: string,\n ...items: T[]\n): void {\n const existing: T[] = Reflect.getOwnMetadata(key, target, method) ?? []\n Reflect.defineMetadata(key, [...existing, ...items], target, method)\n}\n\n// ── Accumulate: Maps ────────────────────────────────────────────\n\n/** Set a key/value in a Map stored in class metadata */\nexport function setInMetaMap<K, V>(key: MetaKey, target: object, mapKey: K, mapValue: V): void {\n const inherited: Map<K, V> | undefined = Reflect.getOwnMetadata(key, target)\n const map = inherited ? new Map(inherited) : new Map<K, V>()\n map.set(mapKey, mapValue)\n Reflect.defineMetadata(key, map, target)\n}\n\n/** Get a Map from class metadata (returns empty Map if absent) */\nexport function getMetaMap<K, V>(key: MetaKey, target: object): Map<K, V> {\n return Reflect.getMetadata(key, target) ?? new Map()\n}\n\n// ── Accumulate: Records ─────────────────────────────────────────\n\n/** Set a key/value in a Record stored in class metadata */\nexport function setInMetaRecord<V>(\n key: MetaKey,\n target: object,\n recKey: string | number,\n recValue: V,\n): void {\n const inherited: Record<string | number, V> | undefined = Reflect.getOwnMetadata(key, target)\n const rec = inherited ? { ...inherited } : {}\n rec[recKey] = recValue\n Reflect.defineMetadata(key, rec, target)\n}\n\n/** Get a Record from class metadata (returns empty object if absent) */\nexport function getMetaRecord<V>(key: MetaKey, target: object): Record<string | number, V> {\n return Reflect.getMetadata(key, target) ?? {}\n}\n"],"mappings":";;;;;;;;;;yBAoBA,SAAgB,aAAgB,IAAc,MAAU,OAAsB,CAC5E,QAAQ,eAAe,IAAK,MAAO,MAAM,CAC3C,CAGA,SAAgB,cAAiB,IAAc,MAAU,OAAgB,OAAsB,CAC7F,QAAQ,eAAe,IAAK,MAAO,OAAQ,MAAM,CACnD,CAKA,SAAgB,aAAgB,IAAc,OAAgB,SAAgB,CAC5E,OAAQ,QAAQ,YAAY,IAAK,MAAM,GAAW,QACpD,CAGA,SAAgB,cAAiB,IAAc,OAAgB,OAAgB,SAAgB,CAC7F,OAAQ,QAAQ,YAAY,IAAK,OAAQ,MAAM,GAAW,QAC5D,CAGA,SAAgB,aAAa,IAAc,OAAyB,CAClE,OAAO,QAAQ,YAAY,IAAK,MAAM,CACxC,CAGA,SAAgB,wBAA2B,IAAc,OAA+B,CACtF,OAAO,QAAQ,YAAY,IAAK,MAAM,CACxC,CAGA,SAAgB,yBACd,IACA,OACA,OACe,CACf,OAAO,QAAQ,YAAY,IAAK,OAAQ,MAAM,CAChD,CAKA,SAAgB,cAAiB,IAAc,OAAgB,GAAG,MAAkB,CAClF,IAAM,SAAgB,QAAQ,eAAe,IAAK,MAAM,GAAK,CAAC,EAC9D,QAAQ,eAAe,IAAK,CAAC,GAAG,SAAU,GAAG,KAAK,EAAG,MAAM,CAC7D,CAGA,SAAgB,eACd,IACA,OACA,OACA,GAAG,MACG,CACN,IAAM,SAAgB,QAAQ,eAAe,IAAK,OAAQ,MAAM,GAAK,CAAC,EACtE,QAAQ,eAAe,IAAK,CAAC,GAAG,SAAU,GAAG,KAAK,EAAG,OAAQ,MAAM,CACrE,CAKA,SAAgB,aAAmB,IAAc,OAAgB,OAAW,SAAmB,CAC7F,IAAM,UAAmC,QAAQ,eAAe,IAAK,MAAM,EACrE,IAAM,UAAY,IAAI,IAAI,SAAS,EAAI,IAAI,IACjD,IAAI,IAAI,OAAQ,QAAQ,EACxB,QAAQ,eAAe,IAAK,IAAK,MAAM,CACzC,CAGA,SAAgB,WAAiB,IAAc,OAA2B,CACxE,OAAO,QAAQ,YAAY,IAAK,MAAM,GAAK,IAAI,GACjD,CAKA,SAAgB,gBACd,IACA,OACA,OACA,SACM,CACN,IAAM,UAAoD,QAAQ,eAAe,IAAK,MAAM,EACtF,IAAM,UAAY,CAAE,GAAG,SAAU,EAAI,CAAC,EAC5C,IAAI,QAAU,SACd,QAAQ,eAAe,IAAK,IAAK,MAAM,CACzC,CAGA,SAAgB,cAAiB,IAAc,OAA4C,CACzF,OAAO,QAAQ,YAAY,IAAK,MAAM,GAAK,CAAC,CAC9C"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v7.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
11
|
var MutableModuleRegistry=class{entries=[];mount(module){this.entries.push(module)}};export{MutableModuleRegistry as t};
|
|
12
|
-
//# sourceMappingURL=module-registry-
|
|
12
|
+
//# sourceMappingURL=module-registry-BfOa58-E.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-registry-
|
|
1
|
+
{"version":3,"file":"module-registry-BfOa58-E.mjs","names":[],"sources":["../src/core/module-registry.ts"],"sourcesContent":["import type { AppModuleEntry } from './app-module'\n\n/**\n * Imperative receiver passed to {@link KickPlugin.setup} and the\n * top-level `bootstrap({ setup })` callback so adopters can register\n * modules conditionally — based on env flags, runtime config,\n * tenant lists, etc. — instead of being limited to the static\n * `modules: [...]` array.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules: [HelloModule()], // static — always mounted\n * setup(registry) {\n * if (env.ENABLE_ADMIN) registry.mount(AdminModule())\n * for (const tenant of env.TENANTS) {\n * registry.mount(TenantModule.scoped(tenant.id, { id: tenant.id }))\n * }\n * },\n * })\n * ```\n *\n * **Currently exposes only `.mount(module)` — the HTTP-feature path.**\n * A future `.use(module)` for non-HTTP modules (queues, cron, workers,\n * DI-only seeds) is planned but not yet implemented; existing non-HTTP\n * modules continue returning `null` from `routes()` and registering\n * via `.mount()`.\n *\n * Same dispatch rules as the static `modules: [...]` array — class\n * entries get `new`-ed at boot, `defineModule` factory output is used\n * directly. The registry doesn't validate the entry shape itself; the\n * Application loader does the typeof discrimination at mount time.\n */\nexport interface ModuleRegistry {\n /**\n * Register a module for the full HTTP lifecycle: `register()` runs,\n * `import.meta.glob` decorator side-effects fire, `routes()` is\n * called and its result mounted, `contributors()` are merged at\n * `'module'` precedence and applied to the mounted routes.\n *\n * Modules are mounted in registration order (first call → first\n * mount). Order across sources is: plugin static modules → plugin\n * `setup()` calls → user static modules → user `setup()` callback.\n */\n mount(module: AppModuleEntry): void\n}\n\n/**\n * Internal collector implementing {@link ModuleRegistry}. Application\n * passes one of these to every `setup()` callback during bootstrap;\n * the collected `entries` array is then run through the same\n * class-vs-instance dispatch as the static `modules: [...]` array.\n *\n * Not exported from the public surface — adopters interact only\n * through the {@link ModuleRegistry} interface.\n */\nexport class MutableModuleRegistry implements ModuleRegistry {\n readonly entries: AppModuleEntry[] = []\n\n mount(module: AppModuleEntry): void {\n this.entries.push(module)\n }\n}\n"],"mappings":";;;;;;;;;;AAwDA,IAAa,sBAAb,KAA6D,CAC3D,QAAqC,CAAC,EAEtC,MAAM,OAA8B,CAClC,KAAK,QAAQ,KAAK,MAAM,CAC1B,CACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v7.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
11
|
const FILTER_OPERATORS=new Set([`eq`,`neq`,`gt`,`gte`,`lt`,`lte`,`between`,`in`,`contains`,`starts`,`ends`]),BUILTIN_DEFAULTS={maxLimit:100,defaultLimit:20,maxSearchLength:200,defaultPage:1},defaults={...BUILTIN_DEFAULTS};function setQueryParsingDefaults(partial){partial.maxLimit!==void 0&&(defaults.maxLimit=partial.maxLimit),partial.defaultLimit!==void 0&&(defaults.defaultLimit=partial.defaultLimit),partial.maxSearchLength!==void 0&&(defaults.maxSearchLength=partial.maxSearchLength)}function getQueryParsingDefaults(){return{...defaults}}function resetQueryParsingDefaults(){Object.assign(defaults,BUILTIN_DEFAULTS)}function parseFilters(filterParam,allowedFields,options){if(!filterParam)return[];let items=Array.isArray(filterParam)?filterParam:[filterParam],results=[],allowed=allowedFields?new Set(allowedFields):null,onReject=options?.onReject;for(let item of items){let firstColon=item.indexOf(`:`);if(firstColon===-1)continue;let secondColon=item.indexOf(`:`,firstColon+1);if(secondColon===-1)continue;let field=item.slice(0,firstColon),operator=item.slice(firstColon+1,secondColon),value=item.slice(secondColon+1);if(!(!field||!value)){if(!FILTER_OPERATORS.has(operator)){onReject?.({kind:`filter`,field,reason:`unknown-operator`});continue}if(allowed&&!allowed.has(field)){onReject?.({kind:`filter`,field,reason:`field-not-allowed`});continue}results.push({field,operator,value})}}return results}function parseSort(sortParam,allowedFields,options){if(!sortParam)return[];let items=Array.isArray(sortParam)?sortParam:[sortParam],results=[],allowed=allowedFields?new Set(allowedFields):null,onReject=options?.onReject;for(let item of items){let lastColon=item.lastIndexOf(`:`);if(lastColon===-1)continue;let field=item.slice(0,lastColon),dir=item.slice(lastColon+1).toLowerCase();if(field&&(dir===`asc`||dir===`desc`)){if(allowed&&!allowed.has(field)){onReject?.({kind:`sort`,field,reason:`field-not-allowed`});continue}results.push({field,direction:dir})}}return results}function parsePagination(params,options){let defaultLimit=options?.defaultLimit??defaults.defaultLimit,maxLimit=options?.maxLimit??defaults.maxLimit,page=typeof params.page==`string`?Number.parseInt(params.page,10):params.page??defaults.defaultPage,limit=typeof params.limit==`string`?Number.parseInt(params.limit,10):params.limit??defaultLimit;return(Number.isNaN(page)||page<1)&&(page=defaults.defaultPage),(Number.isNaN(limit)||limit<1)&&(limit=defaultLimit),limit>maxLimit&&(limit=maxLimit),{page,limit,offset:(page-1)*limit}}function parseSearchQuery(q,options){if(!q)return``;let maxLen=options?.maxSearchLength??defaults.maxSearchLength,trimmed=q.trim();return trimmed.length>maxLen?(options?.onReject?.({kind:`search`,field:`q`,reason:`truncated`}),trimmed.slice(0,maxLen)):trimmed}function normalizeFieldConfig(config){if(config)return`columns`in config&&config.columns&&typeof config.columns==`object`?{filterable:Object.keys(config.columns),sortable:config.sortable?Object.keys(config.sortable):void 0,searchable:void 0}:config}function parseQuery(query,fieldConfig,options){let normalized=normalizeFieldConfig(fieldConfig),rejectOpts=options?.onReject?{onReject:options.onReject}:void 0;return{filters:parseFilters(query.filter,normalized?.filterable,rejectOpts),sort:parseSort(query.sort,normalized?.sortable,rejectOpts),pagination:parsePagination({page:query.page,limit:query.limit},options),search:parseSearchQuery(query.q,options)}}function buildQueryParams(parsed){let params={};return parsed.filters?.length&&(params.filter=parsed.filters.map(f=>`${f.field}:${f.operator}:${f.value}`)),parsed.sort?.length&&(params.sort=parsed.sort.map(s=>`${s.field}:${s.direction}`)),parsed.pagination&&(params.page=parsed.pagination.page,params.limit=parsed.pagination.limit),parsed.search&&(params.q=parsed.search),params}export{parseQuery as a,resetQueryParsingDefaults as c,parsePagination as i,setQueryParsingDefaults as l,getQueryParsingDefaults as n,parseSearchQuery as o,parseFilters as r,parseSort as s,buildQueryParams as t,FILTER_OPERATORS as u};
|
|
12
|
-
//# sourceMappingURL=query-
|
|
12
|
+
//# sourceMappingURL=query-BYUCKdu5.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-B_YnIZ2-.mjs","names":[],"sources":["../src/http/query/types.ts","../src/http/query/parse-query.ts"],"sourcesContent":["/** Supported filter operators for query string parsing */\nexport type FilterOperator =\n | 'eq'\n | 'neq'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'between'\n | 'in'\n | 'contains'\n | 'starts'\n | 'ends'\n\nexport const FILTER_OPERATORS = new Set<string>([\n 'eq',\n 'neq',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'between',\n 'in',\n 'contains',\n 'starts',\n 'ends',\n])\n\nexport interface FilterItem {\n field: string\n operator: FilterOperator\n value: string\n}\n\nexport interface SortItem {\n field: string\n direction: 'asc' | 'desc'\n}\n\nexport interface PaginationParams {\n page: number\n limit: number\n offset: number\n}\n\n/**\n * The result of parsing a query string. ORM-agnostic — pass this to\n * a query builder adapter (Drizzle, Prisma, Sequelize, etc.) to produce\n * database-specific query objects.\n */\nexport interface ParsedQuery {\n filters: FilterItem[]\n sort: SortItem[]\n pagination: PaginationParams\n search: string\n}\n\n/**\n * Helper that extracts the literal field union from a `QueryFieldConfig`\n * key. When the user passes `{ filterable: ['status', 'category'] } as const`,\n * `FieldsOf<typeof config, 'filterable'>` resolves to `'status' | 'category'`.\n *\n * Without `as const`, the arrays widen to `string[]` and the union falls\n * back to `string`. This is the documented escape hatch.\n */\nexport type FieldsOf<\n TConfig extends QueryFieldConfig | undefined,\n K extends 'filterable' | 'sortable' | 'searchable',\n> = TConfig extends { readonly [P in K]: readonly (infer F)[] }\n ? F extends string\n ? F\n : string\n : TConfig extends { columns: infer C }\n ? K extends 'filterable'\n ? keyof C & string\n : string\n : string\n\n/**\n * Narrowed `ParsedQuery` whose `filters[].field` and `sort[].field`\n * unions are constrained to the literal whitelist passed to\n * `ctx.qs()` / `parseQuery()`. Extends `ParsedQuery`, so any function\n * accepting `ParsedQuery` accepts a `TypedParsedQuery<X>` automatically\n * (subtype assignment, no codegen, backward compatible).\n *\n * @example\n * ```ts\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt'],\n * } as const)\n * parsed.filters[0]?.field // 'status' | 'priority'\n * parsed.sort[0]?.field // 'createdAt'\n * ```\n */\nexport interface TypedParsedQuery<\n TConfig extends QueryFieldConfig | undefined,\n> extends ParsedQuery {\n filters: Array<FilterItem & { field: FieldsOf<TConfig, 'filterable'> }>\n sort: Array<SortItem & { field: FieldsOf<TConfig, 'sortable'> }>\n}\n\n/**\n * Restrict which fields can be filtered, sorted, or searched.\n * Fields not in the allow-list are silently ignored.\n *\n * Accepts two forms:\n * 1. **String arrays** (standard): `{ filterable: ['name', 'status'], sortable: ['createdAt'] }`\n * 2. **Column-object maps** (Drizzle-style): `{ columns: { name: col, status: col }, sortable: { createdAt: col } }`\n *\n * When column-object maps are provided, `Object.keys()` is used to derive the field names.\n */\nexport type QueryFieldConfig = StringQueryFieldConfig | ColumnQueryFieldConfig\n\nexport interface StringQueryFieldConfig {\n filterable?: string[]\n sortable?: string[]\n searchable?: string[]\n}\n\n/**\n * Column-object-based field config (e.g., from DrizzleQueryParamsConfig).\n * Keys are the field names used in query strings; values are ORM column references.\n */\nexport interface ColumnQueryFieldConfig {\n columns: Record<string, any>\n sortable?: Record<string, any>\n searchColumns?: any[]\n [key: string]: any\n}\n\n/** Standardized paginated response shape */\nexport interface PaginatedResponse<T = any> {\n data: T[]\n meta: {\n page: number\n limit: number\n total: number\n totalPages: number\n hasNext: boolean\n hasPrev: boolean\n }\n}\n\n/**\n * Interface for ORM-specific query builder adapters.\n * Implement this to translate a `ParsedQuery` into your ORM's query format.\n *\n * @example\n * ```ts\n * // Drizzle adapter\n * class DrizzleQueryAdapter implements QueryBuilderAdapter<DrizzleQueryResult> {\n * build(parsed: ParsedQuery, config: DrizzleConfig): DrizzleQueryResult {\n * // Convert filters → Drizzle SQL conditions\n * // Convert sort → Drizzle orderBy\n * return { where, orderBy, limit, offset }\n * }\n * }\n *\n * // Prisma adapter\n * class PrismaQueryAdapter implements QueryBuilderAdapter<PrismaQueryResult> {\n * build(parsed: ParsedQuery, config: PrismaConfig): PrismaQueryResult {\n * return { where, orderBy, skip, take }\n * }\n * }\n * ```\n */\nexport interface QueryBuilderAdapter<TResult = any, TConfig = any> {\n /** Human-readable name for debugging */\n readonly name: string\n\n /**\n * Convert a ParsedQuery into an ORM-specific query object.\n * @param parsed - The ORM-agnostic parsed query\n * @param config - ORM-specific configuration (column maps, search columns, etc.)\n */\n build(parsed: ParsedQuery, config: TConfig): TResult\n}\n","import {\n FILTER_OPERATORS,\n type FilterItem,\n type FilterOperator,\n type SortItem,\n type PaginationParams,\n type ParsedQuery,\n type QueryFieldConfig,\n type StringQueryFieldConfig,\n type TypedParsedQuery,\n} from './types'\n\n// ── Configurable defaults ───────────────────────────────────────────────\n\n/**\n * Why a rejected field/value was dropped. Surfaced through\n * {@link ParseQueryOptions.onReject} so a caller can warn, count, or\n * return a 400 instead of the historical silent drop.\n */\nexport type QueryRejectReason = 'field-not-allowed' | 'unknown-operator' | 'truncated'\n\nexport interface QueryRejection {\n kind: 'filter' | 'sort' | 'search'\n field: string\n reason: QueryRejectReason\n}\n\nexport interface ParseQueryOptions {\n /** Hard cap on `limit`. Default: global (100, override via {@link setQueryParsingDefaults}). */\n maxLimit?: number\n /** `limit` when the query omits one. Default: global (20). */\n defaultLimit?: number\n /** Max `q` length before truncation. Default: global (200). */\n maxSearchLength?: number\n /**\n * Called once per dropped filter/sort field or truncated search.\n * The historical behaviour is a silent drop; wire this to a logger or\n * a 400 to make over-broad / typo'd queries visible.\n */\n onReject?: (rejection: QueryRejection) => void\n}\n\ninterface QueryParsingDefaults {\n maxLimit: number\n defaultLimit: number\n maxSearchLength: number\n defaultPage: number\n}\n\nconst BUILTIN_DEFAULTS: QueryParsingDefaults = {\n maxLimit: 100,\n defaultLimit: 20,\n maxSearchLength: 200,\n defaultPage: 1,\n}\n\nconst defaults: QueryParsingDefaults = { ...BUILTIN_DEFAULTS }\n\n/**\n * Globally override the query-parsing limits — call once at bootstrap.\n * Per-call {@link ParseQueryOptions} still take precedence over these.\n */\nexport function setQueryParsingDefaults(\n partial: Partial<Omit<QueryParsingDefaults, 'defaultPage'>>,\n): void {\n if (partial.maxLimit !== undefined) defaults.maxLimit = partial.maxLimit\n if (partial.defaultLimit !== undefined) defaults.defaultLimit = partial.defaultLimit\n if (partial.maxSearchLength !== undefined) defaults.maxSearchLength = partial.maxSearchLength\n}\n\n/** Read the current global query-parsing defaults (a copy). */\nexport function getQueryParsingDefaults(): Readonly<QueryParsingDefaults> {\n return { ...defaults }\n}\n\n/**\n * Restore the built-in query-parsing defaults (maxLimit 100, defaultLimit\n * 20, maxSearchLength 200). Useful in test teardown after a\n * {@link setQueryParsingDefaults} override so global state can't leak\n * between tests.\n */\nexport function resetQueryParsingDefaults(): void {\n Object.assign(defaults, BUILTIN_DEFAULTS)\n}\n\n// ── Individual parsers ──────────────────────────────────────────────────\n\n/** Parse filter strings like \"status:eq:active\" into structured objects */\nexport function parseFilters(\n filterParam: string | string[] | undefined,\n allowedFields?: string[],\n options?: Pick<ParseQueryOptions, 'onReject'>,\n): FilterItem[] {\n if (!filterParam) return []\n\n const items = Array.isArray(filterParam) ? filterParam : [filterParam]\n const results: FilterItem[] = []\n const allowed = allowedFields ? new Set(allowedFields) : null\n const onReject = options?.onReject\n\n for (const item of items) {\n // Split on first two colons only — value may contain colons (e.g. timestamps)\n const firstColon = item.indexOf(':')\n if (firstColon === -1) continue\n\n const secondColon = item.indexOf(':', firstColon + 1)\n if (secondColon === -1) continue\n\n const field = item.slice(0, firstColon)\n const operator = item.slice(firstColon + 1, secondColon)\n const value = item.slice(secondColon + 1)\n\n if (!field || !value) continue\n if (!FILTER_OPERATORS.has(operator)) {\n onReject?.({ kind: 'filter', field, reason: 'unknown-operator' })\n continue\n }\n if (allowed && !allowed.has(field)) {\n onReject?.({ kind: 'filter', field, reason: 'field-not-allowed' })\n continue\n }\n\n results.push({ field, operator: operator as FilterOperator, value })\n }\n\n return results\n}\n\n/** Parse sort strings like \"firstName:asc\" into structured objects */\nexport function parseSort(\n sortParam: string | string[] | undefined,\n allowedFields?: string[],\n options?: Pick<ParseQueryOptions, 'onReject'>,\n): SortItem[] {\n if (!sortParam) return []\n\n const items = Array.isArray(sortParam) ? sortParam : [sortParam]\n const results: SortItem[] = []\n const allowed = allowedFields ? new Set(allowedFields) : null\n const onReject = options?.onReject\n\n for (const item of items) {\n // Split on last colon so field names with colons work\n const lastColon = item.lastIndexOf(':')\n if (lastColon === -1) continue\n\n const field = item.slice(0, lastColon)\n const dir = item.slice(lastColon + 1).toLowerCase()\n\n if (!field) continue\n if (dir !== 'asc' && dir !== 'desc') continue\n if (allowed && !allowed.has(field)) {\n onReject?.({ kind: 'sort', field, reason: 'field-not-allowed' })\n continue\n }\n\n results.push({ field, direction: dir })\n }\n\n return results\n}\n\n/** Parse page/limit into pagination with computed offset */\nexport function parsePagination(\n params: {\n page?: string | number\n limit?: string | number\n },\n options?: Pick<ParseQueryOptions, 'maxLimit' | 'defaultLimit'>,\n): PaginationParams {\n const defaultLimit = options?.defaultLimit ?? defaults.defaultLimit\n const maxLimit = options?.maxLimit ?? defaults.maxLimit\n\n let page =\n typeof params.page === 'string'\n ? Number.parseInt(params.page, 10)\n : (params.page ?? defaults.defaultPage)\n let limit =\n typeof params.limit === 'string'\n ? Number.parseInt(params.limit, 10)\n : (params.limit ?? defaultLimit)\n\n if (Number.isNaN(page) || page < 1) page = defaults.defaultPage\n if (Number.isNaN(limit) || limit < 1) limit = defaultLimit\n if (limit > maxLimit) limit = maxLimit\n\n return { page, limit, offset: (page - 1) * limit }\n}\n\n/** Sanitize and truncate search query */\nexport function parseSearchQuery(\n q: string | undefined,\n options?: Pick<ParseQueryOptions, 'maxSearchLength' | 'onReject'>,\n): string {\n if (!q) return ''\n const maxLen = options?.maxSearchLength ?? defaults.maxSearchLength\n const trimmed = q.trim()\n if (trimmed.length > maxLen) {\n options?.onReject?.({ kind: 'search', field: 'q', reason: 'truncated' })\n return trimmed.slice(0, maxLen)\n }\n return trimmed\n}\n\n// ── Config normalizer ───────────────────────────────────────────────────\n\n/**\n * Normalize a QueryFieldConfig into the string-based form.\n * Detects column-object-based configs (DrizzleQueryParamsConfig) by checking\n * for a `columns` property and extracts `Object.keys()`.\n */\nfunction normalizeFieldConfig(config?: QueryFieldConfig): StringQueryFieldConfig | undefined {\n if (!config) return undefined\n\n // Column-object-based config (e.g., DrizzleQueryParamsConfig)\n if ('columns' in config && config.columns && typeof config.columns === 'object') {\n return {\n filterable: Object.keys(config.columns),\n sortable: config.sortable ? Object.keys(config.sortable) : undefined,\n searchable: undefined, // searchColumns are Column objects, not used for string filtering\n }\n }\n\n // Already string-based\n return config as StringQueryFieldConfig\n}\n\n// ── Combined parser ─────────────────────────────────────────────────────\n\n/**\n * Parse a raw Express query object into a structured, ORM-agnostic ParsedQuery.\n *\n * @param query - Raw query string object from `req.query` or Zod-validated object\n * @param fieldConfig - Optional field restrictions (whitelist filterable/sortable/searchable).\n * Accepts both string-based configs (`{ filterable, sortable, searchable }`) and\n * column-object configs (`{ columns, sortable, searchColumns }`).\n * @param options - Limits + an `onReject` hook for dropped/truncated input.\n *\n * @example\n * ```ts\n * // String-based config\n * const parsed = parseQuery(ctx.query, {\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * })\n *\n * // Column-object config (DrizzleQueryParamsConfig)\n * const parsed = parseQuery(ctx.query, TASK_QUERY_CONFIG)\n * ```\n *\n * Query string format:\n * - Filters: `?filter=field:operator:value` (repeatable)\n * - Sort: `?sort=field:asc|desc` (repeatable)\n * - Pagination: `?page=1&limit=20`\n * - Search: `?q=search+term`\n *\n * Filter operators: eq, neq, gt, gte, lt, lte, between, in, contains, starts, ends\n */\nexport function parseQuery<TConfig extends QueryFieldConfig | undefined = undefined>(\n query: Record<string, any>,\n fieldConfig?: TConfig,\n options?: ParseQueryOptions,\n): TypedParsedQuery<TConfig> {\n const normalized = normalizeFieldConfig(fieldConfig)\n const rejectOpts = options?.onReject ? { onReject: options.onReject } : undefined\n return {\n filters: parseFilters(query.filter, normalized?.filterable, rejectOpts),\n sort: parseSort(query.sort, normalized?.sortable, rejectOpts),\n pagination: parsePagination({ page: query.page, limit: query.limit }, options),\n search: parseSearchQuery(query.q, options),\n } as TypedParsedQuery<TConfig>\n}\n\n// ── Query URL builder (for client-side or testing) ──────────────────────\n\n/** Convert ParsedQuery back into query string parameters */\nexport function buildQueryParams(\n parsed: Partial<ParsedQuery>,\n): Record<string, string | string[] | number> {\n const params: Record<string, string | string[] | number> = {}\n\n if (parsed.filters?.length) {\n params.filter = parsed.filters.map((f) => `${f.field}:${f.operator}:${f.value}`)\n }\n\n if (parsed.sort?.length) {\n params.sort = parsed.sort.map((s) => `${s.field}:${s.direction}`)\n }\n\n if (parsed.pagination) {\n params.page = parsed.pagination.page\n params.limit = parsed.pagination.limit\n }\n\n if (parsed.search) {\n params.q = parsed.search\n }\n\n return params\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"query-BYUCKdu5.mjs","names":[],"sources":["../src/http/query/types.ts","../src/http/query/parse-query.ts"],"sourcesContent":["/** Supported filter operators for query string parsing */\nexport type FilterOperator =\n | 'eq'\n | 'neq'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'between'\n | 'in'\n | 'contains'\n | 'starts'\n | 'ends'\n\nexport const FILTER_OPERATORS = new Set<string>([\n 'eq',\n 'neq',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'between',\n 'in',\n 'contains',\n 'starts',\n 'ends',\n])\n\nexport interface FilterItem {\n field: string\n operator: FilterOperator\n value: string\n}\n\nexport interface SortItem {\n field: string\n direction: 'asc' | 'desc'\n}\n\nexport interface PaginationParams {\n page: number\n limit: number\n offset: number\n}\n\n/**\n * The result of parsing a query string. ORM-agnostic — pass this to\n * a query builder adapter (Drizzle, Prisma, Sequelize, etc.) to produce\n * database-specific query objects.\n */\nexport interface ParsedQuery {\n filters: FilterItem[]\n sort: SortItem[]\n pagination: PaginationParams\n search: string\n}\n\n/**\n * Helper that extracts the literal field union from a `QueryFieldConfig`\n * key. When the user passes `{ filterable: ['status', 'category'] } as const`,\n * `FieldsOf<typeof config, 'filterable'>` resolves to `'status' | 'category'`.\n *\n * Without `as const`, the arrays widen to `string[]` and the union falls\n * back to `string`. This is the documented escape hatch.\n */\nexport type FieldsOf<\n TConfig extends QueryFieldConfig | undefined,\n K extends 'filterable' | 'sortable' | 'searchable',\n> = TConfig extends { readonly [P in K]: readonly (infer F)[] }\n ? F extends string\n ? F\n : string\n : TConfig extends { columns: infer C }\n ? K extends 'filterable'\n ? keyof C & string\n : string\n : string\n\n/**\n * Narrowed `ParsedQuery` whose `filters[].field` and `sort[].field`\n * unions are constrained to the literal whitelist passed to\n * `ctx.qs()` / `parseQuery()`. Extends `ParsedQuery`, so any function\n * accepting `ParsedQuery` accepts a `TypedParsedQuery<X>` automatically\n * (subtype assignment, no codegen, backward compatible).\n *\n * @example\n * ```ts\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt'],\n * } as const)\n * parsed.filters[0]?.field // 'status' | 'priority'\n * parsed.sort[0]?.field // 'createdAt'\n * ```\n */\nexport interface TypedParsedQuery<\n TConfig extends QueryFieldConfig | undefined,\n> extends ParsedQuery {\n filters: Array<FilterItem & { field: FieldsOf<TConfig, 'filterable'> }>\n sort: Array<SortItem & { field: FieldsOf<TConfig, 'sortable'> }>\n}\n\n/**\n * Restrict which fields can be filtered, sorted, or searched.\n * Fields not in the allow-list are silently ignored.\n *\n * Accepts two forms:\n * 1. **String arrays** (standard): `{ filterable: ['name', 'status'], sortable: ['createdAt'] }`\n * 2. **Column-object maps** (Drizzle-style): `{ columns: { name: col, status: col }, sortable: { createdAt: col } }`\n *\n * When column-object maps are provided, `Object.keys()` is used to derive the field names.\n */\nexport type QueryFieldConfig = StringQueryFieldConfig | ColumnQueryFieldConfig\n\nexport interface StringQueryFieldConfig {\n filterable?: string[]\n sortable?: string[]\n searchable?: string[]\n}\n\n/**\n * Column-object-based field config (e.g., from DrizzleQueryParamsConfig).\n * Keys are the field names used in query strings; values are ORM column references.\n */\nexport interface ColumnQueryFieldConfig {\n columns: Record<string, any>\n sortable?: Record<string, any>\n searchColumns?: any[]\n [key: string]: any\n}\n\n/** Standardized paginated response shape */\nexport interface PaginatedResponse<T = any> {\n data: T[]\n meta: {\n page: number\n limit: number\n total: number\n totalPages: number\n hasNext: boolean\n hasPrev: boolean\n }\n}\n\n/**\n * Interface for ORM-specific query builder adapters.\n * Implement this to translate a `ParsedQuery` into your ORM's query format.\n *\n * @example\n * ```ts\n * // Drizzle adapter\n * class DrizzleQueryAdapter implements QueryBuilderAdapter<DrizzleQueryResult> {\n * build(parsed: ParsedQuery, config: DrizzleConfig): DrizzleQueryResult {\n * // Convert filters → Drizzle SQL conditions\n * // Convert sort → Drizzle orderBy\n * return { where, orderBy, limit, offset }\n * }\n * }\n *\n * // Prisma adapter\n * class PrismaQueryAdapter implements QueryBuilderAdapter<PrismaQueryResult> {\n * build(parsed: ParsedQuery, config: PrismaConfig): PrismaQueryResult {\n * return { where, orderBy, skip, take }\n * }\n * }\n * ```\n */\nexport interface QueryBuilderAdapter<TResult = any, TConfig = any> {\n /** Human-readable name for debugging */\n readonly name: string\n\n /**\n * Convert a ParsedQuery into an ORM-specific query object.\n * @param parsed - The ORM-agnostic parsed query\n * @param config - ORM-specific configuration (column maps, search columns, etc.)\n */\n build(parsed: ParsedQuery, config: TConfig): TResult\n}\n","import {\n FILTER_OPERATORS,\n type FilterItem,\n type FilterOperator,\n type SortItem,\n type PaginationParams,\n type ParsedQuery,\n type QueryFieldConfig,\n type StringQueryFieldConfig,\n type TypedParsedQuery,\n} from './types'\n\n// ── Configurable defaults ───────────────────────────────────────────────\n\n/**\n * Why a rejected field/value was dropped. Surfaced through\n * {@link ParseQueryOptions.onReject} so a caller can warn, count, or\n * return a 400 instead of the historical silent drop.\n */\nexport type QueryRejectReason = 'field-not-allowed' | 'unknown-operator' | 'truncated'\n\nexport interface QueryRejection {\n kind: 'filter' | 'sort' | 'search'\n field: string\n reason: QueryRejectReason\n}\n\nexport interface ParseQueryOptions {\n /** Hard cap on `limit`. Default: global (100, override via {@link setQueryParsingDefaults}). */\n maxLimit?: number\n /** `limit` when the query omits one. Default: global (20). */\n defaultLimit?: number\n /** Max `q` length before truncation. Default: global (200). */\n maxSearchLength?: number\n /**\n * Called once per dropped filter/sort field or truncated search.\n * The historical behaviour is a silent drop; wire this to a logger or\n * a 400 to make over-broad / typo'd queries visible.\n */\n onReject?: (rejection: QueryRejection) => void\n}\n\ninterface QueryParsingDefaults {\n maxLimit: number\n defaultLimit: number\n maxSearchLength: number\n defaultPage: number\n}\n\nconst BUILTIN_DEFAULTS: QueryParsingDefaults = {\n maxLimit: 100,\n defaultLimit: 20,\n maxSearchLength: 200,\n defaultPage: 1,\n}\n\nconst defaults: QueryParsingDefaults = { ...BUILTIN_DEFAULTS }\n\n/**\n * Globally override the query-parsing limits — call once at bootstrap.\n * Per-call {@link ParseQueryOptions} still take precedence over these.\n */\nexport function setQueryParsingDefaults(\n partial: Partial<Omit<QueryParsingDefaults, 'defaultPage'>>,\n): void {\n if (partial.maxLimit !== undefined) defaults.maxLimit = partial.maxLimit\n if (partial.defaultLimit !== undefined) defaults.defaultLimit = partial.defaultLimit\n if (partial.maxSearchLength !== undefined) defaults.maxSearchLength = partial.maxSearchLength\n}\n\n/** Read the current global query-parsing defaults (a copy). */\nexport function getQueryParsingDefaults(): Readonly<QueryParsingDefaults> {\n return { ...defaults }\n}\n\n/**\n * Restore the built-in query-parsing defaults (maxLimit 100, defaultLimit\n * 20, maxSearchLength 200). Useful in test teardown after a\n * {@link setQueryParsingDefaults} override so global state can't leak\n * between tests.\n */\nexport function resetQueryParsingDefaults(): void {\n Object.assign(defaults, BUILTIN_DEFAULTS)\n}\n\n// ── Individual parsers ──────────────────────────────────────────────────\n\n/** Parse filter strings like \"status:eq:active\" into structured objects */\nexport function parseFilters(\n filterParam: string | string[] | undefined,\n allowedFields?: string[],\n options?: Pick<ParseQueryOptions, 'onReject'>,\n): FilterItem[] {\n if (!filterParam) return []\n\n const items = Array.isArray(filterParam) ? filterParam : [filterParam]\n const results: FilterItem[] = []\n const allowed = allowedFields ? new Set(allowedFields) : null\n const onReject = options?.onReject\n\n for (const item of items) {\n // Split on first two colons only — value may contain colons (e.g. timestamps)\n const firstColon = item.indexOf(':')\n if (firstColon === -1) continue\n\n const secondColon = item.indexOf(':', firstColon + 1)\n if (secondColon === -1) continue\n\n const field = item.slice(0, firstColon)\n const operator = item.slice(firstColon + 1, secondColon)\n const value = item.slice(secondColon + 1)\n\n if (!field || !value) continue\n if (!FILTER_OPERATORS.has(operator)) {\n onReject?.({ kind: 'filter', field, reason: 'unknown-operator' })\n continue\n }\n if (allowed && !allowed.has(field)) {\n onReject?.({ kind: 'filter', field, reason: 'field-not-allowed' })\n continue\n }\n\n results.push({ field, operator: operator as FilterOperator, value })\n }\n\n return results\n}\n\n/** Parse sort strings like \"firstName:asc\" into structured objects */\nexport function parseSort(\n sortParam: string | string[] | undefined,\n allowedFields?: string[],\n options?: Pick<ParseQueryOptions, 'onReject'>,\n): SortItem[] {\n if (!sortParam) return []\n\n const items = Array.isArray(sortParam) ? sortParam : [sortParam]\n const results: SortItem[] = []\n const allowed = allowedFields ? new Set(allowedFields) : null\n const onReject = options?.onReject\n\n for (const item of items) {\n // Split on last colon so field names with colons work\n const lastColon = item.lastIndexOf(':')\n if (lastColon === -1) continue\n\n const field = item.slice(0, lastColon)\n const dir = item.slice(lastColon + 1).toLowerCase()\n\n if (!field) continue\n if (dir !== 'asc' && dir !== 'desc') continue\n if (allowed && !allowed.has(field)) {\n onReject?.({ kind: 'sort', field, reason: 'field-not-allowed' })\n continue\n }\n\n results.push({ field, direction: dir })\n }\n\n return results\n}\n\n/** Parse page/limit into pagination with computed offset */\nexport function parsePagination(\n params: {\n page?: string | number\n limit?: string | number\n },\n options?: Pick<ParseQueryOptions, 'maxLimit' | 'defaultLimit'>,\n): PaginationParams {\n const defaultLimit = options?.defaultLimit ?? defaults.defaultLimit\n const maxLimit = options?.maxLimit ?? defaults.maxLimit\n\n let page =\n typeof params.page === 'string'\n ? Number.parseInt(params.page, 10)\n : (params.page ?? defaults.defaultPage)\n let limit =\n typeof params.limit === 'string'\n ? Number.parseInt(params.limit, 10)\n : (params.limit ?? defaultLimit)\n\n if (Number.isNaN(page) || page < 1) page = defaults.defaultPage\n if (Number.isNaN(limit) || limit < 1) limit = defaultLimit\n if (limit > maxLimit) limit = maxLimit\n\n return { page, limit, offset: (page - 1) * limit }\n}\n\n/** Sanitize and truncate search query */\nexport function parseSearchQuery(\n q: string | undefined,\n options?: Pick<ParseQueryOptions, 'maxSearchLength' | 'onReject'>,\n): string {\n if (!q) return ''\n const maxLen = options?.maxSearchLength ?? defaults.maxSearchLength\n const trimmed = q.trim()\n if (trimmed.length > maxLen) {\n options?.onReject?.({ kind: 'search', field: 'q', reason: 'truncated' })\n return trimmed.slice(0, maxLen)\n }\n return trimmed\n}\n\n// ── Config normalizer ───────────────────────────────────────────────────\n\n/**\n * Normalize a QueryFieldConfig into the string-based form.\n * Detects column-object-based configs (DrizzleQueryParamsConfig) by checking\n * for a `columns` property and extracts `Object.keys()`.\n */\nfunction normalizeFieldConfig(config?: QueryFieldConfig): StringQueryFieldConfig | undefined {\n if (!config) return undefined\n\n // Column-object-based config (e.g., DrizzleQueryParamsConfig)\n if ('columns' in config && config.columns && typeof config.columns === 'object') {\n return {\n filterable: Object.keys(config.columns),\n sortable: config.sortable ? Object.keys(config.sortable) : undefined,\n searchable: undefined, // searchColumns are Column objects, not used for string filtering\n }\n }\n\n // Already string-based\n return config as StringQueryFieldConfig\n}\n\n// ── Combined parser ─────────────────────────────────────────────────────\n\n/**\n * Parse a raw Express query object into a structured, ORM-agnostic ParsedQuery.\n *\n * @param query - Raw query string object from `req.query` or Zod-validated object\n * @param fieldConfig - Optional field restrictions (whitelist filterable/sortable/searchable).\n * Accepts both string-based configs (`{ filterable, sortable, searchable }`) and\n * column-object configs (`{ columns, sortable, searchColumns }`).\n * @param options - Limits + an `onReject` hook for dropped/truncated input.\n *\n * @example\n * ```ts\n * // String-based config\n * const parsed = parseQuery(ctx.query, {\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * })\n *\n * // Column-object config (DrizzleQueryParamsConfig)\n * const parsed = parseQuery(ctx.query, TASK_QUERY_CONFIG)\n * ```\n *\n * Query string format:\n * - Filters: `?filter=field:operator:value` (repeatable)\n * - Sort: `?sort=field:asc|desc` (repeatable)\n * - Pagination: `?page=1&limit=20`\n * - Search: `?q=search+term`\n *\n * Filter operators: eq, neq, gt, gte, lt, lte, between, in, contains, starts, ends\n */\nexport function parseQuery<TConfig extends QueryFieldConfig | undefined = undefined>(\n query: Record<string, any>,\n fieldConfig?: TConfig,\n options?: ParseQueryOptions,\n): TypedParsedQuery<TConfig> {\n const normalized = normalizeFieldConfig(fieldConfig)\n const rejectOpts = options?.onReject ? { onReject: options.onReject } : undefined\n return {\n filters: parseFilters(query.filter, normalized?.filterable, rejectOpts),\n sort: parseSort(query.sort, normalized?.sortable, rejectOpts),\n pagination: parsePagination({ page: query.page, limit: query.limit }, options),\n search: parseSearchQuery(query.q, options),\n } as TypedParsedQuery<TConfig>\n}\n\n// ── Query URL builder (for client-side or testing) ──────────────────────\n\n/** Convert ParsedQuery back into query string parameters */\nexport function buildQueryParams(\n parsed: Partial<ParsedQuery>,\n): Record<string, string | string[] | number> {\n const params: Record<string, string | string[] | number> = {}\n\n if (parsed.filters?.length) {\n params.filter = parsed.filters.map((f) => `${f.field}:${f.operator}:${f.value}`)\n }\n\n if (parsed.sort?.length) {\n params.sort = parsed.sort.map((s) => `${s.field}:${s.direction}`)\n }\n\n if (parsed.pagination) {\n params.page = parsed.pagination.page\n params.limit = parsed.pagination.limit\n }\n\n if (parsed.search) {\n params.q = parsed.search\n }\n\n return params\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit-Dme747JJ.d.mts","names":[],"sources":["../src/http/middleware/rate-limit.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"rate-limit-Dme747JJ.d.mts","names":[],"sources":["../src/http/middleware/rate-limit.ts"],"mappings":";;;UAKiB;EACf,UAAU,cAAc;IAAU;IAAmB,WAAW;;EAChE,UAAU,cAAc;EACxB,MAAM,cAAc;;UAGL;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA,gBAAgB,KAAK;;EAErB;;EAEA,QAAQ;;EAER,QAAQ,KAAK;;EAEb;;;;;;;;;;;;;;;;;;;;;;iBAsFc,UAAU,UAAS,oBAAqB,KAgBnC,SAAO,KAAO,UAAQ,MAAQ,iBAAY,eAAA,cAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v7.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{METADATA}from"./core/interfaces.mjs";import{createLogger}from"./core/logger.mjs";import{n as getClassMetaOrUndefined}from"./metadata-
|
|
12
|
-
//# sourceMappingURL=reply-
|
|
11
|
+
import{METADATA}from"./core/interfaces.mjs";import{createLogger}from"./core/logger.mjs";import{n as getClassMetaOrUndefined}from"./metadata-CyyYO_qa.mjs";import{i as requestStore}from"./context-DYOcPcg8.mjs";const log=createLogger(`RequestScope`),preDestroyCache=new Map;function preDestroyMethod(ctor){let method=preDestroyCache.get(ctor);return method===void 0&&(method=getClassMetaOrUndefined(METADATA.PRE_DESTROY,ctor.prototype)??null,preDestroyCache.set(ctor,method)),method}function disposeRequestStore(store){if(!store.disposed&&(store.disposed=!0,store.instances.size!==0)){for(let instance of store.instances.values()){if(typeof instance!=`object`||!instance)continue;let method=preDestroyMethod(instance.constructor);if(!(!method||typeof instance[method]!=`function`))try{let result=instance[method]();result&&typeof result.then==`function`&&result.catch(err=>log.error(err,`@PreDestroy on ${instance.constructor.name} rejected`))}catch(err){log.error(err,`@PreDestroy on ${instance.constructor.name} threw`)}}store.instances.clear()}}const REQUEST_SCOPE_MIDDLEWARE_MARKER=Symbol.for(`@kickjs/requestScopeMiddleware`);function createRequestStore(requestId){return{requestId:requestId||crypto.randomUUID(),instances:new Map,values:new Map}}function requestScopeMiddleware(){let mw=(req,res,next)=>{let requestIdHeader=req.headers[`x-request-id`],requestId=(Array.isArray(requestIdHeader)?requestIdHeader[0]:requestIdHeader)||crypto.randomUUID(),store=createRequestStore(requestId);req.requestId=requestId,res.once(`close`,()=>disposeRequestStore(store)),requestStore.run(store,next)};return mw[REQUEST_SCOPE_MIDDLEWARE_MARKER]=!0,mw}function isRequestScopeMiddleware(value){return typeof value==`function`&&value[REQUEST_SCOPE_MIDDLEWARE_MARKER]===!0}const REPLY_BRAND=Symbol.for(`kickjs.reply`);function reply(status,body){return{[REPLY_BRAND]:!0,status,body}}reply.created=body=>reply(201,body),reply.accepted=body=>reply(202,body),reply.noContent=()=>reply(204,void 0);function isReply(value){return typeof value==`object`&&!!value&&value[REPLY_BRAND]===!0}function applyHandlerResult(ctx,result){return result===void 0?!1:isReply(result)?result.status===204?(ctx.noContent(),!0):(ctx.json(result.body,result.status),!0):(ctx.json(result),!0)}export{disposeRequestStore as a,createRequestStore as i,isReply as n,isRequestScopeMiddleware as o,reply as r,requestScopeMiddleware as s,applyHandlerResult as t};
|
|
12
|
+
//# sourceMappingURL=reply-Bo950wuK.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reply-Y50qUtDy.mjs","names":[],"sources":["../src/http/middleware/request-scope.ts","../src/http/reply.ts"],"sourcesContent":["// Web Crypto via globalThis.crypto (randomUUID) — portable across\n// node/bun/deno/workers; node:crypto deliberately NOT imported here so this\n// module stays on the edge-safe request path.\nimport type { Request, Response, NextFunction } from 'express'\nimport { requestStore, type RequestStore } from '../request-store'\nimport { METADATA } from '../../core/interfaces'\nimport { getClassMetaOrUndefined } from '../../core/metadata'\nimport { createLogger } from '../../core/logger'\n\nconst log = createLogger('RequestScope')\n\n// Per-class @PreDestroy method-name cache: the metadata is immutable after\n// class definition, so pay the Reflect read once per class, not once per\n// instance per request. `null` = \"checked, none declared\".\nconst preDestroyCache = new Map<Function, string | symbol | null>()\n\nfunction preDestroyMethod(ctor: Function): string | symbol | null {\n let method = preDestroyCache.get(ctor)\n if (method === undefined) {\n method =\n getClassMetaOrUndefined<string | symbol>(METADATA.PRE_DESTROY, (ctor as any).prototype) ??\n null\n preDestroyCache.set(ctor, method)\n }\n return method\n}\n\n/**\n * Run `@PreDestroy` hooks on every REQUEST-scoped instance the request\n * created, then drop them. Called when the response closes (finished or\n * aborted) — the counterpart to `@PostConstruct`, closing the lifecycle gap\n * where per-request services holding transactions/handles/subscriptions were\n * silently dropped with no cleanup callback.\n *\n * Idempotent (guarded by `store.disposed`); hook errors are logged and\n * swallowed so one failing teardown can't break request completion. Async\n * hooks are fired without being awaited — the response is already gone.\n */\nexport function disposeRequestStore(store: RequestStore): void {\n if (store.disposed) return\n store.disposed = true\n if (store.instances.size === 0) return\n for (const instance of store.instances.values()) {\n if (instance === null || typeof instance !== 'object') continue\n const method = preDestroyMethod(instance.constructor)\n if (!method || typeof instance[method] !== 'function') continue\n try {\n const result = instance[method]()\n if (result && typeof result.then === 'function') {\n ;(result as Promise<void>).catch((err) =>\n log.error(err, `@PreDestroy on ${instance.constructor.name} rejected`),\n )\n }\n } catch (err) {\n log.error(err, `@PreDestroy on ${instance.constructor.name} threw`)\n }\n }\n store.instances.clear()\n}\n\n/**\n * Marker symbol stamped on the function returned by {@link requestScopeMiddleware}.\n *\n * `Application.setup()` uses {@link isRequestScopeMiddleware} to detect whether\n * the user-supplied middleware list already contains the request-scope wrapper,\n * and skips its automatic auto-mount when one is found. Lets adopters control\n * the exact position of the ALS frame in their pipeline (e.g., after a custom\n * tracing wrapper) without ending up with two nested frames.\n */\n// `Symbol.for(...)` returns a plain `symbol` (not `unique symbol`), but the\n// runtime detection marker doesn't need the unique nominal property — it only\n// needs the interned-slot reference equality that `Symbol.for` already provides.\nconst REQUEST_SCOPE_MIDDLEWARE_MARKER = Symbol.for('@kickjs/requestScopeMiddleware')\n\n/**\n * Build a fresh per-request store. Shared by {@link requestScopeMiddleware}\n * (Express) and by runtimes whose handler runs outside the connect-middleware\n * chain (e.g. Fastify), so both establish an identical ALS frame.\n */\nexport function createRequestStore(requestId?: string): RequestStore {\n return {\n requestId: requestId || crypto.randomUUID(),\n instances: new Map(),\n // Explicit generic locks the \"string keys only\" decision.\n values: new Map<string, unknown>(),\n }\n}\n\n/**\n * Wraps each request in an AsyncLocalStorage context.\n * Enables REQUEST-scoped DI and automatic requestId propagation to logs.\n *\n * Should be mounted early in the middleware pipeline (before route handlers).\n * `Application` mounts this automatically unless `contextStore: 'manual'` is set\n * or the user already includes one in their middleware list.\n */\nexport function requestScopeMiddleware() {\n const mw = (req: Request, res: Response, next: NextFunction) => {\n const requestIdHeader = req.headers['x-request-id']\n const requestId =\n (Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader) || crypto.randomUUID()\n const store = createRequestStore(requestId)\n // Surface the requestId on req so the standalone requestId() middleware\n // (when also mounted) reuses this value instead of generating a divergent\n // second one. Without this, log lines that read from the ALS store would\n // disagree with the X-Request-Id response header for any request that\n // didn't carry an inbound x-request-id header.\n ;(req as Request & { requestId?: string }).requestId = requestId\n // 'close' fires once the response finished OR the client aborted — run\n // @PreDestroy teardown for the request's scoped instances either way.\n res.once('close', () => disposeRequestStore(store))\n // Pass `next` directly — `run` invokes it with no args, so the arrow\n // wrapper previously allocated per request added nothing.\n requestStore.run(store, next)\n }\n ;(mw as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] = true\n return mw\n}\n\n/**\n * Returns `true` when `value` is the function produced by {@link requestScopeMiddleware}.\n *\n * Detection uses a `Symbol.for(...)` marker so the check survives different\n * copies of `@forinda/kickjs` loaded under different module identities (e.g.,\n * a published package alongside a workspace-linked one during local dev).\n */\nexport function isRequestScopeMiddleware(value: unknown): boolean {\n if (typeof value !== 'function') return false\n return (value as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] === true\n}\n","// Return-value handler support (response-inference-design.md §3.1).\n//\n// Handlers may RETURN their response payload instead of calling `ctx.json`:\n// the runtime auto-sends it when the pipeline finishes with nothing written.\n// `reply(status, body)` wraps a payload with a non-200 status while staying\n// transparent to type inference (`Reply<201, Task>` carries both statically).\n//\n// Edge-safe: no node imports — shared by all four runtimes including the\n// `@forinda/kickjs/web` entry.\n\nimport type { RequestContext } from './context'\n\n// `Symbol.for` so the brand survives duplicate module copies (workspace-linked\n// + published package in one process — same rationale as the request-scope\n// middleware marker).\nconst REPLY_BRAND = Symbol.for('kickjs.reply')\n\n/**\n * A status-carrying response payload for return-value handlers. Create via\n * {@link reply} — the brand is what runtimes detect, and the generics are\n * what the `kick/routes` response typegen unwraps (`Reply<S, T>` → `T`).\n */\nexport interface Reply<S extends number = number, T = unknown> {\n readonly [REPLY_BRAND]: true\n readonly status: S\n readonly body: T\n}\n\n/**\n * Wrap a return-value handler's payload with an explicit status code:\n *\n * ```ts\n * @Post('/')\n * async create(ctx: RequestContext) {\n * return reply(201, await this.tasks.create(ctx.body))\n * }\n * ```\n */\nexport function reply<S extends number, T>(status: S, body: T): Reply<S, T> {\n return { [REPLY_BRAND]: true, status, body }\n}\n\n/** `reply(201, body)` */\nreply.created = <T>(body: T): Reply<201, T> => reply(201, body)\n/** `reply(202, body)` */\nreply.accepted = <T>(body: T): Reply<202, T> => reply(202, body)\n/** Empty 204 — maps to `ctx.noContent()`. */\nreply.noContent = (): Reply<204, undefined> => reply(204, undefined)\n\n/**\n * The statically-inferred response type of a controller handler — what\n * `kick typegen` emits into `KickRoutes[...].response`:\n *\n * - `Awaited<ReturnType>` of the method\n * - `Reply<S, T>` unwraps to `T`\n * - `void`/`undefined` members are dropped (imperative `ctx.json` branches);\n * a handler that never returns a value stays `unknown`, matching the\n * pre-inference emission\n *\n * ```ts\n * type R = InferHandlerResponse<UsersController['get']> // → User\n * ```\n */\nexport type InferHandlerResponse<H> = H extends (...args: never[]) => infer R\n ? [Exclude<Awaited<R>, void | undefined>] extends [never]\n ? unknown\n : UnwrapReply<Exclude<Awaited<R>, void | undefined>>\n : unknown\n\ntype UnwrapReply<A> = A extends Reply<number, infer B> ? B : A\n\nexport function isReply(value: unknown): value is Reply {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[REPLY_BRAND] === true\n )\n}\n\n/**\n * Send a handler's RETURN value when the pipeline finished without anything\n * having written a response. Callers guard on \"driver/response unsettled\" —\n * this only decides how to send:\n *\n * - `undefined` → not handled (returns false; runtime keeps today's behavior)\n * - `Reply` wrapper → status + body (204 → `ctx.noContent()`)\n * - anything else → `ctx.json(value)` (200)\n *\n * Returns true when a response was produced.\n */\nexport function applyHandlerResult(ctx: RequestContext, result: unknown): boolean {\n if (result === undefined) return false\n if (isReply(result)) {\n // Only 204 maps to noContent — an undefined body must NOT coerce the\n // declared status (reply(304, undefined) keeps its 304; the json body\n // is simply empty).\n if (result.status === 204) {\n ctx.noContent()\n return true\n }\n ctx.json(result.body, result.status)\n return true\n }\n ctx.json(result)\n return true\n}\n"],"mappings":";;;;;;;;;;gNASA,MAAM,IAAM,aAAa,cAAc,EAKjC,gBAAkB,IAAI,IAE5B,SAAS,iBAAiB,KAAwC,CAChE,IAAI,OAAS,gBAAgB,IAAI,IAAI,EAOrC,OANI,SAAW,IAAA,KACb,OACE,wBAAyC,SAAS,YAAc,KAAa,SAAS,GACtF,KACF,gBAAgB,IAAI,KAAM,MAAM,GAE3B,MACT,CAaA,SAAgB,oBAAoB,MAA2B,CACzD,UAAM,WACV,MAAM,SAAW,GACb,MAAM,UAAU,OAAS,GAC7B,KAAK,IAAM,YAAY,MAAM,UAAU,OAAO,EAAG,CAC/C,GAAyB,OAAO,UAAa,WAAzC,SAAmD,SACvD,IAAM,OAAS,iBAAiB,SAAS,WAAW,EAChD,MAAC,QAAU,OAAO,SAAS,SAAY,YAC3C,GAAI,CACF,IAAM,OAAS,SAAS,OAAO,CAAC,EAC5B,QAAU,OAAO,OAAO,MAAS,YAClC,OAA0B,MAAO,KAChC,IAAI,MAAM,IAAK,kBAAkB,SAAS,YAAY,KAAK,UAAU,CACvE,CAEJ,OAAS,IAAK,CACZ,IAAI,MAAM,IAAK,kBAAkB,SAAS,YAAY,KAAK,OAAO,CACpE,CACF,CACA,MAAM,UAAU,MAAM,CADtB,CAEF,CAcA,MAAM,gCAAkC,OAAO,IAAI,gCAAgC,EAOnF,SAAgB,mBAAmB,UAAkC,CACnE,MAAO,CACL,UAAW,WAAa,OAAO,WAAW,EAC1C,UAAW,IAAI,IAEf,OAAQ,IAAI,GACd,CACF,CAUA,SAAgB,wBAAyB,CACvC,IAAM,IAAM,IAAc,IAAe,OAAuB,CAC9D,IAAM,gBAAkB,IAAI,QAAQ,gBAC9B,WACH,MAAM,QAAQ,eAAe,EAAI,gBAAgB,GAAK,kBAAoB,OAAO,WAAW,EACzF,MAAQ,mBAAmB,SAAS,EAMzC,IAA0C,UAAY,UAGvD,IAAI,KAAK,YAAe,oBAAoB,KAAK,CAAC,EAGlD,aAAa,IAAI,MAAO,IAAI,CAC9B,EAEA,MADC,IAA2C,iCAAmC,GACxE,EACT,CASA,SAAgB,yBAAyB,MAAyB,CAEhE,OADI,OAAO,OAAU,YACb,MAA6C,mCAAqC,EAC5F,CClHA,MAAM,YAAc,OAAO,IAAI,cAAc,EAuB7C,SAAgB,MAA2B,OAAW,KAAsB,CAC1E,MAAO,EAAG,aAAc,GAAM,OAAQ,IAAK,CAC7C,CAGA,MAAM,QAAc,MAA2B,MAAM,IAAK,IAAI,EAE9D,MAAM,SAAe,MAA2B,MAAM,IAAK,IAAI,EAE/D,MAAM,cAAyC,MAAM,IAAK,IAAA,EAAS,EAwBnE,SAAgB,QAAQ,MAAgC,CACtD,OACE,OAAO,OAAU,YACjB,OACC,MAAkC,eAAiB,EAExD,CAaA,SAAgB,mBAAmB,IAAqB,OAA0B,CAchF,OAbI,SAAW,IAAA,GAAkB,GAC7B,QAAQ,MAAM,EAIZ,OAAO,SAAW,KACpB,IAAI,UAAU,EACP,KAET,IAAI,KAAK,OAAO,KAAM,OAAO,MAAM,EAC5B,KAET,IAAI,KAAK,MAAM,EACR,GACT"}
|
|
1
|
+
{"version":3,"file":"reply-Bo950wuK.mjs","names":[],"sources":["../src/http/middleware/request-scope.ts","../src/http/reply.ts"],"sourcesContent":["// Web Crypto via globalThis.crypto (randomUUID) — portable across\n// node/bun/deno/workers; node:crypto deliberately NOT imported here so this\n// module stays on the edge-safe request path.\nimport type { Request, Response, NextFunction } from 'express'\nimport { requestStore, type RequestStore } from '../request-store'\nimport { METADATA } from '../../core/interfaces'\nimport { getClassMetaOrUndefined } from '../../core/metadata'\nimport { createLogger } from '../../core/logger'\n\nconst log = createLogger('RequestScope')\n\n// Per-class @PreDestroy method-name cache: the metadata is immutable after\n// class definition, so pay the Reflect read once per class, not once per\n// instance per request. `null` = \"checked, none declared\".\nconst preDestroyCache = new Map<Function, string | symbol | null>()\n\nfunction preDestroyMethod(ctor: Function): string | symbol | null {\n let method = preDestroyCache.get(ctor)\n if (method === undefined) {\n method =\n getClassMetaOrUndefined<string | symbol>(METADATA.PRE_DESTROY, (ctor as any).prototype) ??\n null\n preDestroyCache.set(ctor, method)\n }\n return method\n}\n\n/**\n * Run `@PreDestroy` hooks on every REQUEST-scoped instance the request\n * created, then drop them. Called when the response closes (finished or\n * aborted) — the counterpart to `@PostConstruct`, closing the lifecycle gap\n * where per-request services holding transactions/handles/subscriptions were\n * silently dropped with no cleanup callback.\n *\n * Idempotent (guarded by `store.disposed`); hook errors are logged and\n * swallowed so one failing teardown can't break request completion. Async\n * hooks are fired without being awaited — the response is already gone.\n */\nexport function disposeRequestStore(store: RequestStore): void {\n if (store.disposed) return\n store.disposed = true\n if (store.instances.size === 0) return\n for (const instance of store.instances.values()) {\n if (instance === null || typeof instance !== 'object') continue\n const method = preDestroyMethod(instance.constructor)\n if (!method || typeof instance[method] !== 'function') continue\n try {\n const result = instance[method]()\n if (result && typeof result.then === 'function') {\n ;(result as Promise<void>).catch((err) =>\n log.error(err, `@PreDestroy on ${instance.constructor.name} rejected`),\n )\n }\n } catch (err) {\n log.error(err, `@PreDestroy on ${instance.constructor.name} threw`)\n }\n }\n store.instances.clear()\n}\n\n/**\n * Marker symbol stamped on the function returned by {@link requestScopeMiddleware}.\n *\n * `Application.setup()` uses {@link isRequestScopeMiddleware} to detect whether\n * the user-supplied middleware list already contains the request-scope wrapper,\n * and skips its automatic auto-mount when one is found. Lets adopters control\n * the exact position of the ALS frame in their pipeline (e.g., after a custom\n * tracing wrapper) without ending up with two nested frames.\n */\n// `Symbol.for(...)` returns a plain `symbol` (not `unique symbol`), but the\n// runtime detection marker doesn't need the unique nominal property — it only\n// needs the interned-slot reference equality that `Symbol.for` already provides.\nconst REQUEST_SCOPE_MIDDLEWARE_MARKER = Symbol.for('@kickjs/requestScopeMiddleware')\n\n/**\n * Build a fresh per-request store. Shared by {@link requestScopeMiddleware}\n * (Express) and by runtimes whose handler runs outside the connect-middleware\n * chain (e.g. Fastify), so both establish an identical ALS frame.\n */\nexport function createRequestStore(requestId?: string): RequestStore {\n return {\n requestId: requestId || crypto.randomUUID(),\n instances: new Map(),\n // Explicit generic locks the \"string keys only\" decision.\n values: new Map<string, unknown>(),\n }\n}\n\n/**\n * Wraps each request in an AsyncLocalStorage context.\n * Enables REQUEST-scoped DI and automatic requestId propagation to logs.\n *\n * Should be mounted early in the middleware pipeline (before route handlers).\n * `Application` mounts this automatically unless `contextStore: 'manual'` is set\n * or the user already includes one in their middleware list.\n */\nexport function requestScopeMiddleware() {\n const mw = (req: Request, res: Response, next: NextFunction) => {\n const requestIdHeader = req.headers['x-request-id']\n const requestId =\n (Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader) || crypto.randomUUID()\n const store = createRequestStore(requestId)\n // Surface the requestId on req so the standalone requestId() middleware\n // (when also mounted) reuses this value instead of generating a divergent\n // second one. Without this, log lines that read from the ALS store would\n // disagree with the X-Request-Id response header for any request that\n // didn't carry an inbound x-request-id header.\n ;(req as Request & { requestId?: string }).requestId = requestId\n // 'close' fires once the response finished OR the client aborted — run\n // @PreDestroy teardown for the request's scoped instances either way.\n res.once('close', () => disposeRequestStore(store))\n // Pass `next` directly — `run` invokes it with no args, so the arrow\n // wrapper previously allocated per request added nothing.\n requestStore.run(store, next)\n }\n ;(mw as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] = true\n return mw\n}\n\n/**\n * Returns `true` when `value` is the function produced by {@link requestScopeMiddleware}.\n *\n * Detection uses a `Symbol.for(...)` marker so the check survives different\n * copies of `@forinda/kickjs` loaded under different module identities (e.g.,\n * a published package alongside a workspace-linked one during local dev).\n */\nexport function isRequestScopeMiddleware(value: unknown): boolean {\n if (typeof value !== 'function') return false\n return (value as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] === true\n}\n","// Return-value handler support (response-inference-design.md §3.1).\n//\n// Handlers may RETURN their response payload instead of calling `ctx.json`:\n// the runtime auto-sends it when the pipeline finishes with nothing written.\n// `reply(status, body)` wraps a payload with a non-200 status while staying\n// transparent to type inference (`Reply<201, Task>` carries both statically).\n//\n// Edge-safe: no node imports — shared by all four runtimes including the\n// `@forinda/kickjs/web` entry.\n\nimport type { RequestContext } from './context'\nimport type { RuntimeResponse } from './runtime'\n\n// `Symbol.for` so the brand survives duplicate module copies (workspace-linked\n// + published package in one process — same rationale as the request-scope\n// middleware marker).\nconst REPLY_BRAND = Symbol.for('kickjs.reply')\n\n/**\n * A status-carrying response payload for return-value handlers. Create via\n * {@link reply} — the brand is what runtimes detect, and the generics are\n * what the `kick/routes` response typegen unwraps (`Reply<S, T>` → `T`).\n */\nexport interface Reply<S extends number = number, T = unknown> {\n readonly [REPLY_BRAND]: true\n readonly status: S\n readonly body: T\n}\n\n/**\n * Wrap a return-value handler's payload with an explicit status code:\n *\n * ```ts\n * @Post('/')\n * async create(ctx: RequestContext) {\n * return reply(201, await this.tasks.create(ctx.body))\n * }\n * ```\n */\nexport function reply<S extends number, T>(status: S, body: T): Reply<S, T> {\n return { [REPLY_BRAND]: true, status, body }\n}\n\n/** `reply(201, body)` */\nreply.created = <T>(body: T): Reply<201, T> => reply(201, body)\n/** `reply(202, body)` */\nreply.accepted = <T>(body: T): Reply<202, T> => reply(202, body)\n/** Empty 204 — maps to `ctx.noContent()`. */\nreply.noContent = (): Reply<204, undefined> => reply(204, undefined)\n\n/**\n * The statically-inferred response type of a controller handler — what\n * `kick typegen` emits into `KickRoutes[...].response`:\n *\n * - `Awaited<ReturnType>` of the method\n * - `Reply<S, T>` unwraps to `T`\n * - `void`/`undefined` members are dropped (imperative `ctx.json` branches);\n * a handler that never returns a value stays `unknown`, matching the\n * pre-inference emission\n *\n * ```ts\n * type R = InferHandlerResponse<UsersController['get']> // → User\n * ```\n */\nexport type InferHandlerResponse<H> = H extends (...args: never[]) => infer R\n ? [Exclude<Awaited<R>, void | undefined>] extends [never]\n ? unknown\n : UnwrapReply<Exclude<Awaited<R>, void | undefined>>\n : unknown\n\ntype UnwrapReply<A> =\n A extends Reply<number, infer B>\n ? B\n : A extends RuntimeResponse\n ? // The engine's own response object says nothing about the body, so a\n // handler ending `return ctx.json(...)` degrades to `unknown` — which is\n // what the typegen comment always claimed happened. It did not: the bare\n // `RuntimeResponse` leaked into `KickRoutes[...].response`, offering\n // `.status()` / `.setHeader()` to a client expecting a payload.\n // Confidently wrong is worse than unknown.\n //\n // To carry a payload, return it (`return user`) or wrap it\n // (`return reply(201, user)`) — that is what `reply` is for.\n unknown\n : A\n\nexport function isReply(value: unknown): value is Reply {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[REPLY_BRAND] === true\n )\n}\n\n/**\n * Send a handler's RETURN value when the pipeline finished without anything\n * having written a response. Callers guard on \"driver/response unsettled\" —\n * this only decides how to send:\n *\n * - `undefined` → not handled (returns false; runtime keeps today's behavior)\n * - `Reply` wrapper → status + body (204 → `ctx.noContent()`)\n * - anything else → `ctx.json(value)` (200)\n *\n * Returns true when a response was produced.\n */\nexport function applyHandlerResult(ctx: RequestContext, result: unknown): boolean {\n if (result === undefined) return false\n if (isReply(result)) {\n // Only 204 maps to noContent — an undefined body must NOT coerce the\n // declared status (reply(304, undefined) keeps its 304; the json body\n // is simply empty).\n if (result.status === 204) {\n ctx.noContent()\n return true\n }\n ctx.json(result.body, result.status)\n return true\n }\n ctx.json(result)\n return true\n}\n"],"mappings":";;;;;;;;;;gNASA,MAAM,IAAM,aAAa,cAAc,EAKjC,gBAAkB,IAAI,IAE5B,SAAS,iBAAiB,KAAwC,CAChE,IAAI,OAAS,gBAAgB,IAAI,IAAI,EAOrC,OANI,SAAW,IAAA,KACb,OACE,wBAAyC,SAAS,YAAc,KAAa,SAAS,GACtF,KACF,gBAAgB,IAAI,KAAM,MAAM,GAE3B,MACT,CAaA,SAAgB,oBAAoB,MAA2B,CACzD,UAAM,WACV,MAAM,SAAW,GACb,MAAM,UAAU,OAAS,GAC7B,KAAK,IAAM,YAAY,MAAM,UAAU,OAAO,EAAG,CAC/C,GAAyB,OAAO,UAAa,WAAzC,SAAmD,SACvD,IAAM,OAAS,iBAAiB,SAAS,WAAW,EAChD,MAAC,QAAU,OAAO,SAAS,SAAY,YAC3C,GAAI,CACF,IAAM,OAAS,SAAS,OAAO,CAAC,EAC5B,QAAU,OAAO,OAAO,MAAS,YAClC,OAA0B,MAAO,KAChC,IAAI,MAAM,IAAK,kBAAkB,SAAS,YAAY,KAAK,UAAU,CACvE,CAEJ,OAAS,IAAK,CACZ,IAAI,MAAM,IAAK,kBAAkB,SAAS,YAAY,KAAK,OAAO,CACpE,CACF,CACA,MAAM,UAAU,MAAM,CADtB,CAEF,CAcA,MAAM,gCAAkC,OAAO,IAAI,gCAAgC,EAOnF,SAAgB,mBAAmB,UAAkC,CACnE,MAAO,CACL,UAAW,WAAa,OAAO,WAAW,EAC1C,UAAW,IAAI,IAEf,OAAQ,IAAI,GACd,CACF,CAUA,SAAgB,wBAAyB,CACvC,IAAM,IAAM,IAAc,IAAe,OAAuB,CAC9D,IAAM,gBAAkB,IAAI,QAAQ,gBAC9B,WACH,MAAM,QAAQ,eAAe,EAAI,gBAAgB,GAAK,kBAAoB,OAAO,WAAW,EACzF,MAAQ,mBAAmB,SAAS,EAMzC,IAA0C,UAAY,UAGvD,IAAI,KAAK,YAAe,oBAAoB,KAAK,CAAC,EAGlD,aAAa,IAAI,MAAO,IAAI,CAC9B,EAEA,MADC,IAA2C,iCAAmC,GACxE,EACT,CASA,SAAgB,yBAAyB,MAAyB,CAEhE,OADI,OAAO,OAAU,YACb,MAA6C,mCAAqC,EAC5F,CCjHA,MAAM,YAAc,OAAO,IAAI,cAAc,EAuB7C,SAAgB,MAA2B,OAAW,KAAsB,CAC1E,MAAO,EAAG,aAAc,GAAM,OAAQ,IAAK,CAC7C,CAGA,MAAM,QAAc,MAA2B,MAAM,IAAK,IAAI,EAE9D,MAAM,SAAe,MAA2B,MAAM,IAAK,IAAI,EAE/D,MAAM,cAAyC,MAAM,IAAK,IAAA,EAAS,EAsCnE,SAAgB,QAAQ,MAAgC,CACtD,OACE,OAAO,OAAU,YACjB,OACC,MAAkC,eAAiB,EAExD,CAaA,SAAgB,mBAAmB,IAAqB,OAA0B,CAchF,OAbI,SAAW,IAAA,GAAkB,GAC7B,QAAQ,MAAM,EAIZ,OAAO,SAAW,KACpB,IAAI,UAAU,EACP,KAET,IAAI,KAAK,OAAO,KAAM,OAAO,MAAM,EAC5B,KAET,IAAI,KAAK,MAAM,EACR,GACT"}
|