@forinda/kickjs 9.2.0 → 10.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-DjfRMob8.d.mts → adapter-ClEh-Oos.d.mts} +24 -2
- package/dist/adapter-ClEh-Oos.d.mts.map +1 -0
- package/dist/application-Dv514EeR.mjs +12 -0
- package/dist/application-Dv514EeR.mjs.map +1 -0
- package/dist/{application-CGxZCYl1.d.mts → application-oIPzgjfk.d.mts} +38 -11
- package/dist/application-oIPzgjfk.d.mts.map +1 -0
- package/dist/bootstrap-BdyBlBMe.mjs +12 -0
- package/dist/bootstrap-BdyBlBMe.mjs.map +1 -0
- package/dist/{client-ip-pcINkkPr.mjs → client-ip-BbLDp03S.mjs} +2 -2
- package/dist/{client-ip-pcINkkPr.mjs.map → client-ip-BbLDp03S.mjs.map} +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-DmGiAIuH.mjs → config-CJiGeOca.mjs} +3 -3
- package/dist/{config-DmGiAIuH.mjs.map → config-CJiGeOca.mjs.map} +1 -1
- package/dist/{container-CAPN7pBG.mjs → container-CS9v9UFU.mjs} +4 -4
- package/dist/{container-CAPN7pBG.mjs.map → container-CS9v9UFU.mjs.map} +1 -1
- package/dist/context-CtL9RxXp.mjs +12 -0
- package/dist/context-CtL9RxXp.mjs.map +1 -0
- package/dist/{context-errors-DYJjhalQ.mjs → context-errors-7C5TBYn7.mjs} +2 -2
- package/dist/{context-errors-DYJjhalQ.mjs.map → context-errors-7C5TBYn7.mjs.map} +1 -1
- package/dist/contributor-runner-DBpRR5dl.mjs +12 -0
- package/dist/{contributor-runner-vk9XlOED.mjs.map → contributor-runner-DBpRR5dl.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 +2 -2
- package/dist/core/cache.mjs.map +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/cron.mjs.map +1 -1
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/decorators.mjs.map +1 -1
- package/dist/core/errors.mjs +1 -1
- package/dist/core/interfaces.mjs +2 -2
- package/dist/core/interfaces.mjs.map +1 -1
- package/dist/core/logger.mjs +2 -2
- package/dist/core/logger.mjs.map +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 +2 -2
- package/dist/core/reactivity.mjs.map +1 -1
- package/dist/core-B0JmdBvX.mjs +12 -0
- package/dist/{core-DKrKVLfj.mjs.map → core-B0JmdBvX.mjs.map} +1 -1
- package/dist/{csrf-CxJItZVa.mjs → csrf--1t4qDov.mjs} +3 -3
- package/dist/{csrf-CxJItZVa.mjs.map → csrf--1t4qDov.mjs.map} +1 -1
- package/dist/{decorate-DfH1NIqV.mjs → decorate-BHkdkV4l.mjs} +1 -1
- package/dist/{define-adapter-C6lFhCL-.mjs → define-adapter-BLKlUbSL.mjs} +2 -2
- package/dist/{define-adapter-C6lFhCL-.mjs.map → define-adapter-BLKlUbSL.mjs.map} +1 -1
- package/dist/disposables-CuY2S3bw.mjs +12 -0
- package/dist/{disposables-NlW6Mibk.mjs.map → disposables-CuY2S3bw.mjs.map} +1 -1
- package/dist/env-BwzF9MkZ.mjs +13 -0
- package/dist/env-BwzF9MkZ.mjs.map +1 -0
- package/dist/{error-handler-itG65lj0.d.mts → error-handler-BB9wAElm.d.mts} +2 -2
- package/dist/{error-handler-itG65lj0.d.mts.map → error-handler-BB9wAElm.d.mts.map} +1 -1
- package/dist/handler-zKfIgNo4.mjs +13 -0
- package/dist/handler-zKfIgNo4.mjs.map +1 -0
- package/dist/http/application.d.mts +1 -1
- 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 +2 -2
- package/dist/http/middleware/cors.mjs.map +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/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.mjs +2 -2
- package/dist/http/middleware/session.d.mts +1 -1
- package/dist/http/middleware/session.mjs +2 -2
- package/dist/http/middleware/session.mjs.map +1 -1
- package/dist/http/middleware/spa.d.mts +1 -1
- 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 +2 -2
- package/dist/http/middleware/views.mjs.map +1 -1
- 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 +2 -1
- package/dist/http/runtimes/fastify.d.mts.map +1 -1
- package/dist/http/runtimes/fastify.mjs +3 -3
- package/dist/http/runtimes/fastify.mjs.map +1 -1
- package/dist/http/runtimes/h3-web.d.mts +2 -49
- package/dist/http/runtimes/h3-web.mjs +2 -3
- package/dist/http/runtimes/h3.d.mts +48 -18
- package/dist/http/runtimes/h3.d.mts.map +1 -1
- package/dist/http/runtimes/h3.mjs +2 -2
- package/dist/http/runtimes/h3.mjs.map +1 -1
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/jobs-BQ4t3TAb.mjs +12 -0
- package/dist/{jobs-BmpeFvi0.mjs.map → jobs-BQ4t3TAb.mjs.map} +1 -1
- package/dist/{kv-stores-BZTe23Tw.d.mts → kv-stores-Co5k2PYA.d.mts} +21 -6
- package/dist/kv-stores-Co5k2PYA.d.mts.map +1 -0
- package/dist/kv-stores-DsfVy2fk.mjs +12 -0
- package/dist/{kv-stores-DJwxntyw.mjs.map → kv-stores-DsfVy2fk.mjs.map} +1 -1
- package/dist/{metadata-CzzRUnAx.mjs → metadata-zvd6AWkE.mjs} +3 -3
- package/dist/{metadata-CzzRUnAx.mjs.map → metadata-zvd6AWkE.mjs.map} +1 -1
- package/dist/{module-registry-BnmRNy-3.mjs → module-registry-Daf_P2oD.mjs} +2 -2
- package/dist/{module-registry-BnmRNy-3.mjs.map → module-registry-Daf_P2oD.mjs.map} +1 -1
- package/dist/{observers-COlQfinD.mjs → observers-DZgs48UM.mjs} +2 -2
- package/dist/{observers-COlQfinD.mjs.map → observers-DZgs48UM.mjs.map} +1 -1
- package/dist/query-B_k9qn0R.mjs +12 -0
- package/dist/{query-B5DTq_4W.mjs.map → query-B_k9qn0R.mjs.map} +1 -1
- package/dist/reply-BDK9xt7Z.mjs +12 -0
- package/dist/{reply-CbTYIj_e.mjs.map → reply-BDK9xt7Z.mjs.map} +1 -1
- package/dist/{respond-wbMSqHYo.mjs → respond-DFRj2-7q.mjs} +3 -3
- package/dist/{respond-wbMSqHYo.mjs.map → respond-DFRj2-7q.mjs.map} +1 -1
- package/dist/route-flag-BWuvJsGZ.mjs +12 -0
- package/dist/{route-flag-Bb7n9Mnp.mjs.map → route-flag-BWuvJsGZ.mjs.map} +1 -1
- package/dist/route-policy-B3vM6bSf.mjs +12 -0
- package/dist/{route-policy-C_Wsc-b1.mjs.map → route-policy-B3vM6bSf.mjs.map} +1 -1
- package/dist/router-builder-JvMTp50k.mjs +12 -0
- package/dist/{router-builder-DawnRzlp.mjs.map → router-builder-JvMTp50k.mjs.map} +1 -1
- package/dist/{router-builder-CDkW88KQ.d.mts → router-builder-zWuVIOmd.d.mts} +2 -2
- package/dist/{router-builder-CDkW88KQ.d.mts.map → router-builder-zWuVIOmd.d.mts.map} +1 -1
- package/dist/{session-4Tb6q0Ny.d.mts → session-B4hOZj90.d.mts} +13 -4
- package/dist/session-B4hOZj90.d.mts.map +1 -0
- package/dist/{upload-Cy7qQy7M.d.mts → upload-CiMtdXWD.d.mts} +2 -2
- package/dist/upload-CiMtdXWD.d.mts.map +1 -0
- package/dist/{upload-config-D01Ty40f.mjs → upload-config-Dz2INCaa.mjs} +2 -2
- package/dist/{upload-config-D01Ty40f.mjs.map → upload-config-Dz2INCaa.mjs.map} +1 -1
- package/dist/web.d.mts +9 -7
- package/dist/web.d.mts.map +1 -1
- package/dist/web.mjs +2 -2
- package/dist/web.mjs.map +1 -1
- package/package.json +7 -8
- package/dist/adapter-DjfRMob8.d.mts.map +0 -1
- package/dist/application-BSJpaiSY.mjs +0 -12
- package/dist/application-BSJpaiSY.mjs.map +0 -1
- package/dist/application-CGxZCYl1.d.mts.map +0 -1
- package/dist/bootstrap-CbTF6xvv.mjs +0 -12
- package/dist/bootstrap-CbTF6xvv.mjs.map +0 -1
- package/dist/context-gpvcMlG5.mjs +0 -12
- package/dist/context-gpvcMlG5.mjs.map +0 -1
- package/dist/contributor-runner-vk9XlOED.mjs +0 -12
- package/dist/core-DKrKVLfj.mjs +0 -12
- package/dist/disposables-NlW6Mibk.mjs +0 -12
- package/dist/env-__fzPsHD.mjs +0 -13
- package/dist/env-__fzPsHD.mjs.map +0 -1
- package/dist/handler-DnZbcleS.mjs +0 -13
- package/dist/handler-DnZbcleS.mjs.map +0 -1
- package/dist/http/runtimes/h3-web.d.mts.map +0 -1
- package/dist/http/runtimes/h3-web.mjs.map +0 -1
- package/dist/jobs-BmpeFvi0.mjs +0 -12
- package/dist/kv-stores-BZTe23Tw.d.mts.map +0 -1
- package/dist/kv-stores-DJwxntyw.mjs +0 -12
- package/dist/query-B5DTq_4W.mjs +0 -12
- package/dist/reply-CbTYIj_e.mjs +0 -12
- package/dist/route-flag-Bb7n9Mnp.mjs +0 -12
- package/dist/route-policy-C_Wsc-b1.mjs +0 -12
- package/dist/router-builder-DawnRzlp.mjs +0 -12
- package/dist/session-4Tb6q0Ny.d.mts.map +0 -1
- package/dist/upload-Cy7qQy7M.d.mts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core-DKrKVLfj.mjs","names":[],"sources":["../src/core/context-decorator.ts","../src/core/asset-keys.ts","../src/core/assets.ts"],"sourcesContent":["import { assertFlagTest, type RouteFlagTest } from './route-flag'\nimport 'reflect-metadata'\nimport { METADATA, type Constructor, type MaybePromise } from './interfaces'\nimport { pushClassMeta, pushMethodMeta } from './metadata'\nimport type { InjectionToken } from './token'\nimport type { ContextMetaKey, ExecutionContext, MetaValue } from './execution-context'\n\n/**\n * String-literal union of every known context key — the union of the\n * keys declared on {@link ContextKeys} (key-only registry) and\n * {@link ContextMeta} (value-type registry). Resolves to `string` when\n * the project hasn't augmented either yet (so a first-day project keeps\n * compiling), and narrows to the concrete keys once the\n * `declare module '@forinda/kickjs' { interface ContextKeys/ContextMeta { ... } }`\n * blocks land.\n *\n * Used as the element type of `dependsOn`, so a typo like\n * `dependsOn: ['tenent']` becomes a TS error instead of a boot-time\n * `MissingContributorError`.\n *\n * Unioning BOTH registries is deliberate: it means adding a value type\n * via `ContextMeta` automatically makes that key a valid `dependsOn`\n * target, and you can register a dependsOn-able key in `ContextKeys`\n * without being forced to give it a value type. (Before the split,\n * `dependsOn` was keyed off `ContextMeta` alone, so augmenting\n * `ContextMeta` for some keys broke `dependsOn` for every key you\n * hadn't added there.)\n */\n// Moved to `execution-context` (beside the registries it reads) so that\n// module can constrain `get`/`set` without a circular import. Re-exported\n// here so existing `from '@forinda/kickjs'` imports keep resolving.\nexport type { ContextMetaKey } from './execution-context'\n\n/**\n * What a single `deps` entry is allowed to be — the runtime calls\n * `container.resolve(value)`, which accepts an injection token brand\n * or a constructor (decorated class). Anything else (a string literal,\n * an array, a plain object) silently fails at boot when the runner\n * tries to resolve it; constraining at the type level surfaces the\n * mistake in the editor instead.\n */\nexport type DepValue = InjectionToken<unknown> | Constructor<unknown>\n\n/**\n * Maps a `deps` declaration object to its resolved-instance shape.\n *\n * `deps: { db: DB_TOKEN, cache: CacheService }` produces\n * `{ db: Db, cache: CacheService }` — the runner resolves each value\n * against the DI container and hands the result to `resolve()`.\n */\nexport type ResolvedDeps<D extends Record<string, DepValue>> = {\n [K in keyof D]: D[K] extends InjectionToken<infer T>\n ? T\n : D[K] extends Constructor<infer T>\n ? T\n : never\n}\n\n/**\n * Keys of `T` that are declared required (i.e. not `?`-optional).\n */\ntype RequiredParamKeys<T> = {\n // eslint-disable-next-line @typescript-eslint/ban-types\n [K in keyof T]-?: {} extends Pick<T, K> ? never : K\n}[keyof T]\n\n/**\n * Params a call site MUST supply: the required keys of `P` that\n * `paramDefaults` (type `PD`) doesn't already provide.\n *\n * The `string extends keyof P` guard short-circuits the index-signature\n * case — `P` defaults to `Record<string, never>` for the overwhelmingly\n * common zero-params contributor, and a mapped type over `string` would\n * otherwise report \"every key is required\" and break `@Foo` for every\n * existing decorator in the wild.\n */\nexport type MissingParamKeys<P, PD> = string extends keyof P\n ? never\n : Exclude<RequiredParamKeys<P>, keyof PD>\n\n/**\n * The argument type for a call site (`@Foo({...})`, `.with({...})`).\n *\n * `Partial<P>` when every required field has a default, so adopters keep\n * overriding just the fields they care about. Otherwise `Partial<P>`\n * intersected with the undefaulted required fields, making those\n * mandatory at each call site.\n */\nexport type CallSiteParams<P, PD> = [MissingParamKeys<P, PD>] extends [never]\n ? Partial<P>\n : Partial<P> & Pick<P, MissingParamKeys<P, PD> & keyof P>\n\n/**\n * Spec passed to {@link defineContextDecorator}. Describes one entry in\n * the per-request contributor pipeline.\n *\n * @typeParam K - The {@link ContextMeta} key this contributor populates.\n * @typeParam D - DI dependency declaration shape (see {@link ResolvedDeps}).\n * @typeParam Ctx - Concrete execution-context type. Defaults to the abstract\n * {@link ExecutionContext}; HTTP contributors typically\n * default to `RequestContext` at the call site.\n * @typeParam PD - Inferred from `paramDefaults`. Drives which params each\n * call site is required to supply — see {@link CallSiteParams}.\n * Never spell this by hand.\n */\nexport interface ContextDecoratorSpec<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Partial<P>,\n> {\n /** ContextMeta key the resolved value is written to. */\n key: K\n /**\n * Skip this contributor on routes carrying the named route flag\n * (`defineRouteFlag`). See {@link ContributorRegistration.skipWhen}.\n */\n skipWhen?: RouteFlagTest\n /**\n * Run this contributor only on routes carrying the named route flag, or\n * matching the predicate.\n */\n onlyWhen?: RouteFlagTest\n /**\n * DI dependencies resolved by the runner before `resolve()` runs.\n * Map entry keys become property names on the resolved-deps argument\n * passed to `resolve()`.\n */\n deps?: D\n /**\n * Other contributor keys that must populate before this one runs.\n * Enforced via topo-sort at startup; missing deps and cycles fail boot.\n *\n * Typed against {@link ContextMetaKey} — the union of `keyof ContextMeta`\n * and `keyof ContextKeys` — once the project augments either, so\n * `dependsOn: ['tenent']` (typo) is a TS error, not a boot-time\n * `MissingContributorError`. Falls back to plain `string[]` when both\n * registries are empty (no augmentation yet) so first-day projects keep\n * compiling. Cast escape hatch: `dependsOn: ['custom-key' as ContextMetaKey]`.\n */\n dependsOn?: readonly ContextMetaKey[]\n /**\n * If `true`, a thrown `resolve` skips the contributor instead of\n * forwarding to the request error handler. The key is left unset\n * and downstream consumers see `ctx.get(key) === undefined`.\n */\n optional?: boolean\n /**\n * HTTP routes only: run this contributor before request validation and\n * `@Middleware()` handlers, instead of after them.\n *\n * For contributors that must answer before a request's shape is checked —\n * authentication above all. Without it, a request with no credentials and a\n * malformed body is rejected by validation (422) before the auth contributor\n * can reject it (401), telling an anonymous caller what the schema expects.\n *\n * `ctx.body` / `ctx.query` / `ctx.params` are **unvalidated** at this point,\n * and file uploads may not be parsed yet — read credentials (headers,\n * cookies), not the payload. It may only `dependsOn` contributors that also\n * set `beforeValidation`; anything else fails boot.\n */\n beforeValidation?: boolean\n /**\n * Default per-call params merged with the call-site params; call-site\n * wins. When omitted, params default to `{}` and the resolver\n * receives an empty object — back-compat with today's zero-arg\n * decorators.\n *\n * Param shape is intentionally unconstrained: plain data, functions,\n * and closures are all valid. Adopters who want runtime validation\n * should perform it inside `resolve()` — keeps the framework\n * validator-agnostic so projects can plug Zod / Valibot / hand-rolled\n * checks without the framework caring.\n *\n * **Required fields are enforced at the call site.** A required field\n * of `P` that has no entry here must be supplied wherever the\n * decorator is applied — `@Foo({ action: 'audit:read' })`. The bare\n * `@Foo` form and `.registration` stop type-checking for such a\n * decorator, because neither can supply the value.\n *\n * This exists so nobody has to invent a meaningless default just to\n * satisfy the type (`action: 'settings:read'` on a permission\n * contributor every call site overrides anyway). A default that is\n * never correct is worse than no default: forget the argument at one\n * call site and the route silently gates on the placeholder instead\n * of failing to compile.\n *\n * Omit this entirely for the common zero-params case — `P` defaults\n * to `Record<string, never>`, which requires nothing.\n */\n paramDefaults?: PD\n /**\n * Runtime mirror of the compile-time requirement above: param names\n * that must be present after merging call-site params over\n * `paramDefaults`. A missing one throws `TypeError` at the point of\n * use (decoration time for `@Foo`, call time for `@Foo({...})` /\n * `.with({...})`), naming the decorator and the field.\n *\n * The type-level check already covers TypeScript call sites, so this\n * is for the paths types can't reach: plain-JS adopters, `as any`\n * escapes, and params assembled dynamically. Optional — list only\n * fields whose absence is a correctness bug rather than a default.\n */\n requiredParams?: readonly (keyof P & string)[]\n /**\n * Hook invoked when `resolve()` throws and `optional` is `false`.\n * Returning a value writes it to `ctx.set(key, …)`; returning\n * `undefined` / `void` skips. Throwing inside the hook forwards\n * the new error to the request error handler.\n *\n * Async-permitted by design — adopters frequently want to\n * `await auditService.log(err)` or `await cache.fallback(...)`.\n *\n * The third `params` argument carries the resolved per-call params\n * (call-site overrides merged on top of `paramDefaults`).\n */\n onError?: (err: unknown, ctx: Ctx, params: P) => MaybePromise<MetaValue<K> | undefined | void>\n /**\n * Compute and return the value to write into `ctx.set(key, …)`.\n *\n * The third `params` argument carries the resolved per-call params\n * (call-site overrides merged on top of `paramDefaults`). When the\n * decorator is applied zero-arg (`@Foo`), `params` equals\n * `paramDefaults` (or `{}` if defaults are omitted). When applied as\n * a factory (`@Foo({ ... })`), the call-site object is merged on top.\n */\n resolve: (ctx: Ctx, deps: ResolvedDeps<D>, params: P) => MaybePromise<MetaValue<K>>\n}\n\n/**\n * Internal, normalised registration produced by {@link defineContextDecorator}.\n *\n * Frozen and shared by every use site of the decorator (method, class,\n * module, adapter, global). The pipeline builder dedupes by `key` using\n * the precedence rule defined in §20.4 of `architecture.md`.\n */\nexport interface ContributorRegistration<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n> {\n readonly key: K\n readonly deps: D\n readonly dependsOn: readonly ContextMetaKey[]\n readonly optional: boolean\n /**\n * Per-call params from the spec are baked into the closure at\n * decorator-construction time, so the runner-facing `onError`\n * exposes only `(err, ctx)`.\n */\n readonly onError?: (err: unknown, ctx: Ctx) => MaybePromise<MetaValue<K> | undefined | void>\n /**\n * Per-call params from the spec are baked into the closure at\n * decorator-construction time, so the runner-facing `resolve`\n * exposes only `(ctx, deps)`. Adopters who write their own\n * registrations (without `defineContextDecorator`) match this\n * shape directly — params are an implementation detail of the\n * decorator factory, not of the runtime pipeline.\n */\n readonly resolve: (ctx: Ctx, deps: ResolvedDeps<D>) => MaybePromise<MetaValue<K>>\n /**\n * Stack snapshot captured the moment the decorator was defined.\n * Used by the contributor-pipeline builder to point boot-time\n * errors (`MissingContributorError`, `ContributorCycleError`,\n * `DuplicateContributorError`) at the adopter file that declared\n * the broken contributor — instead of forcing them to grep for\n * the key string.\n *\n * Optional because adopters who hand-roll a `ContributorRegistration`\n * (without `defineContextDecorator`) may not have a useful stack to\n * attach. Format is the raw `Error.stack` string; consumers slice\n * the first non-framework frame for display.\n */\n readonly definedAt?: string\n /** Runs before request validation on HTTP routes — see {@link ContextDecoratorSpec.beforeValidation}. */\n readonly beforeValidation?: boolean\n /**\n * Skip this contributor on routes carrying the named route flag.\n *\n * The composable way to say \"not here\". Exempting a contributor otherwise\n * means registering a permissive twin under the same key, which only its\n * author can do — a flag is declared on the route, so an adopter can exempt\n * a plugin's contributor without owning its key or forking it.\n *\n * ```ts\n * defineHttpContextDecorator({ key: 'user', skipWhen: 'auth.public', resolve })\n * ```\n */\n readonly skipWhen?: RouteFlagTest\n /**\n * Run this contributor **only** on routes carrying the named flag. The\n * inverse of {@link skipWhen}, and the case with no expression today short\n * of an `if` inside every resolver.\n */\n readonly onlyWhen?: RouteFlagTest\n}\n\n/**\n * Type-erased registration shape used in the public collection slots\n * (`AppModule.contributors?()`, `AppAdapter.contributors?()`,\n * `KickPlugin.contributors?()`, `bootstrap({ contributors })`).\n *\n * The narrow `ContributorRegistration` carries a `Ctx` parameter so\n * `resolve(ctx, deps)` is typed at the *definition* site, but `Ctx`\n * sits in a contravariant position on `resolve` — meaning a\n * `ContributorRegistration<…, RequestContext>` is *not* assignable to\n * `ContributorRegistration<…, ExecutionContext>` even though every\n * `RequestContext` is-a `ExecutionContext`. That's TS-sound but\n * useless: the runner only ever calls a contributor with the same\n * concrete ctx the route mounts under, so an HTTP-typed contributor\n * never sees a non-HTTP ctx. Erasing `Ctx` to `any` in the collection\n * type lets adopters store contributors typed against `RequestContext`\n * (or any future transport-specific ctx) in the same array without\n * casting.\n */\nexport type AnyContributorRegistration = ContributorRegistration<\n ContextMetaKey,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any\n>\n\n/** Convenience alias for `AnyContributorRegistration[]` collections. */\nexport type ContributorRegistrations =\n | AnyContributorRegistration[]\n | readonly AnyContributorRegistration[]\n\n/**\n * The decorator function returned by either a zero-arg\n * {@link ContextDecorator} application or a factory call. Overloaded\n * to satisfy both class and method decorator shapes — adopters apply\n * the same decorator to either site without TS rejecting the call.\n */\nexport interface ContextDecoratorTarget {\n /** Class decorator usage. */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n (target: Function): void\n /** Method decorator usage. */\n (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void\n}\n\n/**\n * The value returned by {@link defineContextDecorator}.\n *\n * Resolves to one of two shapes depending on whether every required\n * field of `P` has a `paramDefaults` entry:\n *\n * - {@link ContextDecoratorWithDefaults} — the classic shape. Usable\n * bare (`@Foo`), as a factory (`@Foo({...})`), via `.with(params)`,\n * and via `.registration`.\n * - {@link ContextDecoratorRequiringParams} — when `P` has required\n * fields `paramDefaults` doesn't cover. Only the factory and\n * `.with(params)` forms exist, because the others have no way to\n * supply the missing values. Applying such a decorator bare is a\n * compile error rather than a silent `undefined` at request time.\n *\n * `PD` is inferred from the spec's `paramDefaults`; the default here\n * (`Partial<P>`) keeps hand-written `ContextDecorator<K, D, P>`\n * annotations resolving to the permissive shape as before.\n */\nexport type ContextDecorator<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD = Partial<P>,\n> = [MissingParamKeys<P, PD>] extends [never]\n ? ContextDecoratorWithDefaults<K, D, P, Ctx>\n : ContextDecoratorRequiringParams<K, D, P, Ctx, PD>\n\n/**\n * Decorator whose params are fully defaulted (or which takes none).\n * Callable as a class or method decorator directly, as a factory, or\n * via `.with(params)` / `.registration` at non-decorator registration\n * sites.\n */\nexport interface ContextDecoratorWithDefaults<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n> {\n /**\n * Class decorator usage with `paramDefaults` — `@Foo class C {}`.\n * The class constructor is a `Function`, so this overload only\n * matches actual classes (not plain object literals — which\n * fall through to the factory overload below).\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n (target: Function): void\n /**\n * Method decorator usage with `paramDefaults` — `@Foo` over a\n * method. The required `propertyKey` distinguishes this from the\n * factory overload's single-object-arg call shape.\n */\n (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void\n /**\n * Zero-arg factory usage — `@Foo()` returns a decorator with\n * `paramDefaults` applied. Equivalent in behaviour to the bare\n * `@Foo` form, exposed here so adopters who prefer \"always call the\n * factory\" don't need an `as any` cast.\n */\n (): ContextDecoratorTarget\n /**\n * Factory usage — `@Foo({ ... })` returns the actual decorator with\n * call-site params merged onto `paramDefaults`. Matches a single\n * params object; plain objects don't satisfy the `Function` /\n * 2-arg-minimum constraints above, so TS picks this overload.\n *\n * Accepts `Partial<P>` so adopters can override only the fields\n * they care about — anything missing falls back to `paramDefaults`\n * at runtime. Inside `resolve()`, `params` is the fully merged\n * `P`, never a `Partial<P>`.\n *\n * The returned decorator is overloaded for class AND method usage —\n * `@Foo({...}) class C {}` and `@Foo({...}) m() {}` both type-check.\n */\n (params: Partial<P>): ContextDecoratorTarget\n /**\n * Bare registration — uses `paramDefaults` only. Use this at non-\n * decorator registration sites (module / adapter / bootstrap) when\n * the call site doesn't need to override params.\n */\n readonly registration: ContributorRegistration<K, D, Ctx>\n /**\n * Build a registration with call-site params merged onto\n * `paramDefaults`. Use this at non-decorator registration sites\n * when the call site DOES want to override params:\n *\n * ```ts\n * bootstrap({\n * contributors: [LoadTenant.with({ source: 'subdomain' }).registration],\n * })\n * ```\n *\n * `Partial<P>` for the same reason as the factory overload — the\n * runtime merges with `paramDefaults`, so adopters only specify\n * fields they want to override.\n */\n with(params: Partial<P>): { readonly registration: ContributorRegistration<K, D, Ctx> }\n}\n\n/**\n * Decorator with at least one required param that `paramDefaults`\n * doesn't cover. Deliberately narrower than\n * {@link ContextDecoratorWithDefaults}: the bare `@Foo` form, the\n * zero-arg `@Foo()` form, and the `.registration` accessor are all\n * absent, because none of them can supply the missing value.\n *\n * ```ts\n * const OperatorPerm = defineContextDecorator.withParams<{ action: string }>()({\n * key: 'operatorPerm',\n * resolve: (ctx, _deps, { action }) => check(ctx, action),\n * })\n *\n * class Ops {\n * @OperatorPerm // ✗ compile error — `action` missing\n * @Get('/a')\n * a(ctx: RequestContext) {}\n *\n * @OperatorPerm({ action: 'read' }) // ✓\n * @Get('/b')\n * b(ctx: RequestContext) {}\n * }\n * ```\n *\n * If you want the bare form back, give `action` a default in\n * `paramDefaults` — but only when the default is genuinely correct for\n * an undecorated route, which for a permission string it usually isn't.\n */\nexport interface ContextDecoratorRequiringParams<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD = Partial<P>,\n> {\n /**\n * Factory usage — the only decorator form available here. The\n * argument type carries every required field `paramDefaults` didn't\n * provide, so omitting one is a compile error at this call site.\n */\n (params: CallSiteParams<P, PD>): ContextDecoratorTarget\n /**\n * Build a registration for non-decorator registration sites. Same\n * required-field rule as the factory form.\n */\n with(params: CallSiteParams<P, PD>): {\n readonly registration: ContributorRegistration<K, D, Ctx>\n }\n}\n\n/**\n * Curried params-first helper for {@link defineContextDecorator}.\n *\n * Solves the partial-inference problem: TS generics are positional, so\n * the moment an adopter wants to specify the per-call params shape\n * `P`, they're forced to spell `K` and `D` by hand — losing the\n * automatic `deps` inference that drives the `(ctx, deps, params) =>`\n * resolver signature:\n *\n * ```ts\n * // ❌ Positional generics — must repeat the deps type, and `D` no\n * // longer flows from `spec.deps` into `resolve(ctx, deps, ...)`.\n * defineContextDecorator<'tenant', { repo: typeof TENANT_REPO }, MyParams>({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO },\n * resolve: (ctx, { repo }, params) => ...,\n * })\n *\n * // ✅ Curried form — only `P` is spelled; `K`, `D`, `Ctx` infer.\n * defineContextDecorator.withParams<MyParams>()({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO }, // D inferred\n * resolve: (ctx, { repo }, params) => ..., // deps + params both typed\n * })\n * ```\n *\n * Use this whenever the contributor takes call-site params; use the\n * positional form when there are no params (the common case).\n */\nexport interface DefineContextDecoratorWithParams<P extends Record<string, unknown>> {\n <\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n // Inferred from `spec.paramDefaults`. The `Record<never, never>`\n // default (NOT `Partial<P>`) is what makes an omitted `paramDefaults`\n // mean \"nothing is defaulted\" — so every required field of `P` lands\n // on the call site. `Partial<P>` here would make `keyof PD` cover all\n // of `P` and silently defeat the whole check.\n PD extends Partial<P> = Record<never, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, Ctx, PD>,\n ): ContextDecorator<K, D, P, Ctx, PD>\n}\n\n/**\n * The public shape of {@link defineContextDecorator}. Combines the\n * original positional-generic call signature with the curried\n * `.withParams<P>()` helper that fixes partial-inference for the\n * parameterised case.\n */\nexport interface DefineContextDecoratorFn {\n <\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Record<never, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, Ctx, PD>,\n ): ContextDecorator<K, D, P, Ctx, PD>\n\n /**\n * Curried entry point. Spell only the per-call params shape `P`;\n * `K`, `D`, and `Ctx` are inferred from the spec passed to the\n * returned function. See {@link DefineContextDecoratorWithParams}\n * for the rationale.\n */\n withParams<P extends Record<string, unknown>>(): DefineContextDecoratorWithParams<P>\n}\n\n/**\n * Define a typed context contributor.\n *\n * The returned function works as a method or class decorator:\n *\n * ```ts\n * const LoadTenant = defineContextDecorator({\n * key: 'tenant',\n * deps: { tenants: TenantsRepo },\n * resolve: (ctx, { tenants }) => tenants.findByHost(ctx.requestId),\n * })\n *\n * @LoadTenant\n * @Get('/me')\n * me(ctx: RequestContext) {\n * ctx.get('tenant') // typed via ContextMeta\n * }\n * ```\n *\n * For non-decorator registration (module/adapter/bootstrap), pass\n * `LoadTenant.registration` into the appropriate contributors list.\n *\n * Boot-time validation: a missing `resolve`, an empty `key`, or a\n * non-array `dependsOn` throws `TypeError` here at definition time\n * (typically module-load) rather than waiting for the first request.\n * See `architecture.md` §20 for the full pipeline plan.\n *\n * **Parameterised contributors:** TS generics are positional, so to\n * specify `P` (the per-call params shape) with this signature you\n * also have to spell `K` and `D` — which loses automatic `deps`\n * inference. Use {@link DefineContextDecoratorFn.withParams} instead:\n * `defineContextDecorator.withParams<MyParams>()(spec)`.\n */\nfunction defineContextDecoratorImpl<\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Record<never, never>,\n>(spec: ContextDecoratorSpec<K, D, P, Ctx, PD>): ContextDecorator<K, D, P, Ctx, PD> {\n // ---- Boot-time validation --------------------------------------\n // Surface mistakes the moment `defineContextDecorator` is called\n // (typically module-load time) instead of letting them ride to\n // request time as cryptic ContextMeta misses or container failures.\n // Cheap, ~15 lines, catches a whole class of definition-time bugs.\n if (spec === null || typeof spec !== 'object') {\n throw new TypeError(\n 'defineContextDecorator: spec must be an object literal. ' +\n 'See architecture.md §20.4 for the spec shape.',\n )\n }\n if (typeof spec.key !== 'string' || spec.key.length === 0) {\n const got = typeof spec.key === 'string' ? '\"\"' : typeof spec.key\n throw new TypeError(\n `defineContextDecorator: spec.key must be a non-empty string (got ${got}). ` +\n 'Pick a name that matches a key declared on `ContextMeta` (or one of the framework keys).',\n )\n }\n if (typeof spec.resolve !== 'function') {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.resolve is required and must be a function ` +\n `(got ${typeof spec.resolve}). Each contributor needs a resolver that produces ` +\n `the value to write into ctx.set(${JSON.stringify(spec.key)}, …).`,\n )\n }\n if (spec.onError !== undefined && typeof spec.onError !== 'function') {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.onError must be a function when provided ` +\n `(got ${typeof spec.onError}).`,\n )\n }\n if (spec.dependsOn !== undefined && !Array.isArray(spec.dependsOn)) {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.dependsOn must be an array when provided ` +\n `(got ${typeof spec.dependsOn}).`,\n )\n }\n if (spec.requiredParams !== undefined && !Array.isArray(spec.requiredParams)) {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.requiredParams must be an array when provided ` +\n `(got ${typeof spec.requiredParams}).`,\n )\n }\n if (\n Array.isArray(spec.requiredParams) &&\n spec.requiredParams.some((p) => typeof p !== 'string' || p.length === 0)\n ) {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.requiredParams entries must be non-empty strings.`,\n )\n }\n if (\n Array.isArray(spec.dependsOn) &&\n spec.dependsOn.some((dep) => typeof dep !== 'string' || dep.length === 0)\n ) {\n // TS narrows dependsOn to `readonly ContextMetaKey[]` for typed\n // adopters, but JS callers (and TS callers who erase via `as any`)\n // can still slip through with `[42]` or `['']`. Surface it at\n // definition time as a TypeError instead of riding to first\n // request as a `MissingContributorError` on a phantom key.\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.dependsOn entries must be non-empty strings.`,\n )\n }\n\n // Source-location capture. `new Error().stack` here records the\n // adopter's call site so boot-time errors (cycles, missing deps,\n // duplicate keys) can point at the file that declared the broken\n // decorator. Bad boot errors are why frameworks get abandoned —\n // a single stack frame turns \"what's a 'tenent'?\" into \"edit\n // src/contributors/tenant.ts line 42\".\n const definedAt = new Error().stack\n\n // Snapshot + freeze paramDefaults so callers can't mutate the spec\n // object post-definition and shift the merged params under our feet.\n // Same immutability boundary applied to `deps` and `dependsOn`.\n const defaults = Object.freeze({\n ...(spec.paramDefaults ?? ({} as Partial<P>)),\n }) as Readonly<Partial<P>>\n\n // Shared deps + dependsOn — frozen once and reused across every\n // registration produced by this decorator (one per call site).\n // `Object.freeze` is shallow — the values inside (DI tokens,\n // constructors) are intentionally untouched; freezing them would\n // prevent the container from doing legitimate per-instance\n // bookkeeping.\n //\n // The `({} as D)` cast: `spec.deps` is optional (`deps?: D`), so\n // the fallback to `{}` is required. When `D` defaults to\n // `Record<string, never>`, the cast is sound (`{}` matches). When\n // `D` is non-empty AND adopter omitted `deps`, the cast is unsound\n // — but the runner errors loudly the moment it tries to resolve a\n // missing dep, so the mistake surfaces at first request rather\n // than producing wrong-but-silent behaviour. The alternative\n // (forcing `deps` non-optional in the spec) would break ergonomics\n // for the common zero-deps case.\n const sharedDeps = Object.freeze({ ...(spec.deps ?? ({} as D)) }) as D\n const sharedDependsOn = Object.freeze([...(spec.dependsOn ?? [])])\n const sharedOptional = spec.optional ?? false\n\n /**\n * Build a frozen registration that closes over a specific\n * resolved-params object. The runner sees a normal\n * `(ctx, deps) => value` registration — params are baked into the\n * closure, invisible to the pipeline.\n */\n const buildRegistration = (params: P): ContributorRegistration<K, D, Ctx> => {\n // Shallow-freeze the captured params so a misbehaving `resolve()` /\n // `onError()` can't mutate the closure and bleed into later\n // requests through the same registration. Adopters who really need\n // mutable per-request state should write to `ctx.set(...)` from\n // inside the resolver instead of mutating params.\n const frozenParams = Object.freeze({ ...params }) as P\n const onError: ContributorRegistration<K, D, Ctx>['onError'] = spec.onError\n ? (err: unknown, ctx: Ctx) => spec.onError!(err, ctx, frozenParams)\n : undefined\n const resolve: ContributorRegistration<K, D, Ctx>['resolve'] = (\n ctx: Ctx,\n deps: ResolvedDeps<D>,\n ) => spec.resolve(ctx, deps, frozenParams)\n if (spec.skipWhen !== undefined)\n assertFlagTest(spec.skipWhen, `contributor '${spec.key}' skipWhen`)\n if (spec.onlyWhen !== undefined)\n assertFlagTest(spec.onlyWhen, `contributor '${spec.key}' onlyWhen`)\n return Object.freeze({\n key: spec.key,\n deps: sharedDeps,\n dependsOn: sharedDependsOn,\n optional: sharedOptional,\n skipWhen: spec.skipWhen,\n onlyWhen: spec.onlyWhen,\n beforeValidation: spec.beforeValidation === true,\n onError,\n resolve,\n definedAt,\n })\n }\n\n // Default-params registration — used for zero-arg decorator\n // applications and for the `.registration` accessor. Cloning\n // `defaults` so the closure can't be mutated through the spec.\n const defaultRegistration = buildRegistration({ ...defaults } as P)\n\n /**\n * Decide whether the decorator was called as a decorator\n * (`@Foo` / `@Foo(target, key)`) or as a factory\n * (`@Foo()` / `@Foo({...})`).\n *\n * - Class decorator: `(target)` where target is a constructor function.\n * - Method decorator: `(target, propertyKey, descriptor?)` where\n * target is a prototype object and propertyKey is a string/symbol.\n * - Factory (defaults): `()` — adopter wrote `@Foo()` (rare; usually\n * they write the bare `@Foo`). Returns a decorator using\n * `paramDefaults`.\n * - Factory (params): `(params)` where params is a plain object.\n *\n * The legacy decorators that ship with KickJS (`@Get('/path')`,\n * `@Cron('* * * *')`, etc.) use the same heuristic, so adopters\n * already know to pass plain object literals as factory args.\n */\n const isDecoratorCall = (args: unknown[]): boolean => {\n // Zero args is a factory call (`@Foo()`), not a direct decorator\n // application — TS never invokes a decorator with zero args.\n if (args.length === 0) return false\n if (args.length >= 2) return true\n const first = args[0]\n // Functions are class constructors at decoration time.\n if (typeof first === 'function') return true\n // Anything else (plain object, undefined, primitive) → factory call.\n return false\n }\n\n const applyAsDecorator = (\n registration: ContributorRegistration<K, D, Ctx>,\n target: object,\n propertyKey?: string | symbol,\n ): void => {\n if (propertyKey === undefined) {\n // Class decorator — `target` is the constructor.\n pushClassMeta(METADATA.CLASS_CONTRIBUTORS, target, registration)\n } else {\n // Method decorator — `target` is the prototype; we write to its\n // constructor so reads can use the controller class as the\n // lookup key, matching the convention used by @Middleware and\n // consumed by router-builder.ts.\n const constructor = (target as { constructor: object }).constructor\n pushMethodMeta(METADATA.METHOD_CONTRIBUTORS, constructor, String(propertyKey), registration)\n }\n }\n\n /**\n * `true` for `{...}` and `Object.create(null)`. Rejects every\n * built-in subclass (Map / Set / Date / Error / RegExp / Promise),\n * adopter classes, and arrays.\n */\n const isPlainObject = (value: object): boolean => {\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n }\n\n /**\n * Merge call-site `Partial<P>` over `paramDefaults`. Strict guard:\n * accepts only **plain object literals** (or `Object.create(null)`).\n * Reject everything else with a descriptive `TypeError`.\n *\n * Why strict? Object spread copies enumerable own properties only.\n * `{ ...new Date() }` and `{ ...new Map([...])}` both produce `{}`\n * — the params silently drop. Catching this at the boundary\n * surfaces the mistake instead of routing the request with empty\n * config. Adopters who genuinely need a class-instance shape can\n * destructure it themselves: `Foo({ ...new MyParams() })`.\n */\n const mergeParams = (override: unknown): P => {\n if (override === undefined) return { ...defaults } as P\n if (\n override === null ||\n typeof override !== 'object' ||\n Array.isArray(override) ||\n !isPlainObject(override)\n ) {\n const got =\n override === null\n ? 'null'\n : Array.isArray(override)\n ? 'array'\n : typeof override === 'object'\n ? `instance of ${(override as object).constructor?.name ?? 'unknown class'}`\n : typeof override\n throw new TypeError(\n `defineContextDecorator(${spec.key}): factory call requires a plain object literal, got ${got}. ` +\n `Class instances and built-ins (Map, Date, etc) silently spread to empty objects — ` +\n `call the decorator with a spread object literal instead, e.g. ` +\n `\\`MyDecorator({ ...someInstance })\\`.`,\n )\n }\n return { ...defaults, ...(override as Partial<P>) } as P\n }\n\n const sharedRequiredParams = Object.freeze([...(spec.requiredParams ?? [])])\n\n /**\n * Runtime half of the required-params guarantee. The type system\n * already stops TS call sites from omitting a required field, so this\n * catches what types can't see: plain-JS adopters, `as any`, and\n * params built dynamically.\n *\n * `usage` names the form that failed so the message points at the fix\n * — a bare `@Foo` on a decorator with required params needs to become\n * `@Foo({ ... })`, which is a different edit from a factory call that\n * merely forgot one field.\n */\n const assertRequiredParams = (params: P, usage: string): void => {\n if (sharedRequiredParams.length === 0) return\n const missing = sharedRequiredParams.filter(\n (name) => (params as Record<string, unknown>)[name] === undefined,\n )\n if (missing.length === 0) return\n throw new TypeError(\n `defineContextDecorator(${spec.key}): ${usage} is missing required param(s) ` +\n `${missing.map((m) => `'${m}'`).join(', ')}. ` +\n `Supply them at the call site — e.g. \\`@${spec.key}({ ${missing[0]}: … })\\` — or ` +\n `add a default in \\`paramDefaults\\` if one is genuinely correct for every route.`,\n )\n }\n\n // Overloaded function signatures so `decoratorOrFactory`'s type\n // matches `ContextDecorator`'s call shapes directly — no\n // `as unknown as` double-cast needed. The implementation signature\n // (`...args: unknown[]`) is hidden from callers; only the four\n // declared overloads are reachable.\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n function decoratorOrFactory(target: Function): void\n function decoratorOrFactory(\n target: object,\n propertyKey: string | symbol,\n descriptor?: PropertyDescriptor,\n ): void\n function decoratorOrFactory(): ContextDecoratorTarget\n function decoratorOrFactory(params: Partial<P>): ContextDecoratorTarget\n function decoratorOrFactory(...args: unknown[]): unknown {\n if (isDecoratorCall(args)) {\n const [target, propertyKey] = args as [\n object,\n string | symbol | undefined,\n PropertyDescriptor?,\n ]\n // Bare `@Foo` — nothing can supply params here, so a required one\n // that isn't defaulted is a decoration-time (module-load) failure\n // rather than an undefined riding into the resolver.\n assertRequiredParams({ ...defaults } as P, 'bare `@decorator` usage')\n applyAsDecorator(defaultRegistration, target, propertyKey)\n return undefined\n }\n // Factory call — capture merged params and return a decorator.\n const params = mergeParams(args[0])\n assertRequiredParams(params, 'factory call')\n const registration = buildRegistration(params)\n return (target: object, propertyKey?: string | symbol) => {\n applyAsDecorator(registration, target, propertyKey)\n }\n }\n\n // Set a meaningful `.name` so `console.log(LoadTenant)` and stack\n // traces show `[Function: ContextDecorator(tenant)]` instead of\n // `[Function: decoratorOrFactory]`. Function.name is configurable\n // by default, so this works on the existing function reference.\n Object.defineProperty(decoratorOrFactory, 'name', {\n value: `ContextDecorator(${spec.key})`,\n writable: false,\n enumerable: false,\n configurable: true,\n })\n\n // `Object.assign` returns the intersection type, so the decorator\n // gets `.registration` + `.with` properties typed without the\n // previous `as unknown as ContextDecorator<...>` double cast.\n // Properties go on via assign (writable + enumerable defaults),\n // then `Object.freeze` locks the shape so adopters can't mutate\n // `decorator.registration = …` post-construction.\n const decorator = Object.assign(decoratorOrFactory, {\n with: (params: Partial<P>) => {\n const merged = mergeParams(params)\n assertRequiredParams(merged, '`.with()`')\n return { registration: buildRegistration(merged) }\n },\n })\n\n // `.registration` is a getter, not a plain property, so that reading it\n // on a decorator with undefaulted required params throws instead of\n // handing back a registration whose resolver will see `undefined`.\n // (`Object.assign` would have copied the value and lost the check.)\n Object.defineProperty(decorator, 'registration', {\n get() {\n assertRequiredParams({ ...defaults } as P, '`.registration`')\n return defaultRegistration\n },\n enumerable: true,\n configurable: false,\n })\n\n // `ContextDecorator` is a conditional type (permissive vs required-params\n // shape), and TS can't evaluate a conditional whose input generics are\n // still unresolved — so the assignment can't be checked structurally\n // here regardless of how `decorator` is built. The cast is the price of\n // the call-site guarantee; the two branches differ only in which call\n // forms they expose, and every one of them is implemented above.\n return Object.freeze(decorator) as unknown as ContextDecorator<K, D, P, Ctx, PD>\n}\n\n/**\n * Public export — the positional-generic implementation plus the\n * curried `.withParams<P>()` helper attached as a method. Adopters\n * pick whichever form fits the call site:\n *\n * - **Positional** — `defineContextDecorator(spec)` when there are\n * no per-call params (the common case). `K`, `D`, `Ctx` infer.\n * - **Curried** — `defineContextDecorator.withParams<P>()(spec)`\n * when the contributor takes params; spell only `P`, the rest\n * still infer.\n *\n * Frozen so adopters can't reassign `.withParams` at runtime.\n */\nexport const defineContextDecorator: DefineContextDecoratorFn = Object.freeze(\n Object.assign(defineContextDecoratorImpl, {\n withParams: <P extends Record<string, unknown>>(): DefineContextDecoratorWithParams<P> => {\n return <\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Record<never, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, Ctx, PD>,\n ): ContextDecorator<K, D, P, Ctx, PD> => defineContextDecoratorImpl<K, D, P, Ctx, PD>(spec)\n },\n }),\n) as DefineContextDecoratorFn\n","// Shared asset-key strategy — used by the build pipeline, the\n// runtime dev resolver, and the typegen emitter so all three layers\n// agree on what the manifest looks like.\n//\n// Three strategies:\n//\n// - `'strip'` — drop the final extension. `pages/index.pug` →\n// `pages/index`. Best for namespaces where every basename is\n// unique (mail templates, pdf invoices). Shortest autocomplete.\n//\n// - `'with-extension'` — keep the extension. `pages/index.pug` →\n// `pages/index.pug`. Required when extension siblings coexist.\n//\n// - `'auto'` (default) — strip when safe, keep extensions when not.\n// The grouping pass collects files by stripped basename; groups\n// of one keep the stripped key, groups of more than one keep the\n// full path. Backward compatible for projects with no\n// extension-siblings; lossless for projects that have them.\n//\n// The function returns the path / key pairs in walk order (input\n// order preserved) so callers can iterate without re-sorting. Sort\n// in the caller if a stable manifest order is needed.\n\nexport type AssetKeyStrategy = 'auto' | 'strip' | 'with-extension'\n\nexport interface AssetKeyPair {\n /** Source-relative path the file was discovered at. */\n rel: string\n /** Final logical key for the manifest, including the namespace prefix. */\n key: string\n}\n\nexport interface GroupAssetKeysOptions {\n /**\n * Strategy applied to this namespace. Default `'auto'`.\n *\n * Adopters override per-namespace via `assetMap.<ns>.keys` in\n * kick.config.ts when they want uniform behaviour (e.g. always\n * include the extension regardless of collisions).\n */\n strategy?: AssetKeyStrategy\n}\n\nexport interface GroupAssetKeysResult {\n pairs: AssetKeyPair[]\n /**\n * Number of collision groups resolved by keeping extensions.\n * Surfaces as an informational log (\"auto-resolved N collisions\")\n * so adopters know whether they're relying on auto-mode or not.\n * Always 0 for `'strip'` (overwrites) and `'with-extension'`\n * (extensions always preserved, no collision possible).\n */\n collisionGroupsResolved: number\n}\n\n/**\n * Compute final manifest keys for a namespace's discovered paths,\n * given the chosen strategy. Pure function — no fs / config / globals\n * involved.\n *\n * @param namespace assetMap key — e.g. `'mails'`, `'pages'`.\n * @param paths source-relative file paths (POSIX separators) discovered\n * under the namespace's `src` directory.\n * @param opts grouping options.\n */\nexport function groupAssetKeys(\n namespace: string,\n paths: readonly string[],\n opts: GroupAssetKeysOptions = {},\n): GroupAssetKeysResult {\n const strategy = opts.strategy ?? 'auto'\n\n if (strategy === 'with-extension') {\n return {\n pairs: paths.map((rel) => ({ rel, key: `${namespace}/${rel}` })),\n collisionGroupsResolved: 0,\n }\n }\n\n if (strategy === 'strip') {\n // Strict strip — last-walk-order wins on collision; no\n // collision tracking here because the docs document this as the\n // adopter's choice. The caller (build / runtime / typegen) can\n // still warn if it wants; this function just shapes keys.\n return {\n pairs: paths.map((rel) => ({ rel, key: `${namespace}/${stripExt(rel)}` })),\n collisionGroupsResolved: 0,\n }\n }\n\n // strategy === 'auto' — group by stripped basename, keep\n // extensions only on collision groups.\n const groups = new Map<string, string[]>()\n for (const rel of paths) {\n const strippedKey = `${namespace}/${stripExt(rel)}`\n const arr = groups.get(strippedKey)\n if (arr) {\n arr.push(rel)\n } else {\n groups.set(strippedKey, [rel])\n }\n }\n\n let collisionGroupsResolved = 0\n // Build a per-rel decision table so we preserve input ordering in\n // the output (manifest writers rely on stable ordering).\n const keyByRel = new Map<string, string>()\n for (const [strippedKey, rels] of groups) {\n if (rels.length === 1) {\n keyByRel.set(rels[0], strippedKey)\n } else {\n collisionGroupsResolved += 1\n for (const rel of rels) {\n keyByRel.set(rel, `${namespace}/${rel}`)\n }\n }\n }\n\n return {\n pairs: paths.map((rel) => ({\n rel,\n key: keyByRel.get(rel) ?? `${namespace}/${rel}`,\n })),\n collisionGroupsResolved,\n }\n}\n\n/** Strip the final extension from a path. `'mails/welcome.ejs'` → `'mails/welcome'`. */\nexport function stripExt(path: string): string {\n // Inline the logic instead of importing extname so this module\n // stays usable in pure-browser contexts. Same algorithm: find the\n // last dot AFTER the last separator, slice up to it.\n const lastSlash = path.lastIndexOf('/')\n const filename = lastSlash >= 0 ? path.slice(lastSlash + 1) : path\n const lastDot = filename.lastIndexOf('.')\n if (lastDot <= 0) return path // no ext, or hidden file like `.env`\n const dirPart = lastSlash >= 0 ? path.slice(0, lastSlash + 1) : ''\n return dirPart + filename.slice(0, lastDot)\n}\n","/**\n * Asset manager runtime.\n *\n * Three accessor surfaces over one resolver engine:\n *\n * - `assets.x.y()` — Proxy ambient, autocomplete-friendly default.\n * - `useAssets()` — hook returning the same Proxy; mockable per-call.\n * - `resolveAsset(ns, k)` — string resolver for dynamic dispatch.\n *\n * Resolution path:\n *\n * 1. Load the build-time manifest (`<outDir>/.kickjs-assets.json`)\n * when present. Manifest existence = \"we're running from a built\n * dist\". Fast path; mapped paths returned verbatim.\n * 2. Otherwise (dev / test) walk the assetMap src directories from\n * `kick.config.{json,js,cjs}` and synthesise the same manifest\n * shape in memory. First call pays the walk cost; subsequent\n * calls hit the cache.\n * 3. `KICK_ASSETS_ROOT` env var overrides everything — the resolver\n * treats the path as the manifest directory and skips discovery.\n *\n * @module @forinda/kickjs/core/assets\n */\n\nimport { existsSync, readFileSync, readdirSync } from 'node:fs'\nimport { isAbsolute, join, resolve, sep } from 'node:path'\nimport { createToken, type InjectionToken } from './token'\nimport { groupAssetKeys } from './asset-keys'\n\nexport const ASSET_MANIFEST_VERSION = 1 as const\n\ninterface AssetManifest {\n version: typeof ASSET_MANIFEST_VERSION\n entries: Record<string, string>\n}\n\ninterface ResolvedManifest {\n manifest: AssetManifest\n root: string\n}\n\n/**\n * Augmentable interface — `kick typegen` emits\n * a `declare global` augmentation listing every namespace + key for\n * the project's `assetMap`. Adopters who haven't run typegen yet\n * still get a runtime-correct Proxy; only typed autocomplete waits\n * on the augmentation.\n */\nexport interface KickAssets {}\n\n/**\n * DI token for the `assets` Proxy. Adopter code that wants to\n * receive assets via constructor injection registers via\n * `container.registerInstance(ASSETS, useAssets())` in `bootstrap()`\n * and injects with `@Inject(ASSETS)`.\n */\nexport const ASSETS: InjectionToken<KickAssets> = createToken<KickAssets>('kick/assets/Map')\n\nexport class UnknownAssetError extends Error {\n readonly namespace: string\n readonly key: string\n constructor(namespace: string, key: string) {\n super(\n `Unknown asset '${namespace}/${key}'. ` +\n `Make sure the namespace exists in kick.config.ts assetMap and that the file is present in the source directory.`,\n )\n this.name = 'UnknownAssetError'\n this.namespace = namespace\n this.key = key\n }\n}\n\nlet manifestCache: ResolvedManifest | null | undefined = undefined\n\n/**\n * Convert a filesystem path to its posix (forward-slash) form. The\n * asset manifest and the public `resolveAsset` / `assets.x.y()` /\n * `useAssets()` contract returns posix paths on every platform so\n * adopters can:\n *\n * - pass the result to `express.static` / `res.sendFile` / `ejs.renderFile`\n * without worrying about Windows vs. posix separators\n * - splice the path into URLs (`href`, `src`, CDN keys) without\n * replacing `\\` with `/` by hand\n * - compare two paths from `resolveAsset` for equality on any host\n *\n * Node's `fs.*` and Express's path-handling APIs accept either\n * separator on Windows, so this normalisation is safe — the only\n * adopter code it could break is something explicitly parsing\n * Windows backslashes back out of the result, which would already be\n * brittle.\n */\nfunction toPosixPath(p: string): string {\n return p.replaceAll('\\\\', '/')\n}\n\nexport function resolveAsset(namespace: string, key: string): string {\n const resolved = loadManifest()\n if (!resolved) {\n throw new UnknownAssetError(namespace, key)\n }\n const entry = resolved.manifest.entries[`${namespace}/${key}`]\n if (!entry) {\n throw new UnknownAssetError(namespace, key)\n }\n const abs = isAbsolute(entry) ? entry : resolve(resolved.root, entry)\n return toPosixPath(abs)\n}\n\n/** Reset the manifest cache. Tests use this; production code shouldn't need it. */\nexport function clearAssetCache(): void {\n manifestCache = undefined\n}\n\nfunction loadManifest(): ResolvedManifest | null {\n // In development we deliberately skip the cache so adding or removing\n // a template file (e.g. dropping `welcome-back.ejs` into\n // `src/templates/mails/`) takes effect without a server restart. The\n // synthesise-dev-manifest path walks ~10 files; the cost is negligible\n // compared to the user-visible surprise of \"I added a file but\n // assets.x.y() throws\". Production keeps the cache because the built\n // manifest under `dist/.kickjs-assets.json` doesn't change at runtime.\n if (process.env.NODE_ENV !== 'production') {\n return discoverManifest()\n }\n if (manifestCache !== undefined) return manifestCache\n manifestCache = discoverManifest()\n return manifestCache\n}\n\nfunction discoverManifest(): ResolvedManifest | null {\n // `KICK_ASSETS_ROOT` is an explicit, deliberate override — it wins in\n // both dev and prod.\n const envRoot = process.env.KICK_ASSETS_ROOT\n if (envRoot) {\n const fromEnv = readBuiltManifest(envRoot)\n if (fromEnv) return fromEnv\n }\n\n const cwd = process.cwd()\n const config = loadConfigSync(cwd)\n\n // In dev, prefer a live walk of the source tree over any built\n // manifest left on disk by an earlier `kick build`. Otherwise a stale\n // `dist/.kickjs-assets.json` shadows the live `src/` tree: a freshly\n // added template file would resolve to the old map (or throw\n // UnknownAssetError) until the next rebuild — exactly the surprise the\n // cache-skip in `loadManifest` exists to avoid. `synthesiseDevManifest`\n // returns null only when there's no `assetMap` config at all, in which\n // case we fall through to the built-manifest probes below.\n if (process.env.NODE_ENV !== 'production') {\n const fresh = synthesiseDevManifest(cwd, config)\n if (fresh) return fresh\n }\n\n // Honour `kick.config.build.outDir` BEFORE the standard probe list\n // so adopters with non-default Vite outputs (e.g. `output/`,\n // `target/`, anything else) don't have to set KICK_ASSETS_ROOT\n // every time. Cached config load — same loader the dev fallback\n // uses. Skip when outDir matches one of the standard candidates;\n // it'd be probed redundantly otherwise.\n const configOutDir = config?.build?.outDir\n if (configOutDir && !(STANDARD_OUT_DIRS as readonly string[]).includes(configOutDir)) {\n const found = readBuiltManifest(resolve(cwd, configOutDir))\n if (found) return found\n }\n\n for (const candidate of STANDARD_OUT_DIRS) {\n const found = readBuiltManifest(join(cwd, candidate))\n if (found) return found\n }\n\n return synthesiseDevManifest(cwd, config)\n}\n\nconst STANDARD_OUT_DIRS = ['dist', 'build', 'out'] as const\n\nfunction readBuiltManifest(dir: string): ResolvedManifest | null {\n const path = join(dir, '.kickjs-assets.json')\n if (!existsSync(path)) return null\n try {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as Partial<AssetManifest>\n if (parsed.version !== ASSET_MANIFEST_VERSION) return null\n if (!parsed.entries || typeof parsed.entries !== 'object') return null\n return { manifest: parsed as AssetManifest, root: resolve(dir) }\n } catch {\n return null\n }\n}\n\ninterface AssetMapConfigShape {\n assetMap?: Record<\n string,\n {\n src: string\n dest?: string\n glob?: string\n keys?: 'auto' | 'strip' | 'with-extension'\n }\n >\n build?: { outDir?: string }\n}\n\nfunction synthesiseDevManifest(\n cwd: string,\n preloadedConfig?: AssetMapConfigShape | null,\n): ResolvedManifest | null {\n const config = preloadedConfig === undefined ? loadConfigSync(cwd) : preloadedConfig\n if (!config?.assetMap) return null\n\n const entries: Record<string, string> = {}\n // Walk order is filesystem-dependent on Linux/macOS, so we collect\n // every match per-namespace first, then sort + feed `groupAssetKeys`\n // for stable manifest output. Build pipeline does the same — both\n // layers agree on the resulting key set.\n for (const [namespace, entry] of Object.entries(config.assetMap)) {\n if (!entry || typeof entry.src !== 'string') continue\n const srcAbs = resolve(cwd, entry.src)\n if (!existsSync(srcAbs)) continue\n\n const collected: Array<{ rel: string; abs: string }> = []\n walkSync(srcAbs, srcAbs, (relPath, absPath) => {\n if (entry.glob && !matchesGlobLite(relPath, entry.glob, namespace)) return\n collected.push({ rel: relPath, abs: absPath })\n })\n collected.sort((a, b) => a.rel.localeCompare(b.rel))\n\n const { pairs, collisionGroupsResolved } = groupAssetKeys(\n namespace,\n collected.map((c) => c.rel),\n { strategy: entry.keys ?? 'auto' },\n )\n const absByRel = new Map(collected.map((c) => [c.rel, c.abs]))\n for (const { rel, key } of pairs) {\n const abs = absByRel.get(rel)\n if (abs) entries[key] = abs\n }\n if (collisionGroupsResolved > 0 && process.env.NODE_ENV !== 'production') {\n // Dev-only nudge — production runs read the build manifest where\n // the collision was already resolved at build time.\n console.log(\n `[kickjs/assets] '${namespace}': ${collisionGroupsResolved} basename collision(s) auto-resolved by keeping extensions ` +\n `(set assetMap.${namespace}.keys to 'strip' or 'with-extension' to override).`,\n )\n }\n }\n\n return { manifest: { version: ASSET_MANIFEST_VERSION, entries }, root: cwd }\n}\n\nconst warnedGlobs = new Set<string>()\n\n/**\n * Tiny glob matcher used only by dev manifest synthesis. Covers the\n * common assetMap glob patterns (`**\\/*`, `**\\/*.ext`,\n * `**\\/*.{a,b}`). Anything exotic prints a one-time warning + falls\n * through to `**\\/*` semantics rather than silently dropping files.\n *\n * The build pipeline (`packages/cli/src/asset-manager/build.ts`) uses\n * the full `glob` package; adopters who hit the dev-mode warning\n * should run `kick build:assets` to produce a real manifest.\n */\nfunction matchesGlobLite(relPath: string, pattern: string, namespace: string): boolean {\n if (pattern === '**/*' || pattern === '**') return true\n const singleExt = /^\\*\\*\\/\\*\\.(\\w+)$/.exec(pattern)\n if (singleExt) return relPath.endsWith(`.${singleExt[1]}`)\n const braceExt = /^\\*\\*\\/\\*\\.\\{([^}]+)\\}$/.exec(pattern)\n if (braceExt) {\n const exts = braceExt[1].split(',').map((e) => e.trim())\n return exts.some((ext) => relPath.endsWith(`.${ext}`))\n }\n const tag = `${namespace}::${pattern}`\n if (!warnedGlobs.has(tag)) {\n warnedGlobs.add(tag)\n console.warn(\n `[kickjs/assets] Dev-mode glob '${pattern}' on '${namespace}' isn't recognised by the lite matcher. ` +\n `Falling back to '**/*'. Run 'kick build:assets' to use the full glob engine.`,\n )\n }\n return true\n}\n\nfunction walkSync(root: string, dir: string, onFile: (rel: string, abs: string) => void): void {\n let entries\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n walkSync(root, full, onFile)\n } else if (entry.isFile()) {\n const rel = full\n .slice(root.length + 1)\n .split(sep)\n .join('/')\n onFile(rel, full)\n }\n }\n}\n\n/**\n * Sync `kick.config.{json,cjs,js}` loader for dev manifest\n * synthesis. Avoids dynamic `import()` (would force every assets\n * lookup to be async). TypeScript configs (`kick.config.ts`) are\n * skipped here — adopters who use them should run `kick build:assets`\n * to produce a real manifest the resolver can read.\n */\nfunction loadConfigSync(cwd: string): AssetMapConfigShape | null {\n const jsonPath = join(cwd, 'kick.config.json')\n if (existsSync(jsonPath)) {\n try {\n return JSON.parse(readFileSync(jsonPath, 'utf-8')) as AssetMapConfigShape\n } catch {\n return null\n }\n }\n for (const filename of ['kick.config.cjs', 'kick.config.js']) {\n const path = join(cwd, filename)\n if (!existsSync(path)) continue\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { createRequire } = require('node:module') as typeof import('node:module')\n const req = createRequire(join(cwd, 'package.json'))\n const mod = req(path) as { default?: AssetMapConfigShape } & AssetMapConfigShape\n return mod.default ?? mod ?? null\n } catch {\n continue\n }\n }\n\n // Fallback: the CLI snapshot of a transpiled `kick.config.ts`. We\n // can't transpile TS synchronously on the asset hot path, so the CLI\n // (`loadKickConfig`) mirrors the JSON-serialisable `assetMap` +\n // `build.outDir` into `.kickjs/kick.config.json` whenever it loads a\n // `.ts`/`.js` config. This is what makes `assets.x.y()` resolve in\n // dev for `.ts`-config projects before the first production build.\n const snapshotPath = join(cwd, '.kickjs', 'kick.config.json')\n if (existsSync(snapshotPath)) {\n try {\n return JSON.parse(readFileSync(snapshotPath, 'utf-8')) as AssetMapConfigShape\n } catch {\n return null\n }\n }\n return null\n}\n\n/**\n * Keys we MUST return `undefined` for: returning anything truthy\n * here causes Promise resolution to treat the Proxy as a thenable\n * and `await` it (resolving to whatever `then.call(...)` produces).\n * Two-element list deliberately — every other coercion-related key\n * goes through {@link PASSTHROUGH_KEYS} below so `String(assets)`,\n * template literals, and `util.inspect` keep working.\n */\nconst PROMISE_DETECTION_KEYS: ReadonlySet<string | symbol> = new Set<string | symbol>(['then'])\n\n/**\n * Keys we let fall through to the proxy target (a function-shaped\n * object). The default `Object.prototype` implementations of\n * `toString` / `valueOf` etc. are correct for these — returning\n * `undefined` here would break `String(assets)` and template\n * literals (`${assets}`) because `Symbol.toPrimitive` falling through\n * to a missing `toString` throws. Adopters that hit `assets.toString`\n * get `'function () {}'` back, which is fine — the contract is\n * \"asset access returns paths\"; everything else is incidental.\n */\nconst PASSTHROUGH_KEYS: ReadonlySet<string | symbol> = new Set<string | symbol>([\n 'toString',\n 'valueOf',\n 'constructor',\n 'prototype',\n 'name',\n 'length',\n 'asymmetricMatch',\n 'nodeType',\n Symbol.toPrimitive,\n Symbol.toStringTag,\n Symbol.iterator,\n Symbol.asyncIterator,\n])\n\nfunction createNamespaceProxy(parts: readonly string[]): unknown {\n const target = function () {} as unknown as Record<string, unknown>\n return new Proxy(target, {\n apply() {\n if (parts.length < 2) {\n throw new UnknownAssetError(parts[0] ?? '', '')\n }\n const [namespace, ...rest] = parts\n return resolveAsset(namespace, rest.join('/'))\n },\n get(t, prop) {\n if (PROMISE_DETECTION_KEYS.has(prop)) return undefined\n if (PASSTHROUGH_KEYS.has(prop)) return Reflect.get(t, prop)\n if (typeof prop === 'symbol') return undefined\n return createNamespaceProxy([...parts, prop])\n },\n })\n}\n\nfunction createAssetProxy(): KickAssets {\n return new Proxy({} as Record<string, unknown>, {\n get(t, prop) {\n if (PROMISE_DETECTION_KEYS.has(prop)) return undefined\n if (PASSTHROUGH_KEYS.has(prop)) return Reflect.get(t, prop)\n if (typeof prop === 'symbol') return undefined\n return createNamespaceProxy([prop])\n },\n }) as unknown as KickAssets\n}\n\n/**\n * Variant A — the ambient Proxy. Default accessor for static call\n * sites where the namespace + key are literals known at compile time.\n *\n * @example\n * ```ts\n * import { assets } from '@forinda/kickjs'\n * const path = assets.mails.welcome()\n * ```\n */\nexport const assets: KickAssets = createAssetProxy()\n\n/**\n * Variant B — the hook accessor. Returns the same Proxy as `assets`;\n * factory shape exists for testability + DI. Mock via\n * `vi.mock('@forinda/kickjs', () => ({ useAssets: () => fakeAssets }))`.\n *\n * @example\n * ```ts\n * import { useAssets } from '@forinda/kickjs'\n * class MailService {\n * private assets = useAssets()\n * send(name: string) {\n * return ejs.renderFile(this.assets.mails[name](), {})\n * }\n * }\n * ```\n */\nexport function useAssets(): KickAssets {\n return assets\n}\n\n// Install this module's resolver into the container's asset slot — the\n// container deliberately avoids importing this file (edge purity; see\n// asset-resolver-slot.ts). Runs at module scope so any node app that loads\n// the asset manager gets full @Asset injection.\nimport { _setAssetResolver } from './asset-resolver-slot'\n_setAssetResolver(resolveAsset)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"core-B0JmdBvX.mjs","names":[],"sources":["../src/core/context-decorator.ts","../src/core/asset-keys.ts","../src/core/assets.ts"],"sourcesContent":["import { assertFlagTest, type RouteFlagTest } from './route-flag'\nimport 'reflect-metadata'\nimport { METADATA, type Constructor, type MaybePromise } from './interfaces'\nimport { pushClassMeta, pushMethodMeta } from './metadata'\nimport type { InjectionToken } from './token'\nimport type { ContextMetaKey, ExecutionContext, MetaValue } from './execution-context'\n\n/**\n * String-literal union of every known context key — the union of the\n * keys declared on {@link ContextKeys} (key-only registry) and\n * {@link ContextMeta} (value-type registry). Resolves to `string` when\n * the project hasn't augmented either yet (so a first-day project keeps\n * compiling), and narrows to the concrete keys once the\n * `declare module '@forinda/kickjs' { interface ContextKeys/ContextMeta { ... } }`\n * blocks land.\n *\n * Used as the element type of `dependsOn`, so a typo like\n * `dependsOn: ['tenent']` becomes a TS error instead of a boot-time\n * `MissingContributorError`.\n *\n * Unioning BOTH registries is deliberate: it means adding a value type\n * via `ContextMeta` automatically makes that key a valid `dependsOn`\n * target, and you can register a dependsOn-able key in `ContextKeys`\n * without being forced to give it a value type. (Before the split,\n * `dependsOn` was keyed off `ContextMeta` alone, so augmenting\n * `ContextMeta` for some keys broke `dependsOn` for every key you\n * hadn't added there.)\n */\n// Moved to `execution-context` (beside the registries it reads) so that\n// module can constrain `get`/`set` without a circular import. Re-exported\n// here so existing `from '@forinda/kickjs'` imports keep resolving.\nexport type { ContextMetaKey } from './execution-context'\n\n/**\n * What a single `deps` entry is allowed to be — the runtime calls\n * `container.resolve(value)`, which accepts an injection token brand\n * or a constructor (decorated class). Anything else (a string literal,\n * an array, a plain object) silently fails at boot when the runner\n * tries to resolve it; constraining at the type level surfaces the\n * mistake in the editor instead.\n */\nexport type DepValue = InjectionToken<unknown> | Constructor<unknown>\n\n/**\n * Maps a `deps` declaration object to its resolved-instance shape.\n *\n * `deps: { db: DB_TOKEN, cache: CacheService }` produces\n * `{ db: Db, cache: CacheService }` — the runner resolves each value\n * against the DI container and hands the result to `resolve()`.\n */\nexport type ResolvedDeps<D extends Record<string, DepValue>> = {\n [K in keyof D]: D[K] extends InjectionToken<infer T>\n ? T\n : D[K] extends Constructor<infer T>\n ? T\n : never\n}\n\n/**\n * Keys of `T` that are declared required (i.e. not `?`-optional).\n */\ntype RequiredParamKeys<T> = {\n // eslint-disable-next-line @typescript-eslint/ban-types\n [K in keyof T]-?: {} extends Pick<T, K> ? never : K\n}[keyof T]\n\n/**\n * Params a call site MUST supply: the required keys of `P` that\n * `paramDefaults` (type `PD`) doesn't already provide.\n *\n * The `string extends keyof P` guard short-circuits the index-signature\n * case — `P` defaults to `Record<string, never>` for the overwhelmingly\n * common zero-params contributor, and a mapped type over `string` would\n * otherwise report \"every key is required\" and break `@Foo` for every\n * existing decorator in the wild.\n */\nexport type MissingParamKeys<P, PD> = string extends keyof P\n ? never\n : Exclude<RequiredParamKeys<P>, keyof PD>\n\n/**\n * The argument type for a call site (`@Foo({...})`, `.with({...})`).\n *\n * `Partial<P>` when every required field has a default, so adopters keep\n * overriding just the fields they care about. Otherwise `Partial<P>`\n * intersected with the undefaulted required fields, making those\n * mandatory at each call site.\n */\nexport type CallSiteParams<P, PD> = [MissingParamKeys<P, PD>] extends [never]\n ? Partial<P>\n : Partial<P> & Pick<P, MissingParamKeys<P, PD> & keyof P>\n\n/**\n * Spec passed to {@link defineContextDecorator}. Describes one entry in\n * the per-request contributor pipeline.\n *\n * @typeParam K - The {@link ContextMeta} key this contributor populates.\n * @typeParam D - DI dependency declaration shape (see {@link ResolvedDeps}).\n * @typeParam Ctx - Concrete execution-context type. Defaults to the abstract\n * {@link ExecutionContext}; HTTP contributors typically\n * default to `RequestContext` at the call site.\n * @typeParam PD - Inferred from `paramDefaults`. Drives which params each\n * call site is required to supply — see {@link CallSiteParams}.\n * Never spell this by hand.\n */\nexport interface ContextDecoratorSpec<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Partial<P>,\n> {\n /** ContextMeta key the resolved value is written to. */\n key: K\n /**\n * Skip this contributor on routes carrying the named route flag\n * (`defineRouteFlag`). See {@link ContributorRegistration.skipWhen}.\n */\n skipWhen?: RouteFlagTest\n /**\n * Run this contributor only on routes carrying the named route flag, or\n * matching the predicate.\n */\n onlyWhen?: RouteFlagTest\n /**\n * DI dependencies resolved by the runner before `resolve()` runs.\n * Map entry keys become property names on the resolved-deps argument\n * passed to `resolve()`.\n */\n deps?: D\n /**\n * Other contributor keys that must populate before this one runs.\n * Enforced via topo-sort at startup; missing deps and cycles fail boot.\n *\n * Typed against {@link ContextMetaKey} — the union of `keyof ContextMeta`\n * and `keyof ContextKeys` — once the project augments either, so\n * `dependsOn: ['tenent']` (typo) is a TS error, not a boot-time\n * `MissingContributorError`. Falls back to plain `string[]` when both\n * registries are empty (no augmentation yet) so first-day projects keep\n * compiling. Cast escape hatch: `dependsOn: ['custom-key' as ContextMetaKey]`.\n */\n dependsOn?: readonly ContextMetaKey[]\n /**\n * If `true`, a thrown `resolve` skips the contributor instead of\n * forwarding to the request error handler. The key is left unset\n * and downstream consumers see `ctx.get(key) === undefined`.\n */\n optional?: boolean\n /**\n * HTTP routes only: run this contributor before request validation and\n * `@Middleware()` handlers, instead of after them.\n *\n * For contributors that must answer before a request's shape is checked —\n * authentication above all. Without it, a request with no credentials and a\n * malformed body is rejected by validation (422) before the auth contributor\n * can reject it (401), telling an anonymous caller what the schema expects.\n *\n * `ctx.body` / `ctx.query` / `ctx.params` are **unvalidated** at this point,\n * and file uploads may not be parsed yet — read credentials (headers,\n * cookies), not the payload. It may only `dependsOn` contributors that also\n * set `beforeValidation`; anything else fails boot.\n */\n beforeValidation?: boolean\n /**\n * Default per-call params merged with the call-site params; call-site\n * wins. When omitted, params default to `{}` and the resolver\n * receives an empty object — back-compat with today's zero-arg\n * decorators.\n *\n * Param shape is intentionally unconstrained: plain data, functions,\n * and closures are all valid. Adopters who want runtime validation\n * should perform it inside `resolve()` — keeps the framework\n * validator-agnostic so projects can plug Zod / Valibot / hand-rolled\n * checks without the framework caring.\n *\n * **Required fields are enforced at the call site.** A required field\n * of `P` that has no entry here must be supplied wherever the\n * decorator is applied — `@Foo({ action: 'audit:read' })`. The bare\n * `@Foo` form and `.registration` stop type-checking for such a\n * decorator, because neither can supply the value.\n *\n * This exists so nobody has to invent a meaningless default just to\n * satisfy the type (`action: 'settings:read'` on a permission\n * contributor every call site overrides anyway). A default that is\n * never correct is worse than no default: forget the argument at one\n * call site and the route silently gates on the placeholder instead\n * of failing to compile.\n *\n * Omit this entirely for the common zero-params case — `P` defaults\n * to `Record<string, never>`, which requires nothing.\n */\n paramDefaults?: PD\n /**\n * Runtime mirror of the compile-time requirement above: param names\n * that must be present after merging call-site params over\n * `paramDefaults`. A missing one throws `TypeError` at the point of\n * use (decoration time for `@Foo`, call time for `@Foo({...})` /\n * `.with({...})`), naming the decorator and the field.\n *\n * The type-level check already covers TypeScript call sites, so this\n * is for the paths types can't reach: plain-JS adopters, `as any`\n * escapes, and params assembled dynamically. Optional — list only\n * fields whose absence is a correctness bug rather than a default.\n */\n requiredParams?: readonly (keyof P & string)[]\n /**\n * Hook invoked when `resolve()` throws and `optional` is `false`.\n * Returning a value writes it to `ctx.set(key, …)`; returning\n * `undefined` / `void` skips. Throwing inside the hook forwards\n * the new error to the request error handler.\n *\n * Async-permitted by design — adopters frequently want to\n * `await auditService.log(err)` or `await cache.fallback(...)`.\n *\n * The third `params` argument carries the resolved per-call params\n * (call-site overrides merged on top of `paramDefaults`).\n */\n onError?: (err: unknown, ctx: Ctx, params: P) => MaybePromise<MetaValue<K> | undefined | void>\n /**\n * Compute and return the value to write into `ctx.set(key, …)`.\n *\n * The third `params` argument carries the resolved per-call params\n * (call-site overrides merged on top of `paramDefaults`). When the\n * decorator is applied zero-arg (`@Foo`), `params` equals\n * `paramDefaults` (or `{}` if defaults are omitted). When applied as\n * a factory (`@Foo({ ... })`), the call-site object is merged on top.\n */\n resolve: (ctx: Ctx, deps: ResolvedDeps<D>, params: P) => MaybePromise<MetaValue<K>>\n}\n\n/**\n * Internal, normalised registration produced by {@link defineContextDecorator}.\n *\n * Frozen and shared by every use site of the decorator (method, class,\n * module, adapter, global). The pipeline builder dedupes by `key` using\n * the precedence rule defined in §20.4 of `architecture.md`.\n */\nexport interface ContributorRegistration<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n> {\n readonly key: K\n readonly deps: D\n readonly dependsOn: readonly ContextMetaKey[]\n readonly optional: boolean\n /**\n * Per-call params from the spec are baked into the closure at\n * decorator-construction time, so the runner-facing `onError`\n * exposes only `(err, ctx)`.\n */\n readonly onError?: (err: unknown, ctx: Ctx) => MaybePromise<MetaValue<K> | undefined | void>\n /**\n * Per-call params from the spec are baked into the closure at\n * decorator-construction time, so the runner-facing `resolve`\n * exposes only `(ctx, deps)`. Adopters who write their own\n * registrations (without `defineContextDecorator`) match this\n * shape directly — params are an implementation detail of the\n * decorator factory, not of the runtime pipeline.\n */\n readonly resolve: (ctx: Ctx, deps: ResolvedDeps<D>) => MaybePromise<MetaValue<K>>\n /**\n * Stack snapshot captured the moment the decorator was defined.\n * Used by the contributor-pipeline builder to point boot-time\n * errors (`MissingContributorError`, `ContributorCycleError`,\n * `DuplicateContributorError`) at the adopter file that declared\n * the broken contributor — instead of forcing them to grep for\n * the key string.\n *\n * Optional because adopters who hand-roll a `ContributorRegistration`\n * (without `defineContextDecorator`) may not have a useful stack to\n * attach. Format is the raw `Error.stack` string; consumers slice\n * the first non-framework frame for display.\n */\n readonly definedAt?: string\n /** Runs before request validation on HTTP routes — see {@link ContextDecoratorSpec.beforeValidation}. */\n readonly beforeValidation?: boolean\n /**\n * Skip this contributor on routes carrying the named route flag.\n *\n * The composable way to say \"not here\". Exempting a contributor otherwise\n * means registering a permissive twin under the same key, which only its\n * author can do — a flag is declared on the route, so an adopter can exempt\n * a plugin's contributor without owning its key or forking it.\n *\n * ```ts\n * defineHttpContextDecorator({ key: 'user', skipWhen: 'auth.public', resolve })\n * ```\n */\n readonly skipWhen?: RouteFlagTest\n /**\n * Run this contributor **only** on routes carrying the named flag. The\n * inverse of {@link skipWhen}, and the case with no expression today short\n * of an `if` inside every resolver.\n */\n readonly onlyWhen?: RouteFlagTest\n}\n\n/**\n * Type-erased registration shape used in the public collection slots\n * (`AppModule.contributors?()`, `AppAdapter.contributors?()`,\n * `KickPlugin.contributors?()`, `bootstrap({ contributors })`).\n *\n * The narrow `ContributorRegistration` carries a `Ctx` parameter so\n * `resolve(ctx, deps)` is typed at the *definition* site, but `Ctx`\n * sits in a contravariant position on `resolve` — meaning a\n * `ContributorRegistration<…, RequestContext>` is *not* assignable to\n * `ContributorRegistration<…, ExecutionContext>` even though every\n * `RequestContext` is-a `ExecutionContext`. That's TS-sound but\n * useless: the runner only ever calls a contributor with the same\n * concrete ctx the route mounts under, so an HTTP-typed contributor\n * never sees a non-HTTP ctx. Erasing `Ctx` to `any` in the collection\n * type lets adopters store contributors typed against `RequestContext`\n * (or any future transport-specific ctx) in the same array without\n * casting.\n */\nexport type AnyContributorRegistration = ContributorRegistration<\n ContextMetaKey,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any\n>\n\n/** Convenience alias for `AnyContributorRegistration[]` collections. */\nexport type ContributorRegistrations =\n | AnyContributorRegistration[]\n | readonly AnyContributorRegistration[]\n\n/**\n * The decorator function returned by either a zero-arg\n * {@link ContextDecorator} application or a factory call. Overloaded\n * to satisfy both class and method decorator shapes — adopters apply\n * the same decorator to either site without TS rejecting the call.\n */\nexport interface ContextDecoratorTarget {\n /** Class decorator usage. */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n (target: Function): void\n /** Method decorator usage. */\n (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void\n}\n\n/**\n * The value returned by {@link defineContextDecorator}.\n *\n * Resolves to one of two shapes depending on whether every required\n * field of `P` has a `paramDefaults` entry:\n *\n * - {@link ContextDecoratorWithDefaults} — the classic shape. Usable\n * bare (`@Foo`), as a factory (`@Foo({...})`), via `.with(params)`,\n * and via `.registration`.\n * - {@link ContextDecoratorRequiringParams} — when `P` has required\n * fields `paramDefaults` doesn't cover. Only the factory and\n * `.with(params)` forms exist, because the others have no way to\n * supply the missing values. Applying such a decorator bare is a\n * compile error rather than a silent `undefined` at request time.\n *\n * `PD` is inferred from the spec's `paramDefaults`; the default here\n * (`Partial<P>`) keeps hand-written `ContextDecorator<K, D, P>`\n * annotations resolving to the permissive shape as before.\n */\nexport type ContextDecorator<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD = Partial<P>,\n> = [MissingParamKeys<P, PD>] extends [never]\n ? ContextDecoratorWithDefaults<K, D, P, Ctx>\n : ContextDecoratorRequiringParams<K, D, P, Ctx, PD>\n\n/**\n * Decorator whose params are fully defaulted (or which takes none).\n * Callable as a class or method decorator directly, as a factory, or\n * via `.with(params)` / `.registration` at non-decorator registration\n * sites.\n */\nexport interface ContextDecoratorWithDefaults<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n> {\n /**\n * Class decorator usage with `paramDefaults` — `@Foo class C {}`.\n * The class constructor is a `Function`, so this overload only\n * matches actual classes (not plain object literals — which\n * fall through to the factory overload below).\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n (target: Function): void\n /**\n * Method decorator usage with `paramDefaults` — `@Foo` over a\n * method. The required `propertyKey` distinguishes this from the\n * factory overload's single-object-arg call shape.\n */\n (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void\n /**\n * Zero-arg factory usage — `@Foo()` returns a decorator with\n * `paramDefaults` applied. Equivalent in behaviour to the bare\n * `@Foo` form, exposed here so adopters who prefer \"always call the\n * factory\" don't need an `as any` cast.\n */\n (): ContextDecoratorTarget\n /**\n * Factory usage — `@Foo({ ... })` returns the actual decorator with\n * call-site params merged onto `paramDefaults`. Matches a single\n * params object; plain objects don't satisfy the `Function` /\n * 2-arg-minimum constraints above, so TS picks this overload.\n *\n * Accepts `Partial<P>` so adopters can override only the fields\n * they care about — anything missing falls back to `paramDefaults`\n * at runtime. Inside `resolve()`, `params` is the fully merged\n * `P`, never a `Partial<P>`.\n *\n * The returned decorator is overloaded for class AND method usage —\n * `@Foo({...}) class C {}` and `@Foo({...}) m() {}` both type-check.\n */\n (params: Partial<P>): ContextDecoratorTarget\n /**\n * Bare registration — uses `paramDefaults` only. Use this at non-\n * decorator registration sites (module / adapter / bootstrap) when\n * the call site doesn't need to override params.\n */\n readonly registration: ContributorRegistration<K, D, Ctx>\n /**\n * Build a registration with call-site params merged onto\n * `paramDefaults`. Use this at non-decorator registration sites\n * when the call site DOES want to override params:\n *\n * ```ts\n * bootstrap({\n * contributors: [LoadTenant.with({ source: 'subdomain' }).registration],\n * })\n * ```\n *\n * `Partial<P>` for the same reason as the factory overload — the\n * runtime merges with `paramDefaults`, so adopters only specify\n * fields they want to override.\n */\n with(params: Partial<P>): { readonly registration: ContributorRegistration<K, D, Ctx> }\n}\n\n/**\n * Decorator with at least one required param that `paramDefaults`\n * doesn't cover. Deliberately narrower than\n * {@link ContextDecoratorWithDefaults}: the bare `@Foo` form, the\n * zero-arg `@Foo()` form, and the `.registration` accessor are all\n * absent, because none of them can supply the missing value.\n *\n * ```ts\n * const OperatorPerm = defineContextDecorator.withParams<{ action: string }>()({\n * key: 'operatorPerm',\n * resolve: (ctx, _deps, { action }) => check(ctx, action),\n * })\n *\n * class Ops {\n * @OperatorPerm // ✗ compile error — `action` missing\n * @Get('/a')\n * a(ctx: RequestContext) {}\n *\n * @OperatorPerm({ action: 'read' }) // ✓\n * @Get('/b')\n * b(ctx: RequestContext) {}\n * }\n * ```\n *\n * If you want the bare form back, give `action` a default in\n * `paramDefaults` — but only when the default is genuinely correct for\n * an undecorated route, which for a permission string it usually isn't.\n */\nexport interface ContextDecoratorRequiringParams<\n K extends ContextMetaKey = ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD = Partial<P>,\n> {\n /**\n * Factory usage — the only decorator form available here. The\n * argument type carries every required field `paramDefaults` didn't\n * provide, so omitting one is a compile error at this call site.\n */\n (params: CallSiteParams<P, PD>): ContextDecoratorTarget\n /**\n * Build a registration for non-decorator registration sites. Same\n * required-field rule as the factory form.\n */\n with(params: CallSiteParams<P, PD>): {\n readonly registration: ContributorRegistration<K, D, Ctx>\n }\n}\n\n/**\n * Curried params-first helper for {@link defineContextDecorator}.\n *\n * Solves the partial-inference problem: TS generics are positional, so\n * the moment an adopter wants to specify the per-call params shape\n * `P`, they're forced to spell `K` and `D` by hand — losing the\n * automatic `deps` inference that drives the `(ctx, deps, params) =>`\n * resolver signature:\n *\n * ```ts\n * // ❌ Positional generics — must repeat the deps type, and `D` no\n * // longer flows from `spec.deps` into `resolve(ctx, deps, ...)`.\n * defineContextDecorator<'tenant', { repo: typeof TENANT_REPO }, MyParams>({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO },\n * resolve: (ctx, { repo }, params) => ...,\n * })\n *\n * // ✅ Curried form — only `P` is spelled; `K`, `D`, `Ctx` infer.\n * defineContextDecorator.withParams<MyParams>()({\n * key: 'tenant',\n * deps: { repo: TENANT_REPO }, // D inferred\n * resolve: (ctx, { repo }, params) => ..., // deps + params both typed\n * })\n * ```\n *\n * Use this whenever the contributor takes call-site params; use the\n * positional form when there are no params (the common case).\n */\nexport interface DefineContextDecoratorWithParams<P extends Record<string, unknown>> {\n <\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n // Inferred from `spec.paramDefaults`. The `Record<never, never>`\n // default (NOT `Partial<P>`) is what makes an omitted `paramDefaults`\n // mean \"nothing is defaulted\" — so every required field of `P` lands\n // on the call site. `Partial<P>` here would make `keyof PD` cover all\n // of `P` and silently defeat the whole check.\n PD extends Partial<P> = Record<never, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, Ctx, PD>,\n ): ContextDecorator<K, D, P, Ctx, PD>\n}\n\n/**\n * The public shape of {@link defineContextDecorator}. Combines the\n * original positional-generic call signature with the curried\n * `.withParams<P>()` helper that fixes partial-inference for the\n * parameterised case.\n */\nexport interface DefineContextDecoratorFn {\n <\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Record<never, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, Ctx, PD>,\n ): ContextDecorator<K, D, P, Ctx, PD>\n\n /**\n * Curried entry point. Spell only the per-call params shape `P`;\n * `K`, `D`, and `Ctx` are inferred from the spec passed to the\n * returned function. See {@link DefineContextDecoratorWithParams}\n * for the rationale.\n */\n withParams<P extends Record<string, unknown>>(): DefineContextDecoratorWithParams<P>\n}\n\n/**\n * Define a typed context contributor.\n *\n * The returned function works as a method or class decorator:\n *\n * ```ts\n * const LoadTenant = defineContextDecorator({\n * key: 'tenant',\n * deps: { tenants: TenantsRepo },\n * resolve: (ctx, { tenants }) => tenants.findByHost(ctx.requestId),\n * })\n *\n * @LoadTenant\n * @Get('/me')\n * me(ctx: RequestContext) {\n * ctx.get('tenant') // typed via ContextMeta\n * }\n * ```\n *\n * For non-decorator registration (module/adapter/bootstrap), pass\n * `LoadTenant.registration` into the appropriate contributors list.\n *\n * Boot-time validation: a missing `resolve`, an empty `key`, or a\n * non-array `dependsOn` throws `TypeError` here at definition time\n * (typically module-load) rather than waiting for the first request.\n * See `architecture.md` §20 for the full pipeline plan.\n *\n * **Parameterised contributors:** TS generics are positional, so to\n * specify `P` (the per-call params shape) with this signature you\n * also have to spell `K` and `D` — which loses automatic `deps`\n * inference. Use {@link DefineContextDecoratorFn.withParams} instead:\n * `defineContextDecorator.withParams<MyParams>()(spec)`.\n */\nfunction defineContextDecoratorImpl<\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n P extends Record<string, unknown> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Record<never, never>,\n>(spec: ContextDecoratorSpec<K, D, P, Ctx, PD>): ContextDecorator<K, D, P, Ctx, PD> {\n // ---- Boot-time validation --------------------------------------\n // Surface mistakes the moment `defineContextDecorator` is called\n // (typically module-load time) instead of letting them ride to\n // request time as cryptic ContextMeta misses or container failures.\n // Cheap, ~15 lines, catches a whole class of definition-time bugs.\n if (spec === null || typeof spec !== 'object') {\n throw new TypeError(\n 'defineContextDecorator: spec must be an object literal. ' +\n 'See architecture.md §20.4 for the spec shape.',\n )\n }\n if (typeof spec.key !== 'string' || spec.key.length === 0) {\n const got = typeof spec.key === 'string' ? '\"\"' : typeof spec.key\n throw new TypeError(\n `defineContextDecorator: spec.key must be a non-empty string (got ${got}). ` +\n 'Pick a name that matches a key declared on `ContextMeta` (or one of the framework keys).',\n )\n }\n if (typeof spec.resolve !== 'function') {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.resolve is required and must be a function ` +\n `(got ${typeof spec.resolve}). Each contributor needs a resolver that produces ` +\n `the value to write into ctx.set(${JSON.stringify(spec.key)}, …).`,\n )\n }\n if (spec.onError !== undefined && typeof spec.onError !== 'function') {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.onError must be a function when provided ` +\n `(got ${typeof spec.onError}).`,\n )\n }\n if (spec.dependsOn !== undefined && !Array.isArray(spec.dependsOn)) {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.dependsOn must be an array when provided ` +\n `(got ${typeof spec.dependsOn}).`,\n )\n }\n if (spec.requiredParams !== undefined && !Array.isArray(spec.requiredParams)) {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.requiredParams must be an array when provided ` +\n `(got ${typeof spec.requiredParams}).`,\n )\n }\n if (\n Array.isArray(spec.requiredParams) &&\n spec.requiredParams.some((p) => typeof p !== 'string' || p.length === 0)\n ) {\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.requiredParams entries must be non-empty strings.`,\n )\n }\n if (\n Array.isArray(spec.dependsOn) &&\n spec.dependsOn.some((dep) => typeof dep !== 'string' || dep.length === 0)\n ) {\n // TS narrows dependsOn to `readonly ContextMetaKey[]` for typed\n // adopters, but JS callers (and TS callers who erase via `as any`)\n // can still slip through with `[42]` or `['']`. Surface it at\n // definition time as a TypeError instead of riding to first\n // request as a `MissingContributorError` on a phantom key.\n throw new TypeError(\n `defineContextDecorator(${spec.key}): spec.dependsOn entries must be non-empty strings.`,\n )\n }\n\n // Source-location capture. `new Error().stack` here records the\n // adopter's call site so boot-time errors (cycles, missing deps,\n // duplicate keys) can point at the file that declared the broken\n // decorator. Bad boot errors are why frameworks get abandoned —\n // a single stack frame turns \"what's a 'tenent'?\" into \"edit\n // src/contributors/tenant.ts line 42\".\n const definedAt = new Error().stack\n\n // Snapshot + freeze paramDefaults so callers can't mutate the spec\n // object post-definition and shift the merged params under our feet.\n // Same immutability boundary applied to `deps` and `dependsOn`.\n const defaults = Object.freeze({\n ...(spec.paramDefaults ?? ({} as Partial<P>)),\n }) as Readonly<Partial<P>>\n\n // Shared deps + dependsOn — frozen once and reused across every\n // registration produced by this decorator (one per call site).\n // `Object.freeze` is shallow — the values inside (DI tokens,\n // constructors) are intentionally untouched; freezing them would\n // prevent the container from doing legitimate per-instance\n // bookkeeping.\n //\n // The `({} as D)` cast: `spec.deps` is optional (`deps?: D`), so\n // the fallback to `{}` is required. When `D` defaults to\n // `Record<string, never>`, the cast is sound (`{}` matches). When\n // `D` is non-empty AND adopter omitted `deps`, the cast is unsound\n // — but the runner errors loudly the moment it tries to resolve a\n // missing dep, so the mistake surfaces at first request rather\n // than producing wrong-but-silent behaviour. The alternative\n // (forcing `deps` non-optional in the spec) would break ergonomics\n // for the common zero-deps case.\n const sharedDeps = Object.freeze({ ...(spec.deps ?? ({} as D)) }) as D\n const sharedDependsOn = Object.freeze([...(spec.dependsOn ?? [])])\n const sharedOptional = spec.optional ?? false\n\n /**\n * Build a frozen registration that closes over a specific\n * resolved-params object. The runner sees a normal\n * `(ctx, deps) => value` registration — params are baked into the\n * closure, invisible to the pipeline.\n */\n const buildRegistration = (params: P): ContributorRegistration<K, D, Ctx> => {\n // Shallow-freeze the captured params so a misbehaving `resolve()` /\n // `onError()` can't mutate the closure and bleed into later\n // requests through the same registration. Adopters who really need\n // mutable per-request state should write to `ctx.set(...)` from\n // inside the resolver instead of mutating params.\n const frozenParams = Object.freeze({ ...params }) as P\n const onError: ContributorRegistration<K, D, Ctx>['onError'] = spec.onError\n ? (err: unknown, ctx: Ctx) => spec.onError!(err, ctx, frozenParams)\n : undefined\n const resolve: ContributorRegistration<K, D, Ctx>['resolve'] = (\n ctx: Ctx,\n deps: ResolvedDeps<D>,\n ) => spec.resolve(ctx, deps, frozenParams)\n if (spec.skipWhen !== undefined)\n assertFlagTest(spec.skipWhen, `contributor '${spec.key}' skipWhen`)\n if (spec.onlyWhen !== undefined)\n assertFlagTest(spec.onlyWhen, `contributor '${spec.key}' onlyWhen`)\n return Object.freeze({\n key: spec.key,\n deps: sharedDeps,\n dependsOn: sharedDependsOn,\n optional: sharedOptional,\n skipWhen: spec.skipWhen,\n onlyWhen: spec.onlyWhen,\n beforeValidation: spec.beforeValidation === true,\n onError,\n resolve,\n definedAt,\n })\n }\n\n // Default-params registration — used for zero-arg decorator\n // applications and for the `.registration` accessor. Cloning\n // `defaults` so the closure can't be mutated through the spec.\n const defaultRegistration = buildRegistration({ ...defaults } as P)\n\n /**\n * Decide whether the decorator was called as a decorator\n * (`@Foo` / `@Foo(target, key)`) or as a factory\n * (`@Foo()` / `@Foo({...})`).\n *\n * - Class decorator: `(target)` where target is a constructor function.\n * - Method decorator: `(target, propertyKey, descriptor?)` where\n * target is a prototype object and propertyKey is a string/symbol.\n * - Factory (defaults): `()` — adopter wrote `@Foo()` (rare; usually\n * they write the bare `@Foo`). Returns a decorator using\n * `paramDefaults`.\n * - Factory (params): `(params)` where params is a plain object.\n *\n * The legacy decorators that ship with KickJS (`@Get('/path')`,\n * `@Cron('* * * *')`, etc.) use the same heuristic, so adopters\n * already know to pass plain object literals as factory args.\n */\n const isDecoratorCall = (args: unknown[]): boolean => {\n // Zero args is a factory call (`@Foo()`), not a direct decorator\n // application — TS never invokes a decorator with zero args.\n if (args.length === 0) return false\n if (args.length >= 2) return true\n const first = args[0]\n // Functions are class constructors at decoration time.\n if (typeof first === 'function') return true\n // Anything else (plain object, undefined, primitive) → factory call.\n return false\n }\n\n const applyAsDecorator = (\n registration: ContributorRegistration<K, D, Ctx>,\n target: object,\n propertyKey?: string | symbol,\n ): void => {\n if (propertyKey === undefined) {\n // Class decorator — `target` is the constructor.\n pushClassMeta(METADATA.CLASS_CONTRIBUTORS, target, registration)\n } else {\n // Method decorator — `target` is the prototype; we write to its\n // constructor so reads can use the controller class as the\n // lookup key, matching the convention used by @Middleware and\n // consumed by router-builder.ts.\n const constructor = (target as { constructor: object }).constructor\n pushMethodMeta(METADATA.METHOD_CONTRIBUTORS, constructor, String(propertyKey), registration)\n }\n }\n\n /**\n * `true` for `{...}` and `Object.create(null)`. Rejects every\n * built-in subclass (Map / Set / Date / Error / RegExp / Promise),\n * adopter classes, and arrays.\n */\n const isPlainObject = (value: object): boolean => {\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n }\n\n /**\n * Merge call-site `Partial<P>` over `paramDefaults`. Strict guard:\n * accepts only **plain object literals** (or `Object.create(null)`).\n * Reject everything else with a descriptive `TypeError`.\n *\n * Why strict? Object spread copies enumerable own properties only.\n * `{ ...new Date() }` and `{ ...new Map([...])}` both produce `{}`\n * — the params silently drop. Catching this at the boundary\n * surfaces the mistake instead of routing the request with empty\n * config. Adopters who genuinely need a class-instance shape can\n * destructure it themselves: `Foo({ ...new MyParams() })`.\n */\n const mergeParams = (override: unknown): P => {\n if (override === undefined) return { ...defaults } as P\n if (\n override === null ||\n typeof override !== 'object' ||\n Array.isArray(override) ||\n !isPlainObject(override)\n ) {\n const got =\n override === null\n ? 'null'\n : Array.isArray(override)\n ? 'array'\n : typeof override === 'object'\n ? `instance of ${(override as object).constructor?.name ?? 'unknown class'}`\n : typeof override\n throw new TypeError(\n `defineContextDecorator(${spec.key}): factory call requires a plain object literal, got ${got}. ` +\n `Class instances and built-ins (Map, Date, etc) silently spread to empty objects — ` +\n `call the decorator with a spread object literal instead, e.g. ` +\n `\\`MyDecorator({ ...someInstance })\\`.`,\n )\n }\n return { ...defaults, ...(override as Partial<P>) } as P\n }\n\n const sharedRequiredParams = Object.freeze([...(spec.requiredParams ?? [])])\n\n /**\n * Runtime half of the required-params guarantee. The type system\n * already stops TS call sites from omitting a required field, so this\n * catches what types can't see: plain-JS adopters, `as any`, and\n * params built dynamically.\n *\n * `usage` names the form that failed so the message points at the fix\n * — a bare `@Foo` on a decorator with required params needs to become\n * `@Foo({ ... })`, which is a different edit from a factory call that\n * merely forgot one field.\n */\n const assertRequiredParams = (params: P, usage: string): void => {\n if (sharedRequiredParams.length === 0) return\n const missing = sharedRequiredParams.filter(\n (name) => (params as Record<string, unknown>)[name] === undefined,\n )\n if (missing.length === 0) return\n throw new TypeError(\n `defineContextDecorator(${spec.key}): ${usage} is missing required param(s) ` +\n `${missing.map((m) => `'${m}'`).join(', ')}. ` +\n `Supply them at the call site — e.g. \\`@${spec.key}({ ${missing[0]}: … })\\` — or ` +\n `add a default in \\`paramDefaults\\` if one is genuinely correct for every route.`,\n )\n }\n\n // Overloaded function signatures so `decoratorOrFactory`'s type\n // matches `ContextDecorator`'s call shapes directly — no\n // `as unknown as` double-cast needed. The implementation signature\n // (`...args: unknown[]`) is hidden from callers; only the four\n // declared overloads are reachable.\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n function decoratorOrFactory(target: Function): void\n function decoratorOrFactory(\n target: object,\n propertyKey: string | symbol,\n descriptor?: PropertyDescriptor,\n ): void\n function decoratorOrFactory(): ContextDecoratorTarget\n function decoratorOrFactory(params: Partial<P>): ContextDecoratorTarget\n function decoratorOrFactory(...args: unknown[]): unknown {\n if (isDecoratorCall(args)) {\n const [target, propertyKey] = args as [\n object,\n string | symbol | undefined,\n PropertyDescriptor?,\n ]\n // Bare `@Foo` — nothing can supply params here, so a required one\n // that isn't defaulted is a decoration-time (module-load) failure\n // rather than an undefined riding into the resolver.\n assertRequiredParams({ ...defaults } as P, 'bare `@decorator` usage')\n applyAsDecorator(defaultRegistration, target, propertyKey)\n return undefined\n }\n // Factory call — capture merged params and return a decorator.\n const params = mergeParams(args[0])\n assertRequiredParams(params, 'factory call')\n const registration = buildRegistration(params)\n return (target: object, propertyKey?: string | symbol) => {\n applyAsDecorator(registration, target, propertyKey)\n }\n }\n\n // Set a meaningful `.name` so `console.log(LoadTenant)` and stack\n // traces show `[Function: ContextDecorator(tenant)]` instead of\n // `[Function: decoratorOrFactory]`. Function.name is configurable\n // by default, so this works on the existing function reference.\n Object.defineProperty(decoratorOrFactory, 'name', {\n value: `ContextDecorator(${spec.key})`,\n writable: false,\n enumerable: false,\n configurable: true,\n })\n\n // `Object.assign` returns the intersection type, so the decorator\n // gets `.registration` + `.with` properties typed without the\n // previous `as unknown as ContextDecorator<...>` double cast.\n // Properties go on via assign (writable + enumerable defaults),\n // then `Object.freeze` locks the shape so adopters can't mutate\n // `decorator.registration = …` post-construction.\n const decorator = Object.assign(decoratorOrFactory, {\n with: (params: Partial<P>) => {\n const merged = mergeParams(params)\n assertRequiredParams(merged, '`.with()`')\n return { registration: buildRegistration(merged) }\n },\n })\n\n // `.registration` is a getter, not a plain property, so that reading it\n // on a decorator with undefaulted required params throws instead of\n // handing back a registration whose resolver will see `undefined`.\n // (`Object.assign` would have copied the value and lost the check.)\n Object.defineProperty(decorator, 'registration', {\n get() {\n assertRequiredParams({ ...defaults } as P, '`.registration`')\n return defaultRegistration\n },\n enumerable: true,\n configurable: false,\n })\n\n // `ContextDecorator` is a conditional type (permissive vs required-params\n // shape), and TS can't evaluate a conditional whose input generics are\n // still unresolved — so the assignment can't be checked structurally\n // here regardless of how `decorator` is built. The cast is the price of\n // the call-site guarantee; the two branches differ only in which call\n // forms they expose, and every one of them is implemented above.\n return Object.freeze(decorator) as unknown as ContextDecorator<K, D, P, Ctx, PD>\n}\n\n/**\n * Public export — the positional-generic implementation plus the\n * curried `.withParams<P>()` helper attached as a method. Adopters\n * pick whichever form fits the call site:\n *\n * - **Positional** — `defineContextDecorator(spec)` when there are\n * no per-call params (the common case). `K`, `D`, `Ctx` infer.\n * - **Curried** — `defineContextDecorator.withParams<P>()(spec)`\n * when the contributor takes params; spell only `P`, the rest\n * still infer.\n *\n * Frozen so adopters can't reassign `.withParams` at runtime.\n */\nexport const defineContextDecorator: DefineContextDecoratorFn = Object.freeze(\n Object.assign(defineContextDecoratorImpl, {\n withParams: <P extends Record<string, unknown>>(): DefineContextDecoratorWithParams<P> => {\n return <\n K extends ContextMetaKey,\n D extends Record<string, DepValue> = Record<string, never>,\n Ctx extends ExecutionContext = ExecutionContext,\n PD extends Partial<P> = Record<never, never>,\n >(\n spec: ContextDecoratorSpec<K, D, P, Ctx, PD>,\n ): ContextDecorator<K, D, P, Ctx, PD> => defineContextDecoratorImpl<K, D, P, Ctx, PD>(spec)\n },\n }),\n) as DefineContextDecoratorFn\n","// Shared asset-key strategy — used by the build pipeline, the\n// runtime dev resolver, and the typegen emitter so all three layers\n// agree on what the manifest looks like.\n//\n// Three strategies:\n//\n// - `'strip'` — drop the final extension. `pages/index.pug` →\n// `pages/index`. Best for namespaces where every basename is\n// unique (mail templates, pdf invoices). Shortest autocomplete.\n//\n// - `'with-extension'` — keep the extension. `pages/index.pug` →\n// `pages/index.pug`. Required when extension siblings coexist.\n//\n// - `'auto'` (default) — strip when safe, keep extensions when not.\n// The grouping pass collects files by stripped basename; groups\n// of one keep the stripped key, groups of more than one keep the\n// full path. Backward compatible for projects with no\n// extension-siblings; lossless for projects that have them.\n//\n// The function returns the path / key pairs in walk order (input\n// order preserved) so callers can iterate without re-sorting. Sort\n// in the caller if a stable manifest order is needed.\n\nexport type AssetKeyStrategy = 'auto' | 'strip' | 'with-extension'\n\nexport interface AssetKeyPair {\n /** Source-relative path the file was discovered at. */\n rel: string\n /** Final logical key for the manifest, including the namespace prefix. */\n key: string\n}\n\nexport interface GroupAssetKeysOptions {\n /**\n * Strategy applied to this namespace. Default `'auto'`.\n *\n * Adopters override per-namespace via `assetMap.<ns>.keys` in\n * kick.config.ts when they want uniform behaviour (e.g. always\n * include the extension regardless of collisions).\n */\n strategy?: AssetKeyStrategy\n}\n\nexport interface GroupAssetKeysResult {\n pairs: AssetKeyPair[]\n /**\n * Number of collision groups resolved by keeping extensions.\n * Surfaces as an informational log (\"auto-resolved N collisions\")\n * so adopters know whether they're relying on auto-mode or not.\n * Always 0 for `'strip'` (overwrites) and `'with-extension'`\n * (extensions always preserved, no collision possible).\n */\n collisionGroupsResolved: number\n}\n\n/**\n * Compute final manifest keys for a namespace's discovered paths,\n * given the chosen strategy. Pure function — no fs / config / globals\n * involved.\n *\n * @param namespace assetMap key — e.g. `'mails'`, `'pages'`.\n * @param paths source-relative file paths (POSIX separators) discovered\n * under the namespace's `src` directory.\n * @param opts grouping options.\n */\nexport function groupAssetKeys(\n namespace: string,\n paths: readonly string[],\n opts: GroupAssetKeysOptions = {},\n): GroupAssetKeysResult {\n const strategy = opts.strategy ?? 'auto'\n\n if (strategy === 'with-extension') {\n return {\n pairs: paths.map((rel) => ({ rel, key: `${namespace}/${rel}` })),\n collisionGroupsResolved: 0,\n }\n }\n\n if (strategy === 'strip') {\n // Strict strip — last-walk-order wins on collision; no\n // collision tracking here because the docs document this as the\n // adopter's choice. The caller (build / runtime / typegen) can\n // still warn if it wants; this function just shapes keys.\n return {\n pairs: paths.map((rel) => ({ rel, key: `${namespace}/${stripExt(rel)}` })),\n collisionGroupsResolved: 0,\n }\n }\n\n // strategy === 'auto' — group by stripped basename, keep\n // extensions only on collision groups.\n const groups = new Map<string, string[]>()\n for (const rel of paths) {\n const strippedKey = `${namespace}/${stripExt(rel)}`\n const arr = groups.get(strippedKey)\n if (arr) {\n arr.push(rel)\n } else {\n groups.set(strippedKey, [rel])\n }\n }\n\n let collisionGroupsResolved = 0\n // Build a per-rel decision table so we preserve input ordering in\n // the output (manifest writers rely on stable ordering).\n const keyByRel = new Map<string, string>()\n for (const [strippedKey, rels] of groups) {\n if (rels.length === 1) {\n keyByRel.set(rels[0], strippedKey)\n } else {\n collisionGroupsResolved += 1\n for (const rel of rels) {\n keyByRel.set(rel, `${namespace}/${rel}`)\n }\n }\n }\n\n return {\n pairs: paths.map((rel) => ({\n rel,\n key: keyByRel.get(rel) ?? `${namespace}/${rel}`,\n })),\n collisionGroupsResolved,\n }\n}\n\n/** Strip the final extension from a path. `'mails/welcome.ejs'` → `'mails/welcome'`. */\nexport function stripExt(path: string): string {\n // Inline the logic instead of importing extname so this module\n // stays usable in pure-browser contexts. Same algorithm: find the\n // last dot AFTER the last separator, slice up to it.\n const lastSlash = path.lastIndexOf('/')\n const filename = lastSlash >= 0 ? path.slice(lastSlash + 1) : path\n const lastDot = filename.lastIndexOf('.')\n if (lastDot <= 0) return path // no ext, or hidden file like `.env`\n const dirPart = lastSlash >= 0 ? path.slice(0, lastSlash + 1) : ''\n return dirPart + filename.slice(0, lastDot)\n}\n","/**\n * Asset manager runtime.\n *\n * Three accessor surfaces over one resolver engine:\n *\n * - `assets.x.y()` — Proxy ambient, autocomplete-friendly default.\n * - `useAssets()` — hook returning the same Proxy; mockable per-call.\n * - `resolveAsset(ns, k)` — string resolver for dynamic dispatch.\n *\n * Resolution path:\n *\n * 1. Load the build-time manifest (`<outDir>/.kickjs-assets.json`)\n * when present. Manifest existence = \"we're running from a built\n * dist\". Fast path; mapped paths returned verbatim.\n * 2. Otherwise (dev / test) walk the assetMap src directories from\n * `kick.config.{json,js,cjs}` and synthesise the same manifest\n * shape in memory. First call pays the walk cost; subsequent\n * calls hit the cache.\n * 3. `KICK_ASSETS_ROOT` env var overrides everything — the resolver\n * treats the path as the manifest directory and skips discovery.\n *\n * @module @forinda/kickjs/core/assets\n */\n\nimport { existsSync, readFileSync, readdirSync } from 'node:fs'\nimport { isAbsolute, join, resolve, sep } from 'node:path'\nimport { createToken, type InjectionToken } from './token'\nimport { groupAssetKeys } from './asset-keys'\n\nexport const ASSET_MANIFEST_VERSION = 1 as const\n\ninterface AssetManifest {\n version: typeof ASSET_MANIFEST_VERSION\n entries: Record<string, string>\n}\n\ninterface ResolvedManifest {\n manifest: AssetManifest\n root: string\n}\n\n/**\n * Augmentable interface — `kick typegen` emits\n * a `declare global` augmentation listing every namespace + key for\n * the project's `assetMap`. Adopters who haven't run typegen yet\n * still get a runtime-correct Proxy; only typed autocomplete waits\n * on the augmentation.\n */\nexport interface KickAssets {}\n\n/**\n * DI token for the `assets` Proxy. Adopter code that wants to\n * receive assets via constructor injection registers via\n * `container.registerInstance(ASSETS, useAssets())` in `bootstrap()`\n * and injects with `@Inject(ASSETS)`.\n */\nexport const ASSETS: InjectionToken<KickAssets> = createToken<KickAssets>('kick/assets/Map')\n\nexport class UnknownAssetError extends Error {\n readonly namespace: string\n readonly key: string\n constructor(namespace: string, key: string) {\n super(\n `Unknown asset '${namespace}/${key}'. ` +\n `Make sure the namespace exists in kick.config.ts assetMap and that the file is present in the source directory.`,\n )\n this.name = 'UnknownAssetError'\n this.namespace = namespace\n this.key = key\n }\n}\n\nlet manifestCache: ResolvedManifest | null | undefined = undefined\n\n/**\n * Convert a filesystem path to its posix (forward-slash) form. The\n * asset manifest and the public `resolveAsset` / `assets.x.y()` /\n * `useAssets()` contract returns posix paths on every platform so\n * adopters can:\n *\n * - pass the result to `express.static` / `res.sendFile` / `ejs.renderFile`\n * without worrying about Windows vs. posix separators\n * - splice the path into URLs (`href`, `src`, CDN keys) without\n * replacing `\\` with `/` by hand\n * - compare two paths from `resolveAsset` for equality on any host\n *\n * Node's `fs.*` and Express's path-handling APIs accept either\n * separator on Windows, so this normalisation is safe — the only\n * adopter code it could break is something explicitly parsing\n * Windows backslashes back out of the result, which would already be\n * brittle.\n */\nfunction toPosixPath(p: string): string {\n return p.replaceAll('\\\\', '/')\n}\n\nexport function resolveAsset(namespace: string, key: string): string {\n const resolved = loadManifest()\n if (!resolved) {\n throw new UnknownAssetError(namespace, key)\n }\n const entry = resolved.manifest.entries[`${namespace}/${key}`]\n if (!entry) {\n throw new UnknownAssetError(namespace, key)\n }\n const abs = isAbsolute(entry) ? entry : resolve(resolved.root, entry)\n return toPosixPath(abs)\n}\n\n/** Reset the manifest cache. Tests use this; production code shouldn't need it. */\nexport function clearAssetCache(): void {\n manifestCache = undefined\n}\n\nfunction loadManifest(): ResolvedManifest | null {\n // In development we deliberately skip the cache so adding or removing\n // a template file (e.g. dropping `welcome-back.ejs` into\n // `src/templates/mails/`) takes effect without a server restart. The\n // synthesise-dev-manifest path walks ~10 files; the cost is negligible\n // compared to the user-visible surprise of \"I added a file but\n // assets.x.y() throws\". Production keeps the cache because the built\n // manifest under `dist/.kickjs-assets.json` doesn't change at runtime.\n if (process.env.NODE_ENV !== 'production') {\n return discoverManifest()\n }\n if (manifestCache !== undefined) return manifestCache\n manifestCache = discoverManifest()\n return manifestCache\n}\n\nfunction discoverManifest(): ResolvedManifest | null {\n // `KICK_ASSETS_ROOT` is an explicit, deliberate override — it wins in\n // both dev and prod.\n const envRoot = process.env.KICK_ASSETS_ROOT\n if (envRoot) {\n const fromEnv = readBuiltManifest(envRoot)\n if (fromEnv) return fromEnv\n }\n\n const cwd = process.cwd()\n const config = loadConfigSync(cwd)\n\n // In dev, prefer a live walk of the source tree over any built\n // manifest left on disk by an earlier `kick build`. Otherwise a stale\n // `dist/.kickjs-assets.json` shadows the live `src/` tree: a freshly\n // added template file would resolve to the old map (or throw\n // UnknownAssetError) until the next rebuild — exactly the surprise the\n // cache-skip in `loadManifest` exists to avoid. `synthesiseDevManifest`\n // returns null only when there's no `assetMap` config at all, in which\n // case we fall through to the built-manifest probes below.\n if (process.env.NODE_ENV !== 'production') {\n const fresh = synthesiseDevManifest(cwd, config)\n if (fresh) return fresh\n }\n\n // Honour `kick.config.build.outDir` BEFORE the standard probe list\n // so adopters with non-default Vite outputs (e.g. `output/`,\n // `target/`, anything else) don't have to set KICK_ASSETS_ROOT\n // every time. Cached config load — same loader the dev fallback\n // uses. Skip when outDir matches one of the standard candidates;\n // it'd be probed redundantly otherwise.\n const configOutDir = config?.build?.outDir\n if (configOutDir && !(STANDARD_OUT_DIRS as readonly string[]).includes(configOutDir)) {\n const found = readBuiltManifest(resolve(cwd, configOutDir))\n if (found) return found\n }\n\n for (const candidate of STANDARD_OUT_DIRS) {\n const found = readBuiltManifest(join(cwd, candidate))\n if (found) return found\n }\n\n return synthesiseDevManifest(cwd, config)\n}\n\nconst STANDARD_OUT_DIRS = ['dist', 'build', 'out'] as const\n\nfunction readBuiltManifest(dir: string): ResolvedManifest | null {\n const path = join(dir, '.kickjs-assets.json')\n if (!existsSync(path)) return null\n try {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as Partial<AssetManifest>\n if (parsed.version !== ASSET_MANIFEST_VERSION) return null\n if (!parsed.entries || typeof parsed.entries !== 'object') return null\n return { manifest: parsed as AssetManifest, root: resolve(dir) }\n } catch {\n return null\n }\n}\n\ninterface AssetMapConfigShape {\n assetMap?: Record<\n string,\n {\n src: string\n dest?: string\n glob?: string\n keys?: 'auto' | 'strip' | 'with-extension'\n }\n >\n build?: { outDir?: string }\n}\n\nfunction synthesiseDevManifest(\n cwd: string,\n preloadedConfig?: AssetMapConfigShape | null,\n): ResolvedManifest | null {\n const config = preloadedConfig === undefined ? loadConfigSync(cwd) : preloadedConfig\n if (!config?.assetMap) return null\n\n const entries: Record<string, string> = {}\n // Walk order is filesystem-dependent on Linux/macOS, so we collect\n // every match per-namespace first, then sort + feed `groupAssetKeys`\n // for stable manifest output. Build pipeline does the same — both\n // layers agree on the resulting key set.\n for (const [namespace, entry] of Object.entries(config.assetMap)) {\n if (!entry || typeof entry.src !== 'string') continue\n const srcAbs = resolve(cwd, entry.src)\n if (!existsSync(srcAbs)) continue\n\n const collected: Array<{ rel: string; abs: string }> = []\n walkSync(srcAbs, srcAbs, (relPath, absPath) => {\n if (entry.glob && !matchesGlobLite(relPath, entry.glob, namespace)) return\n collected.push({ rel: relPath, abs: absPath })\n })\n collected.sort((a, b) => a.rel.localeCompare(b.rel))\n\n const { pairs, collisionGroupsResolved } = groupAssetKeys(\n namespace,\n collected.map((c) => c.rel),\n { strategy: entry.keys ?? 'auto' },\n )\n const absByRel = new Map(collected.map((c) => [c.rel, c.abs]))\n for (const { rel, key } of pairs) {\n const abs = absByRel.get(rel)\n if (abs) entries[key] = abs\n }\n if (collisionGroupsResolved > 0 && process.env.NODE_ENV !== 'production') {\n // Dev-only nudge — production runs read the build manifest where\n // the collision was already resolved at build time.\n console.log(\n `[kickjs/assets] '${namespace}': ${collisionGroupsResolved} basename collision(s) auto-resolved by keeping extensions ` +\n `(set assetMap.${namespace}.keys to 'strip' or 'with-extension' to override).`,\n )\n }\n }\n\n return { manifest: { version: ASSET_MANIFEST_VERSION, entries }, root: cwd }\n}\n\nconst warnedGlobs = new Set<string>()\n\n/**\n * Tiny glob matcher used only by dev manifest synthesis. Covers the\n * common assetMap glob patterns (`**\\/*`, `**\\/*.ext`,\n * `**\\/*.{a,b}`). Anything exotic prints a one-time warning + falls\n * through to `**\\/*` semantics rather than silently dropping files.\n *\n * The build pipeline (`packages/cli/src/asset-manager/build.ts`) uses\n * the full `glob` package; adopters who hit the dev-mode warning\n * should run `kick build:assets` to produce a real manifest.\n */\nfunction matchesGlobLite(relPath: string, pattern: string, namespace: string): boolean {\n if (pattern === '**/*' || pattern === '**') return true\n const singleExt = /^\\*\\*\\/\\*\\.(\\w+)$/.exec(pattern)\n if (singleExt) return relPath.endsWith(`.${singleExt[1]}`)\n const braceExt = /^\\*\\*\\/\\*\\.\\{([^}]+)\\}$/.exec(pattern)\n if (braceExt) {\n const exts = braceExt[1].split(',').map((e) => e.trim())\n return exts.some((ext) => relPath.endsWith(`.${ext}`))\n }\n const tag = `${namespace}::${pattern}`\n if (!warnedGlobs.has(tag)) {\n warnedGlobs.add(tag)\n console.warn(\n `[kickjs/assets] Dev-mode glob '${pattern}' on '${namespace}' isn't recognised by the lite matcher. ` +\n `Falling back to '**/*'. Run 'kick build:assets' to use the full glob engine.`,\n )\n }\n return true\n}\n\nfunction walkSync(root: string, dir: string, onFile: (rel: string, abs: string) => void): void {\n let entries\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n walkSync(root, full, onFile)\n } else if (entry.isFile()) {\n const rel = full\n .slice(root.length + 1)\n .split(sep)\n .join('/')\n onFile(rel, full)\n }\n }\n}\n\n/**\n * Sync `kick.config.{json,cjs,js}` loader for dev manifest\n * synthesis. Avoids dynamic `import()` (would force every assets\n * lookup to be async). TypeScript configs (`kick.config.ts`) are\n * skipped here — adopters who use them should run `kick build:assets`\n * to produce a real manifest the resolver can read.\n */\nfunction loadConfigSync(cwd: string): AssetMapConfigShape | null {\n const jsonPath = join(cwd, 'kick.config.json')\n if (existsSync(jsonPath)) {\n try {\n return JSON.parse(readFileSync(jsonPath, 'utf-8')) as AssetMapConfigShape\n } catch {\n return null\n }\n }\n for (const filename of ['kick.config.cjs', 'kick.config.js']) {\n const path = join(cwd, filename)\n if (!existsSync(path)) continue\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { createRequire } = require('node:module') as typeof import('node:module')\n const req = createRequire(join(cwd, 'package.json'))\n const mod = req(path) as { default?: AssetMapConfigShape } & AssetMapConfigShape\n return mod.default ?? mod ?? null\n } catch {\n continue\n }\n }\n\n // Fallback: the CLI snapshot of a transpiled `kick.config.ts`. We\n // can't transpile TS synchronously on the asset hot path, so the CLI\n // (`loadKickConfig`) mirrors the JSON-serialisable `assetMap` +\n // `build.outDir` into `.kickjs/kick.config.json` whenever it loads a\n // `.ts`/`.js` config. This is what makes `assets.x.y()` resolve in\n // dev for `.ts`-config projects before the first production build.\n const snapshotPath = join(cwd, '.kickjs', 'kick.config.json')\n if (existsSync(snapshotPath)) {\n try {\n return JSON.parse(readFileSync(snapshotPath, 'utf-8')) as AssetMapConfigShape\n } catch {\n return null\n }\n }\n return null\n}\n\n/**\n * Keys we MUST return `undefined` for: returning anything truthy\n * here causes Promise resolution to treat the Proxy as a thenable\n * and `await` it (resolving to whatever `then.call(...)` produces).\n * Two-element list deliberately — every other coercion-related key\n * goes through {@link PASSTHROUGH_KEYS} below so `String(assets)`,\n * template literals, and `util.inspect` keep working.\n */\nconst PROMISE_DETECTION_KEYS: ReadonlySet<string | symbol> = new Set<string | symbol>(['then'])\n\n/**\n * Keys we let fall through to the proxy target (a function-shaped\n * object). The default `Object.prototype` implementations of\n * `toString` / `valueOf` etc. are correct for these — returning\n * `undefined` here would break `String(assets)` and template\n * literals (`${assets}`) because `Symbol.toPrimitive` falling through\n * to a missing `toString` throws. Adopters that hit `assets.toString`\n * get `'function () {}'` back, which is fine — the contract is\n * \"asset access returns paths\"; everything else is incidental.\n */\nconst PASSTHROUGH_KEYS: ReadonlySet<string | symbol> = new Set<string | symbol>([\n 'toString',\n 'valueOf',\n 'constructor',\n 'prototype',\n 'name',\n 'length',\n 'asymmetricMatch',\n 'nodeType',\n Symbol.toPrimitive,\n Symbol.toStringTag,\n Symbol.iterator,\n Symbol.asyncIterator,\n])\n\nfunction createNamespaceProxy(parts: readonly string[]): unknown {\n const target = function () {} as unknown as Record<string, unknown>\n return new Proxy(target, {\n apply() {\n if (parts.length < 2) {\n throw new UnknownAssetError(parts[0] ?? '', '')\n }\n const [namespace, ...rest] = parts\n return resolveAsset(namespace, rest.join('/'))\n },\n get(t, prop) {\n if (PROMISE_DETECTION_KEYS.has(prop)) return undefined\n if (PASSTHROUGH_KEYS.has(prop)) return Reflect.get(t, prop)\n if (typeof prop === 'symbol') return undefined\n return createNamespaceProxy([...parts, prop])\n },\n })\n}\n\nfunction createAssetProxy(): KickAssets {\n return new Proxy({} as Record<string, unknown>, {\n get(t, prop) {\n if (PROMISE_DETECTION_KEYS.has(prop)) return undefined\n if (PASSTHROUGH_KEYS.has(prop)) return Reflect.get(t, prop)\n if (typeof prop === 'symbol') return undefined\n return createNamespaceProxy([prop])\n },\n }) as unknown as KickAssets\n}\n\n/**\n * Variant A — the ambient Proxy. Default accessor for static call\n * sites where the namespace + key are literals known at compile time.\n *\n * @example\n * ```ts\n * import { assets } from '@forinda/kickjs'\n * const path = assets.mails.welcome()\n * ```\n */\nexport const assets: KickAssets = createAssetProxy()\n\n/**\n * Variant B — the hook accessor. Returns the same Proxy as `assets`;\n * factory shape exists for testability + DI. Mock via\n * `vi.mock('@forinda/kickjs', () => ({ useAssets: () => fakeAssets }))`.\n *\n * @example\n * ```ts\n * import { useAssets } from '@forinda/kickjs'\n * class MailService {\n * private assets = useAssets()\n * send(name: string) {\n * return ejs.renderFile(this.assets.mails[name](), {})\n * }\n * }\n * ```\n */\nexport function useAssets(): KickAssets {\n return assets\n}\n\n// Install this module's resolver into the container's asset slot — the\n// container deliberately avoids importing this file (edge purity; see\n// asset-resolver-slot.ts). Runs at module scope so any node app that loads\n// the asset manager gets full @Asset injection.\nimport { _setAssetResolver } from './asset-resolver-slot'\n_setAssetResolver(resolveAsset)\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v10.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{n as assertFlagTest,s as matchesFlagTest}from"./route-flag-
|
|
12
|
-
//# sourceMappingURL=csrf
|
|
11
|
+
import{n as assertFlagTest,s as matchesFlagTest}from"./route-flag-BWuvJsGZ.mjs";import{n as resolvePathname}from"./client-ip-BbLDp03S.mjs";import{a as setCookie,i as sendJson,n as readCookies}from"./respond-DFRj2-7q.mjs";function randomHex(bytes){let buf=crypto.getRandomValues(new Uint8Array(bytes));return Array.from(buf,b=>b.toString(16).padStart(2,`0`)).join(``)}function csrf(options={}){let cookieName=options.cookie??`_csrf`,headerName=options.header??`x-csrf-token`,protectedMethods=new Set((options.methods??[`POST`,`PUT`,`PATCH`,`DELETE`]).map(m=>m.toUpperCase())),ignorePaths=new Set(options.ignorePaths??[]),tokenLength=options.tokenLength??32,cookieOpts={httpOnly:!1,sameSite:`strict`,secure:process.env.NODE_ENV===`production`,path:`/`,...options.cookieOptions};return(req,res,next)=>{let token=readCookies(req)[cookieName];if(token||(token=randomHex(tokenLength),setCookie(res,cookieName,token,cookieOpts)),!protectedMethods.has(req.method.toUpperCase())||ignorePaths.has(resolvePathname(req)))return next();let headerToken=req.headers[headerName];if(!headerToken||headerToken!==token)return sendJson(res,403,{message:`CSRF token mismatch`});next()}}function csrfGuard(options={}){let cookieName=options.cookie??`_csrf`,headerName=options.header??`x-csrf-token`,protectedMethods=new Set((options.methods??[`POST`,`PUT`,`PATCH`,`DELETE`]).map(m=>m.toUpperCase())),ignorePaths=new Set(options.ignorePaths??[]),tokenLength=options.tokenLength??32,exemptWhen=options.exemptWhen;exemptWhen!==void 0&&assertFlagTest(exemptWhen,`csrfGuard({ exemptWhen })`);let cookieOpts={httpOnly:!1,sameSite:`strict`,secure:process.env.NODE_ENV===`production`,path:`/`,...options.cookieOptions};return(ctx,next)=>{let token=readCookies(ctx.req)[cookieName];token||(token=randomHex(tokenLength),setCookie(ctx.res,cookieName,token,cookieOpts));let method=ctx.route?.method??ctx.req.method??`GET`;if(!protectedMethods.has(method.toUpperCase())||exemptWhen!==void 0&&matchesFlagTest(exemptWhen,ctx.route?.flags,ctx.route)||ignorePaths.has(resolvePathname(ctx.req)))return next();let headerToken=ctx.headers[headerName];!headerToken||headerToken!==token?ctx.json({message:`CSRF token mismatch`},403):next()}}export{csrfGuard as n,randomHex as r,csrf as t};
|
|
12
|
+
//# sourceMappingURL=csrf--1t4qDov.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csrf-CxJItZVa.mjs","names":[],"sources":["../src/core/web-crypto.ts","../src/http/middleware/csrf.ts"],"sourcesContent":["/**\n * Web Crypto helpers — portable across node/bun/deno/workers via\n * `globalThis.crypto`. The Web Crypto migration lives here so the request\n * path never imports `node:crypto` (edge-runtime portability).\n */\n\n/** Random hex string — replacement for `randomBytes(n).toString('hex')`. */\nexport function randomHex(bytes: number): string {\n const buf = crypto.getRandomValues(new Uint8Array(bytes))\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n","import { readCookies, sendJson, setCookie } from './respond'\nimport type { Request, Response, NextFunction } from 'express'\nimport { resolvePathname, type ClientRequestLike } from '../client-ip'\n\nimport { randomHex } from '../../core/web-crypto'\nimport { assertFlagTest, matchesFlagTest, type RouteFlagTest } from '../../core/route-flag'\nimport type { MiddlewareHandler } from '../../core/decorators'\nimport type { RequestContext } from '../context'\n\nexport interface CsrfOptions {\n /** Cookie name for the CSRF token (default: '_csrf') */\n cookie?: string\n /** Header name to check for the token (default: 'x-csrf-token') */\n header?: string\n /** HTTP methods that require CSRF validation (default: POST, PUT, PATCH, DELETE) */\n methods?: string[]\n /** Paths to exclude from CSRF checks (e.g. webhooks) */\n ignorePaths?: string[]\n /** Token byte length before hex encoding (default: 32 = 64 hex chars) */\n tokenLength?: number\n /** Cookie options */\n cookieOptions?: {\n /**\n * Default `false`, because double-submit CSRF needs the page to read the\n * token and echo it in a header. Set `true` only if the client receives\n * the token another way — server-rendered, or from your own endpoint.\n */\n httpOnly?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n secure?: boolean\n path?: string\n }\n}\n\n/**\n * Double-submit cookie CSRF protection middleware.\n *\n * On every request, sets a CSRF token cookie. For state-changing methods\n * (POST, PUT, PATCH, DELETE), validates that the request header matches\n * the cookie value.\n *\n * @example\n * ```ts\n * import { csrf } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middlewares: [\n * cookieParser(),\n * csrf(),\n * // ... other middleware\n * ],\n * })\n * ```\n *\n * Client usage:\n * 1. Read the `_csrf` cookie value\n * 2. Send it in the `x-csrf-token` header on every mutating request\n */\nexport function csrf(options: CsrfOptions = {}) {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const cookieOpts = {\n // `false`, not `true`: double-submit CSRF requires the PAGE to read this\n // cookie and echo it in a header. Under `httpOnly: true` `document.cookie`\n // returns nothing, no header is sent, and every mutating request answers\n // 403 — the documented client flow could not work. The token is not a\n // credential; a token an attacker cannot read is also one your own page\n // cannot send. Set `cookieOptions: { httpOnly: true }` only when the token\n // reaches the client some other way (server-rendered, or your own endpoint).\n httpOnly: false,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Generate or reuse CSRF token\n // NOT `req.cookies` alone: that is populated only by an upstream parser,\n // which Fastify and h3 never have and Express only has if the app mounts\n // one. Without this the middleware never saw the token it had just issued.\n const cookies = readCookies(req)\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomHex(tokenLength)\n setCookie(res, cookieName, token, cookieOpts)\n }\n\n // Skip validation for safe methods and ignored paths\n if (!protectedMethods.has(req.method.toUpperCase())) {\n return next()\n }\n\n // `req.path` is Express-only, so `has(undefined)` never matched and\n // configured exemptions were silently ignored on Fastify / h3.\n if (ignorePaths.has(resolvePathname(req as ClientRequestLike))) {\n return next()\n }\n\n // Validate: header token must match cookie token\n const headerToken = req.headers[headerName] as string | undefined\n\n if (!headerToken || headerToken !== token) {\n return sendJson(res, 403, {\n message: 'CSRF token mismatch',\n })\n }\n\n next()\n }\n}\n\nexport interface CsrfGuardOptions extends CsrfOptions {\n /**\n * Skip the token check on routes carrying a route flag — a name, a list of\n * names (any-of), or a predicate.\n *\n * This is the reason the guard exists. `csrf()` runs before route matching,\n * so its only handle on \"not this endpoint\" is {@link CsrfOptions.ignorePaths}\n * — an exact pathname string that cannot express `/webhooks/:provider` and\n * keeps parsing after an `apiPrefix` change that voids it. A flag is declared\n * on the route itself:\n *\n * ```ts\n * const CsrfExempt = defineRouteFlag('csrf.exempt')\n *\n * @CsrfExempt\n * @Controller()\n * class WebhooksController {\n * @Post('/:provider') receive(ctx: RequestContext) {}\n * }\n * ```\n */\n exemptWhen?: RouteFlagTest\n}\n\n/**\n * Double-submit cookie CSRF protection as a `(ctx, next)` middleware.\n *\n * The ctx-style counterpart of {@link csrf}: it runs inside the matched route,\n * so it can read `ctx.route.flags` and be exempted per route, and it works on\n * every runtime including the `@forinda/kickjs/web` fetch entry.\n *\n * ```ts\n * // per controller — the flag-aware form\n * @Middleware(csrfGuard({ exemptWhen: 'csrf.exempt' }))\n * @Controller()\n * class BillingController {}\n *\n * // edge, app-wide\n * createWebApp({ h3, modules, middleware: [csrfGuard()] })\n * ```\n *\n * Mount `csrf()` instead when you want one app-wide connect middleware that\n * also covers requests matching no route — nothing to exempt, nothing to read.\n */\nexport function csrfGuard(options: CsrfGuardOptions = {}): MiddlewareHandler {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const exemptWhen = options.exemptWhen\n if (exemptWhen !== undefined) assertFlagTest(exemptWhen, 'csrfGuard({ exemptWhen })')\n const cookieOpts = {\n // Same default as `csrf()` above, and for the same reason.\n httpOnly: false,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (ctx: RequestContext, next: () => void): void => {\n const cookies = readCookies(ctx.req)\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomHex(tokenLength)\n setCookie(ctx.res, cookieName, token, cookieOpts)\n }\n\n // Safe methods only ever issue the token.\n const method = ctx.route?.method ?? (ctx.req as { method?: string }).method ?? 'GET'\n if (!protectedMethods.has(method.toUpperCase())) return next()\n\n // Flag first: it is the declared exemption, and cheaper than the path set.\n if (exemptWhen !== undefined && matchesFlagTest(exemptWhen, ctx.route?.flags, ctx.route)) {\n return next()\n }\n\n if (ignorePaths.has(resolvePathname(ctx.req as unknown as ClientRequestLike))) return next()\n\n const headerToken = ctx.headers[headerName] as string | undefined\n if (!headerToken || headerToken !== token) {\n ctx.json({ message: 'CSRF token mismatch' }, 403)\n return\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;6NAOA,SAAgB,UAAU,MAAuB,CAC/C,IAAM,IAAM,OAAO,gBAAgB,IAAI,WAAW,KAAK,CAAC,EACxD,OAAO,MAAM,KAAK,IAAM,GAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CACxE,CCiDA,SAAgB,KAAK,QAAuB,CAAC,EAAG,CAC9C,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,QAAQ,EAAA,CAAG,IAAK,GAAM,EAAE,YAAY,CAAC,CACpF,EACM,YAAc,IAAI,IAAI,QAAQ,aAAe,CAAC,CAAC,EAC/C,YAAc,QAAQ,aAAe,GACrC,WAAa,CAQjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,aACb,EAEA,OAAQ,IAAc,IAAe,OAAuB,CAM1D,IAAI,MADY,YAAY,GACV,CAAC,CAAC,YAcpB,GAZK,QACH,MAAQ,UAAU,WAAW,EAC7B,UAAU,IAAK,WAAY,MAAO,UAAU,GAI1C,CAAC,iBAAiB,IAAI,IAAI,OAAO,YAAY,CAAC,GAM9C,YAAY,IAAI,gBAAgB,GAAwB,CAAC,EAC3D,OAAO,KAAK,EAId,IAAM,YAAc,IAAI,QAAQ,YAEhC,GAAI,CAAC,aAAe,cAAgB,MAClC,OAAO,SAAS,IAAK,IAAK,CACxB,QAAS,qBACX,CAAC,EAGH,KAAK,CACP,CACF,CA8CA,SAAgB,UAAU,QAA4B,CAAC,EAAsB,CAC3E,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,QAAQ,EAAA,CAAG,IAAK,GAAM,EAAE,YAAY,CAAC,CACpF,EACM,YAAc,IAAI,IAAI,QAAQ,aAAe,CAAC,CAAC,EAC/C,YAAc,QAAQ,aAAe,GACrC,WAAa,QAAQ,WACvB,aAAe,IAAA,IAAW,eAAe,WAAY,2BAA2B,EACpF,IAAM,WAAa,CAEjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,aACb,EAEA,OAAQ,IAAqB,OAA2B,CAEtD,IAAI,MADY,YAAY,IAAI,GACd,CAAC,CAAC,YAEf,QACH,MAAQ,UAAU,WAAW,EAC7B,UAAU,IAAI,IAAK,WAAY,MAAO,UAAU,GAIlD,IAAM,OAAS,IAAI,OAAO,QAAW,IAAI,IAA4B,QAAU,MAQ/E,GAPI,CAAC,iBAAiB,IAAI,OAAO,YAAY,CAAC,GAG1C,aAAe,IAAA,IAAa,gBAAgB,WAAY,IAAI,OAAO,MAAO,IAAI,KAAK,GAInF,YAAY,IAAI,gBAAgB,IAAI,GAAmC,CAAC,EAAG,OAAO,KAAK,EAE3F,IAAM,YAAc,IAAI,QAAQ,YAChC,GAAI,CAAC,aAAe,cAAgB,MAAO,CACzC,IAAI,KAAK,CAAE,QAAS,qBAAsB,EAAG,GAAG,EAChD,MACF,CAEA,KAAK,CACP,CACF"}
|
|
1
|
+
{"version":3,"file":"csrf--1t4qDov.mjs","names":[],"sources":["../src/core/web-crypto.ts","../src/http/middleware/csrf.ts"],"sourcesContent":["/**\n * Web Crypto helpers — portable across node/bun/deno/workers via\n * `globalThis.crypto`. The Web Crypto migration lives here so the request\n * path never imports `node:crypto` (edge-runtime portability).\n */\n\n/** Random hex string — replacement for `randomBytes(n).toString('hex')`. */\nexport function randomHex(bytes: number): string {\n const buf = crypto.getRandomValues(new Uint8Array(bytes))\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n","import { readCookies, sendJson, setCookie } from './respond'\nimport type { Request, Response, NextFunction } from 'express'\nimport { resolvePathname, type ClientRequestLike } from '../client-ip'\n\nimport { randomHex } from '../../core/web-crypto'\nimport { assertFlagTest, matchesFlagTest, type RouteFlagTest } from '../../core/route-flag'\nimport type { MiddlewareHandler } from '../../core/decorators'\nimport type { RequestContext } from '../context'\n\nexport interface CsrfOptions {\n /** Cookie name for the CSRF token (default: '_csrf') */\n cookie?: string\n /** Header name to check for the token (default: 'x-csrf-token') */\n header?: string\n /** HTTP methods that require CSRF validation (default: POST, PUT, PATCH, DELETE) */\n methods?: string[]\n /** Paths to exclude from CSRF checks (e.g. webhooks) */\n ignorePaths?: string[]\n /** Token byte length before hex encoding (default: 32 = 64 hex chars) */\n tokenLength?: number\n /** Cookie options */\n cookieOptions?: {\n /**\n * Default `false`, because double-submit CSRF needs the page to read the\n * token and echo it in a header. Set `true` only if the client receives\n * the token another way — server-rendered, or from your own endpoint.\n */\n httpOnly?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n secure?: boolean\n path?: string\n }\n}\n\n/**\n * Double-submit cookie CSRF protection middleware.\n *\n * On every request, sets a CSRF token cookie. For state-changing methods\n * (POST, PUT, PATCH, DELETE), validates that the request header matches\n * the cookie value.\n *\n * @example\n * ```ts\n * import { csrf } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middlewares: [\n * cookieParser(),\n * csrf(),\n * // ... other middleware\n * ],\n * })\n * ```\n *\n * Client usage:\n * 1. Read the `_csrf` cookie value\n * 2. Send it in the `x-csrf-token` header on every mutating request\n */\nexport function csrf(options: CsrfOptions = {}) {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const cookieOpts = {\n // `false`, not `true`: double-submit CSRF requires the PAGE to read this\n // cookie and echo it in a header. Under `httpOnly: true` `document.cookie`\n // returns nothing, no header is sent, and every mutating request answers\n // 403 — the documented client flow could not work. The token is not a\n // credential; a token an attacker cannot read is also one your own page\n // cannot send. Set `cookieOptions: { httpOnly: true }` only when the token\n // reaches the client some other way (server-rendered, or your own endpoint).\n httpOnly: false,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Generate or reuse CSRF token\n // NOT `req.cookies` alone: that is populated only by an upstream parser,\n // which Fastify and h3 never have and Express only has if the app mounts\n // one. Without this the middleware never saw the token it had just issued.\n const cookies = readCookies(req)\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomHex(tokenLength)\n setCookie(res, cookieName, token, cookieOpts)\n }\n\n // Skip validation for safe methods and ignored paths\n if (!protectedMethods.has(req.method.toUpperCase())) {\n return next()\n }\n\n // `req.path` is Express-only, so `has(undefined)` never matched and\n // configured exemptions were silently ignored on Fastify / h3.\n if (ignorePaths.has(resolvePathname(req as ClientRequestLike))) {\n return next()\n }\n\n // Validate: header token must match cookie token\n const headerToken = req.headers[headerName] as string | undefined\n\n if (!headerToken || headerToken !== token) {\n return sendJson(res, 403, {\n message: 'CSRF token mismatch',\n })\n }\n\n next()\n }\n}\n\nexport interface CsrfGuardOptions extends CsrfOptions {\n /**\n * Skip the token check on routes carrying a route flag — a name, a list of\n * names (any-of), or a predicate.\n *\n * This is the reason the guard exists. `csrf()` runs before route matching,\n * so its only handle on \"not this endpoint\" is {@link CsrfOptions.ignorePaths}\n * — an exact pathname string that cannot express `/webhooks/:provider` and\n * keeps parsing after an `apiPrefix` change that voids it. A flag is declared\n * on the route itself:\n *\n * ```ts\n * const CsrfExempt = defineRouteFlag('csrf.exempt')\n *\n * @CsrfExempt\n * @Controller()\n * class WebhooksController {\n * @Post('/:provider') receive(ctx: RequestContext) {}\n * }\n * ```\n */\n exemptWhen?: RouteFlagTest\n}\n\n/**\n * Double-submit cookie CSRF protection as a `(ctx, next)` middleware.\n *\n * The ctx-style counterpart of {@link csrf}: it runs inside the matched route,\n * so it can read `ctx.route.flags` and be exempted per route, and it works on\n * every runtime including the `@forinda/kickjs/web` fetch entry.\n *\n * ```ts\n * // per controller — the flag-aware form\n * @Middleware(csrfGuard({ exemptWhen: 'csrf.exempt' }))\n * @Controller()\n * class BillingController {}\n *\n * // edge, app-wide\n * createWebApp({ h3, modules, middleware: [csrfGuard()] })\n * ```\n *\n * Mount `csrf()` instead when you want one app-wide connect middleware that\n * also covers requests matching no route — nothing to exempt, nothing to read.\n */\nexport function csrfGuard(options: CsrfGuardOptions = {}): MiddlewareHandler {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const exemptWhen = options.exemptWhen\n if (exemptWhen !== undefined) assertFlagTest(exemptWhen, 'csrfGuard({ exemptWhen })')\n const cookieOpts = {\n // Same default as `csrf()` above, and for the same reason.\n httpOnly: false,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (ctx: RequestContext, next: () => void): void => {\n const cookies = readCookies(ctx.req)\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomHex(tokenLength)\n setCookie(ctx.res, cookieName, token, cookieOpts)\n }\n\n // Safe methods only ever issue the token.\n const method = ctx.route?.method ?? (ctx.req as { method?: string }).method ?? 'GET'\n if (!protectedMethods.has(method.toUpperCase())) return next()\n\n // Flag first: it is the declared exemption, and cheaper than the path set.\n if (exemptWhen !== undefined && matchesFlagTest(exemptWhen, ctx.route?.flags, ctx.route)) {\n return next()\n }\n\n if (ignorePaths.has(resolvePathname(ctx.req as unknown as ClientRequestLike))) return next()\n\n const headerToken = ctx.headers[headerName] as string | undefined\n if (!headerToken || headerToken !== token) {\n ctx.json({ message: 'CSRF token mismatch' }, 403)\n return\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;6NAOA,SAAgB,UAAU,MAAuB,CAC/C,IAAM,IAAM,OAAO,gBAAgB,IAAI,WAAW,KAAK,CAAC,EACxD,OAAO,MAAM,KAAK,IAAM,GAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CACxE,CCiDA,SAAgB,KAAK,QAAuB,CAAC,EAAG,CAC9C,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,QAAQ,EAAA,CAAG,IAAK,GAAM,EAAE,YAAY,CAAC,CACpF,EACM,YAAc,IAAI,IAAI,QAAQ,aAAe,CAAC,CAAC,EAC/C,YAAc,QAAQ,aAAe,GACrC,WAAa,CAQjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,aACb,EAEA,OAAQ,IAAc,IAAe,OAAuB,CAM1D,IAAI,MADY,YAAY,GACV,CAAC,CAAC,YAcpB,GAZK,QACH,MAAQ,UAAU,WAAW,EAC7B,UAAU,IAAK,WAAY,MAAO,UAAU,GAI1C,CAAC,iBAAiB,IAAI,IAAI,OAAO,YAAY,CAAC,GAM9C,YAAY,IAAI,gBAAgB,GAAwB,CAAC,EAC3D,OAAO,KAAK,EAId,IAAM,YAAc,IAAI,QAAQ,YAEhC,GAAI,CAAC,aAAe,cAAgB,MAClC,OAAO,SAAS,IAAK,IAAK,CACxB,QAAS,qBACX,CAAC,EAGH,KAAK,CACP,CACF,CA8CA,SAAgB,UAAU,QAA4B,CAAC,EAAsB,CAC3E,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,QAAQ,EAAA,CAAG,IAAK,GAAM,EAAE,YAAY,CAAC,CACpF,EACM,YAAc,IAAI,IAAI,QAAQ,aAAe,CAAC,CAAC,EAC/C,YAAc,QAAQ,aAAe,GACrC,WAAa,QAAQ,WACvB,aAAe,IAAA,IAAW,eAAe,WAAY,2BAA2B,EACpF,IAAM,WAAa,CAEjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,aACb,EAEA,OAAQ,IAAqB,OAA2B,CAEtD,IAAI,MADY,YAAY,IAAI,GACd,CAAC,CAAC,YAEf,QACH,MAAQ,UAAU,WAAW,EAC7B,UAAU,IAAI,IAAK,WAAY,MAAO,UAAU,GAIlD,IAAM,OAAS,IAAI,OAAO,QAAW,IAAI,IAA4B,QAAU,MAQ/E,GAPI,CAAC,iBAAiB,IAAI,OAAO,YAAY,CAAC,GAG1C,aAAe,IAAA,IAAa,gBAAgB,WAAY,IAAI,OAAO,MAAO,IAAI,KAAK,GAInF,YAAY,IAAI,gBAAgB,IAAI,GAAmC,CAAC,EAAG,OAAO,KAAK,EAE3F,IAAM,YAAc,IAAI,QAAQ,YAC5B,CAAC,aAAe,cAAgB,MAClC,IAAI,KAAK,CAAE,QAAS,qBAAsB,EAAG,GAAG,EAIlD,KAAK,CACP,CACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v10.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 mergeConfig=(defaults={},overrides={})=>({...defaults,...overrides}),composeName=(base,scope)=>`${base}:${scope}`;function defineAdapter(options){let buildSync=(instanceName,scoped,overrides)=>{let config=mergeConfig(options.defaults,overrides),built=options.build(config,{name:instanceName,scoped});return built.name=instanceName,built},buildAsync=(instanceName,opts)=>{let inner;return{name:instanceName,async beforeStart(ctx){let deps=(opts.inject??[]).map(tok=>ctx.container.resolve(tok)),config=await opts.useFactory(...deps);inner=buildSync(instanceName,!1,config),await inner.beforeStart?.(ctx)},async afterStart(ctx){await inner?.afterStart?.(ctx)},async shutdown(){await inner?.shutdown?.()},async onHealthCheck(){return await inner?.onHealthCheck?.()??{name:instanceName,status:`up`}}}},factory=(config=>buildSync(options.name,!1,config));return factory.scoped=(scopeName,config)=>buildSync(composeName(options.name,scopeName),!0,config),factory.async=opts=>buildAsync(options.name,opts),Object.defineProperty(factory,"definition",{value:Object.freeze({...options})}),factory}export{defineAdapter as t};
|
|
12
|
-
//# sourceMappingURL=define-adapter-
|
|
12
|
+
//# sourceMappingURL=define-adapter-BLKlUbSL.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-adapter-
|
|
1
|
+
{"version":3,"file":"define-adapter-BLKlUbSL.mjs","names":[],"sources":["../src/core/define-adapter.ts"],"sourcesContent":["import type { AppAdapter, AdapterContext } from './adapter'\nimport type { BuildContext } from './define-plugin'\n\n/**\n * Options passed to {@link defineAdapter}. Mirrors {@link DefinePluginOptions}\n * exactly — see `architecture.md` §21.3.4 for the definePlugin/defineAdapter\n * symmetry rationale.\n *\n * The optional `TExtra` generic lets an adapter ship public methods\n * beyond the standard {@link AppAdapter} contract — they're preserved\n * on the returned adapter instance so external callers (tests, peer\n * adapters) can invoke them directly. See `OtelAdapter.applyRedaction`\n * for the canonical use case.\n */\nexport interface DefineAdapterOptions<TConfig, TExtra = unknown> {\n /**\n * Stable identity used for logging, `dependsOn` lookups, and `.scoped()`\n * namespacing. Required for adapters that participate in `dependsOn`\n * sort — error messages reference adapters by name.\n */\n name: string\n version?: string\n requires?: { kickjs?: string }\n defaults?: Partial<TConfig>\n build(config: TConfig, ctx: BuildContext): Omit<AppAdapter, 'name'> & TExtra\n}\n\n/**\n * Async-config form of an adapter. Identical shape to `PluginAsyncOptions`\n * but resolved during `beforeStart` (the earliest async hook in\n * `AppAdapter`) — meaning `middleware()` and `contributors()` from the inner\n * adapter are **not** picked up. Use the bare/`.scoped()` form when you need\n * those.\n */\nexport interface AdapterAsyncOptions<TConfig> {\n inject?: ReadonlyArray<unknown>\n useFactory(...deps: any[]): TConfig | Promise<TConfig>\n}\n\n/**\n * Factory returned by {@link defineAdapter}. Same surface as\n * {@link PluginFactory} (call / `.scoped()` / `.async()`) so adopters learn\n * one mental model for both primitives. Returned instances carry both\n * the standard {@link AppAdapter} contract and any `TExtra` extension\n * methods the `build` function exposed.\n */\nexport interface AdapterFactory<TConfig, TExtra = unknown> {\n (config?: Partial<TConfig>): AppAdapter & TExtra\n scoped(scopeName: string, config?: Partial<TConfig>): AppAdapter & TExtra\n async(opts: AdapterAsyncOptions<TConfig>): AppAdapter\n readonly definition: Readonly<DefineAdapterOptions<TConfig, TExtra>>\n}\n\nconst mergeConfig = <TConfig>(\n defaults: Partial<TConfig> = {},\n overrides: Partial<TConfig> = {},\n): TConfig => ({ ...defaults, ...overrides }) as TConfig\n\nconst composeName = (base: string, scope: string): string => `${base}:${scope}`\n\n/**\n * Build an {@link AppAdapter} factory from a typed definition. See\n * `architecture.md` §21.3.4 for migration guidance — most first-party\n * adapter packages collapse to a single `defineAdapter()` call after this\n * lands.\n *\n * @example\n * ```ts\n * const TenantAdapter = defineAdapter<TenantConfig>({\n * name: 'TenantAdapter',\n * defaults: { strategy: 'header', required: true },\n * build: (config) => ({\n * middleware: () => [tenantResolverMiddleware(config)],\n * beforeStart: ({ container }) => registerTenantFactory(container, config),\n * }),\n * })\n *\n * bootstrap({\n * adapters: [\n * TenantAdapter({ strategy: 'subdomain' }),\n * TenantAdapter.scoped('shard-eu', { headerName: 'x-eu-tenant' }),\n * ],\n * })\n * ```\n */\nexport function defineAdapter<TConfig = Record<string, unknown>, TExtra = unknown>(\n options: DefineAdapterOptions<TConfig, TExtra>,\n): AdapterFactory<TConfig, TExtra> {\n const buildSync = (\n instanceName: string,\n scoped: boolean,\n overrides?: Partial<TConfig>,\n ): AppAdapter & TExtra => {\n const config = mergeConfig(options.defaults, overrides)\n // Mutate `name` on the build result instead of spreading — spread\n // strips prototype methods when `build()` returns a class instance.\n // Extension methods declared by the build function (`TExtra`) survive\n // the mutation since they're own properties on the build result.\n const built = options.build(config, { name: instanceName, scoped }) as AppAdapter & TExtra\n ;(built as AppAdapter).name = instanceName\n return built\n }\n\n const buildAsync = (instanceName: string, opts: AdapterAsyncOptions<TConfig>): AppAdapter => {\n let inner: AppAdapter | undefined\n\n return {\n name: instanceName,\n async beforeStart(ctx: AdapterContext) {\n const deps = (opts.inject ?? []).map((tok) => ctx.container.resolve(tok as never))\n const config = await opts.useFactory(...deps)\n inner = buildSync(instanceName, false, config as Partial<TConfig>)\n await inner.beforeStart?.(ctx)\n },\n async afterStart(ctx: AdapterContext) {\n await inner?.afterStart?.(ctx)\n },\n async shutdown() {\n await inner?.shutdown?.()\n },\n async onHealthCheck() {\n return (await inner?.onHealthCheck?.()) ?? { name: instanceName, status: 'up' }\n },\n }\n }\n\n const factory = ((config?: Partial<TConfig>) =>\n buildSync(options.name, false, config)) as AdapterFactory<TConfig, TExtra>\n\n factory.scoped = (scopeName: string, config?: Partial<TConfig>) =>\n buildSync(composeName(options.name, scopeName), true, config)\n\n factory.async = (opts: AdapterAsyncOptions<TConfig>) => buildAsync(options.name, opts)\n\n Object.defineProperty(factory, 'definition', { value: Object.freeze({ ...options }) })\n\n return factory\n}\n"],"mappings":";;;;;;;;;;AAqDA,MAAM,aACJ,SAA6B,CAAC,EAC9B,UAA8B,CAAC,KAClB,CAAE,GAAG,SAAU,GAAG,SAAU,GAErC,aAAe,KAAc,QAA0B,GAAG,KAAK,GAAG,QA2BxE,SAAgB,cACd,QACiC,CACjC,IAAM,WACJ,aACA,OACA,YACwB,CACxB,IAAM,OAAS,YAAY,QAAQ,SAAU,SAAS,EAKhD,MAAQ,QAAQ,MAAM,OAAQ,CAAE,KAAM,aAAc,MAAO,CAAC,EAElE,MADC,OAAsB,KAAO,aACvB,KACT,EAEM,YAAc,aAAsB,OAAmD,CAC3F,IAAI,MAEJ,MAAO,CACL,KAAM,aACN,MAAM,YAAY,IAAqB,CACrC,IAAM,MAAQ,KAAK,QAAU,CAAC,EAAA,CAAG,IAAK,KAAQ,IAAI,UAAU,QAAQ,GAAY,CAAC,EAC3E,OAAS,MAAM,KAAK,WAAW,GAAG,IAAI,EAC5C,MAAQ,UAAU,aAAc,GAAO,MAA0B,EACjE,MAAM,MAAM,cAAc,GAAG,CAC/B,EACA,MAAM,WAAW,IAAqB,CACpC,MAAM,OAAO,aAAa,GAAG,CAC/B,EACA,MAAM,UAAW,CACf,MAAM,OAAO,WAAW,CAC1B,EACA,MAAM,eAAgB,CACpB,OAAQ,MAAM,OAAO,gBAAgB,GAAM,CAAE,KAAM,aAAc,OAAQ,IAAK,CAChF,CACF,CACF,EAEM,SAAY,QAChB,UAAU,QAAQ,KAAM,GAAO,MAAM,GASvC,MAPA,SAAQ,QAAU,UAAmB,SACnC,UAAU,YAAY,QAAQ,KAAM,SAAS,EAAG,GAAM,MAAM,EAE9D,QAAQ,MAAS,MAAuC,WAAW,QAAQ,KAAM,IAAI,EAErF,OAAO,eAAe,QAAS,aAAc,CAAE,MAAO,OAAO,OAAO,CAAE,GAAG,OAAQ,CAAC,CAAE,CAAC,EAE9E,OACT"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs v10.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
|
+
const disposables=/* @__PURE__ */ new Set;function registerDisposable(fn){return disposables.add(fn),()=>disposables.delete(fn)}function drainDisposables(){let pending=[...disposables];return disposables.clear(),pending}async function runDisposables(list){await Promise.allSettled(list.map(async fn=>fn()))}async function disposeAll(){await runDisposables(drainDisposables())}export{runDisposables as i,drainDisposables as n,registerDisposable as r,disposeAll as t};
|
|
12
|
+
//# sourceMappingURL=disposables-CuY2S3bw.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"disposables-
|
|
1
|
+
{"version":3,"file":"disposables-CuY2S3bw.mjs","names":[],"sources":["../src/core/disposables.ts"],"sourcesContent":["/**\n * Registry for framework-owned resources that hold timers or other handles\n * outside the adapter/plugin lifecycle — e.g. the in-memory session and\n * rate-limit stores' cleanup intervals. `Application.shutdown()` drains it so\n * startup and shutdown stay symmetric even for resources created lazily by\n * middleware factories.\n *\n * Intervals are `.unref()`'d so they never wedge process exit — this registry\n * exists for IN-PROCESS teardown (test suites, HMR, multi-app hosts) where\n * each un-disposed store would otherwise leak a live interval + backing Map\n * per app instance.\n */\n\nexport type Disposable = () => void | Promise<void>\n\nconst disposables = new Set<Disposable>()\n\n/** Register a cleanup callback. Returns an unregister function. */\nexport function registerDisposable(fn: Disposable): () => void {\n disposables.add(fn)\n return () => disposables.delete(fn)\n}\n\n/**\n * Remove and return all currently registered disposables WITHOUT running\n * them. Used by HMR rebuild to snapshot the outgoing app's resources before\n * `setup()` registers the incoming app's — so the stale set can be disposed\n * on success, or restored on failure, without touching the new one.\n */\nexport function drainDisposables(): Disposable[] {\n const pending = [...disposables]\n disposables.clear()\n return pending\n}\n\n/**\n * Run a list of disposables. Errors — including SYNCHRONOUS throws — are\n * swallowed per-entry: the `async` callback converts a sync throw into a\n * rejection that `allSettled` absorbs, so one bad disposable can't abort\n * the rest (a bare `Promise.resolve(fn())` would let the throw escape the\n * `.map()` before `allSettled` ever ran).\n */\nexport async function runDisposables(list: Disposable[]): Promise<void> {\n await Promise.allSettled(list.map(async (fn) => fn()))\n}\n\n/** Run and clear all registered disposables. Errors are swallowed per-entry. */\nexport async function disposeAll(): Promise<void> {\n await runDisposables(drainDisposables())\n}\n"],"mappings":";;;;;;;;;;AAeA,MAAM,4BAAc,IAAI,IAGxB,SAAgB,mBAAmB,GAA4B,CAE7D,OADA,YAAY,IAAI,EAAE,MACL,YAAY,OAAO,EAAE,CACpC,CAQA,SAAgB,kBAAiC,CAC/C,IAAM,QAAU,CAAC,GAAG,WAAW,EAE/B,OADA,YAAY,MAAM,EACX,OACT,CASA,eAAsB,eAAe,KAAmC,CACtE,MAAM,QAAQ,WAAW,KAAK,IAAI,KAAO,KAAO,GAAG,CAAC,CAAC,CACvD,CAGA,eAAsB,YAA4B,CAChD,MAAM,eAAe,iBAAiB,CAAC,CACzC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs v10.1.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Felix Orinda
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
import{t as Container}from"./container-CS9v9UFU.mjs";import{createRequire}from"node:module";import{existsSync}from"node:fs";import{resolve}from"node:path";import{detectSchema}from"@forinda/kickjs-schema";var __require=/* @__PURE__ */ createRequire(import.meta.url);let _zod;function getZod(){if(_zod)return _zod;try{_zod=__require(`zod`)}catch{throw Error("@forinda/kickjs: the 'zod' peer dependency is required for baseEnvSchema / defineEnv / loadEnv but is not installed. Install it (`pnpm add zod`), or define your env with a non-Zod schema and call loadEnvFromSchema() instead — @forinda/kickjs-schema supports Valibot, Yup, and any Standard Schema validator without zod.")}return _zod}function isTestRun(){return process.env.NODE_ENV===`test`||!!process.env.VITEST}function envMode(){return isTestRun()?`test`:process.env.NODE_ENV??`development`}const GENERIC_ENV_FILES=[`.env.local`,`.env`];function resolveEnvFiles(){let explicit=process.env.KICKJS_ENV_FILE;if(explicit===`off`||explicit===`none`)return null;if(explicit)return explicit.split(`,`).map(f=>f.trim()).filter(Boolean);let exists=f=>existsSync(resolve(process.cwd(),f)),mode=envMode(),modeFiles=[`.env.${mode}.local`,`.env.${mode}`],found=(mode===`test`&&modeFiles.some(exists)?modeFiles:[...modeFiles,...GENERIC_ENV_FILES]).filter(exists);return found.length>0?found:null}let warnedTestBackfill=!1;function appliedKeys(before,parsed){return Object.keys(parsed).filter(k=>process.env[k]!==before[k])}function tryLoadDotenv(override=!1){let files=resolveEnvFiles();if(files)try{let dotenv=__require(`dotenv`),path=override?files.toReversed():files,usedGeneric=files.some(f=>GENERIC_ENV_FILES.includes(f)),mayWarn=!warnedTestBackfill&&isTestRun()&&usedGeneric,before=mayWarn?{...process.env}:void 0,result=dotenv.config({path,override,quiet:!0});if(mayWarn&&result.parsed){let applied=appliedKeys(before,result.parsed);applied.length>0&&(warnedTestBackfill=!0,console.warn(`[kickjs] test run took ${applied.length} env var(s) from ${files.join(`, `)} (${applied.slice(0,5).join(`, `)}${applied.length>5?`, …`:``}). Create a .env.test to isolate the suite, or set KICKJS_ENV_FILE=off.`))}}catch{}}tryLoadDotenv();function buildBaseEnvSchema(){let z=getZod();return z.object({PORT:z.coerce.number().default(3e3),NODE_ENV:z.enum([`development`,`production`,`test`]).default(`development`),LOG_LEVEL:z.string().default(`info`)})}let _baseEnvSchema;function getBaseEnvSchema(){return _baseEnvSchema||=buildBaseEnvSchema(),_baseEnvSchema}const baseEnvSchema=new Proxy({},{get(_t,prop){let real=getBaseEnvSchema(),value=real[prop];return typeof value==`function`?value.bind(real):value},has(_t,prop){return prop in getBaseEnvSchema()}});let cachedEnv=null,envOverlay=null;function overlaid(env){return envOverlay?{...env,...envOverlay}:env}function resolveEnvKey(key){return envOverlay&&key in envOverlay?envOverlay[key]:cachedEnv?.[key]}let cachedSchema=null;function defineEnv(extend){let base=getBaseEnvSchema(),userSchema=extend(base);return base.extend(userSchema.shape)}function loadEnv(schema){let s=schema??cachedSchema??getBaseEnvSchema();return cachedEnv&&cachedSchema===s||(cachedSchema=s,cachedEnv=s.parse(process.env),Container._envResolver=resolveEnvKey),overlaid(cachedEnv)}function getEnv(key,fallback){return isZodObject(fallback)?(warnedSchemaArg||(warnedSchemaArg=!0,process.emitWarning(`getEnv(key, schema) is deprecated: register the schema once with loadEnv(envSchema); the second argument is now a fallback value.`,`DeprecationWarning`)),loadEnv(fallback)[key]):loadEnv()[key]??fallback}let warnedSchemaArg=!1;function isZodObject(value){return typeof value==`object`&&!!value&&typeof value.safeParse==`function`&&`shape`in value}async function withEnv(overrides,fn){loadEnv();let previous=envOverlay;envOverlay={...previous,...overrides};try{return await fn()}finally{envOverlay=previous}}function reloadEnv(){tryLoadDotenv(!0),cachedEnv=null,cachedSchema&&(typeof cachedSchema.parse==`function`?loadEnv(cachedSchema):loadEnvFromSchema(cachedSchema))}function resetEnvCache(){cachedEnv=null,cachedSchema=null,warnedTestBackfill=!1,Container._envResolver=null}function loadEnvFromSchema(schema){let result=detectSchema(schema).safeParse(process.env);if(!result.success){let details=result.issues.map(i=>` ${i.path.join(`.`)||`(root)`}: ${i.message}`).join(`
|
|
12
|
+
`);throw Error(`Environment validation failed:\n${details}`)}return cachedEnv=result.data,cachedSchema=schema,Container._envResolver=resolveEnvKey,overlaid(result.data)}export{loadEnvFromSchema as a,withEnv as c,loadEnv as i,__require as l,defineEnv as n,reloadEnv as o,getEnv as r,resetEnvCache as s,baseEnvSchema as t};
|
|
13
|
+
//# sourceMappingURL=env-BwzF9MkZ.mjs.map
|