@forinda/kickjs 10.0.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-CvEm8Gpp.d.mts → adapter-ClEh-Oos.d.mts} +2 -2
- package/dist/{adapter-CvEm8Gpp.d.mts.map → adapter-ClEh-Oos.d.mts.map} +1 -1
- package/dist/{application-D21ZVAWG.mjs → application-Dv514EeR.mjs} +3 -3
- package/dist/{application-D21ZVAWG.mjs.map → application-Dv514EeR.mjs.map} +1 -1
- package/dist/{application-1Hln746x.d.mts → application-oIPzgjfk.d.mts} +20 -9
- 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-BkLX8wXf.mjs → client-ip-BbLDp03S.mjs} +2 -2
- package/dist/{client-ip-BkLX8wXf.mjs.map → client-ip-BbLDp03S.mjs.map} +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-rXsEm1rn.mjs → config-CJiGeOca.mjs} +3 -3
- package/dist/{config-rXsEm1rn.mjs.map → config-CJiGeOca.mjs.map} +1 -1
- package/dist/{container-CsCdem73.mjs → container-CS9v9UFU.mjs} +3 -3
- package/dist/{container-CsCdem73.mjs.map → container-CS9v9UFU.mjs.map} +1 -1
- package/dist/{context-Bd02q5Sm.mjs → context-CtL9RxXp.mjs} +3 -3
- package/dist/{context-Bd02q5Sm.mjs.map → context-CtL9RxXp.mjs.map} +1 -1
- package/dist/{context-errors-BUdMBVWO.mjs → context-errors-7C5TBYn7.mjs} +2 -2
- package/dist/{context-errors-BUdMBVWO.mjs.map → context-errors-7C5TBYn7.mjs.map} +1 -1
- package/dist/{contributor-runner-CZTAFvla.mjs → contributor-runner-DBpRR5dl.mjs} +3 -3
- package/dist/{contributor-runner-CZTAFvla.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 +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/errors.mjs +1 -1
- package/dist/core/interfaces.mjs +1 -1
- package/dist/core/logger.mjs +1 -1
- package/dist/core/path.mjs +1 -1
- package/dist/core/plugin.d.mts +1 -1
- package/dist/core/plugin.mjs +1 -1
- package/dist/core/reactivity.mjs +1 -1
- package/dist/{core-B0mErNE1.mjs → core-B0JmdBvX.mjs} +3 -3
- package/dist/{core-B0mErNE1.mjs.map → core-B0JmdBvX.mjs.map} +1 -1
- package/dist/{csrf-BnetlmWY.mjs → csrf--1t4qDov.mjs} +3 -3
- package/dist/{csrf-BnetlmWY.mjs.map → csrf--1t4qDov.mjs.map} +1 -1
- package/dist/{decorate-B7-z930n.mjs → decorate-BHkdkV4l.mjs} +1 -1
- package/dist/{define-adapter-B4D_Rci1.mjs → define-adapter-BLKlUbSL.mjs} +2 -2
- package/dist/{define-adapter-B4D_Rci1.mjs.map → define-adapter-BLKlUbSL.mjs.map} +1 -1
- package/dist/{disposables-CgQqGmPL.mjs → disposables-CuY2S3bw.mjs} +2 -2
- package/dist/{disposables-CgQqGmPL.mjs.map → disposables-CuY2S3bw.mjs.map} +1 -1
- package/dist/{env-ngWmBB9d.mjs → env-BwzF9MkZ.mjs} +3 -3
- package/dist/{env-ngWmBB9d.mjs.map → env-BwzF9MkZ.mjs.map} +1 -1
- package/dist/{error-handler-Chw07m-M.d.mts → error-handler-BB9wAElm.d.mts} +2 -2
- package/dist/{error-handler-Chw07m-M.d.mts.map → error-handler-BB9wAElm.d.mts.map} +1 -1
- package/dist/{handler-CyRoAqNt.mjs → handler-zKfIgNo4.mjs} +3 -3
- package/dist/{handler-CyRoAqNt.mjs.map → handler-zKfIgNo4.mjs.map} +1 -1
- 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 +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/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/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/query/index.mjs +2 -2
- package/dist/http/router-builder.d.mts +1 -1
- package/dist/http/router-builder.mjs +2 -2
- package/dist/http/runtimes/fastify.d.mts +1 -1
- package/dist/http/runtimes/fastify.mjs +2 -2
- package/dist/http/runtimes/h3-web.mjs +1 -1
- package/dist/http/runtimes/h3.d.mts +1 -1
- 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 +7 -7
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{jobs-Ccs6PkmC.mjs → jobs-BQ4t3TAb.mjs} +3 -3
- package/dist/{jobs-Ccs6PkmC.mjs.map → jobs-BQ4t3TAb.mjs.map} +1 -1
- package/dist/{kv-stores-BYeO77zK.d.mts → kv-stores-Co5k2PYA.d.mts} +3 -3
- package/dist/{kv-stores-BYeO77zK.d.mts.map → kv-stores-Co5k2PYA.d.mts.map} +1 -1
- package/dist/{kv-stores-9IkW7Rjj.mjs → kv-stores-DsfVy2fk.mjs} +3 -3
- package/dist/{kv-stores-9IkW7Rjj.mjs.map → kv-stores-DsfVy2fk.mjs.map} +1 -1
- package/dist/{metadata-DCWKG472.mjs → metadata-zvd6AWkE.mjs} +2 -2
- package/dist/{metadata-DCWKG472.mjs.map → metadata-zvd6AWkE.mjs.map} +1 -1
- package/dist/{module-registry-Cl12vSJd.mjs → module-registry-Daf_P2oD.mjs} +2 -2
- package/dist/{module-registry-Cl12vSJd.mjs.map → module-registry-Daf_P2oD.mjs.map} +1 -1
- package/dist/{observers-9FRE8VCB.mjs → observers-DZgs48UM.mjs} +2 -2
- package/dist/{observers-9FRE8VCB.mjs.map → observers-DZgs48UM.mjs.map} +1 -1
- package/dist/{query-DUkboS0j.mjs → query-B_k9qn0R.mjs} +2 -2
- package/dist/{query-DUkboS0j.mjs.map → query-B_k9qn0R.mjs.map} +1 -1
- package/dist/{reply-DElkObMy.mjs → reply-BDK9xt7Z.mjs} +3 -3
- package/dist/{reply-DElkObMy.mjs.map → reply-BDK9xt7Z.mjs.map} +1 -1
- package/dist/{respond-Dcj1NF_X.mjs → respond-DFRj2-7q.mjs} +3 -3
- package/dist/{respond-Dcj1NF_X.mjs.map → respond-DFRj2-7q.mjs.map} +1 -1
- package/dist/{route-flag-jd8PDm20.mjs → route-flag-BWuvJsGZ.mjs} +3 -3
- package/dist/{route-flag-jd8PDm20.mjs.map → route-flag-BWuvJsGZ.mjs.map} +1 -1
- package/dist/{route-policy-DlunIjGc.mjs → route-policy-B3vM6bSf.mjs} +2 -2
- package/dist/{route-policy-DlunIjGc.mjs.map → route-policy-B3vM6bSf.mjs.map} +1 -1
- package/dist/{router-builder-DZsCjpeV.mjs → router-builder-JvMTp50k.mjs} +3 -3
- package/dist/{router-builder-DZsCjpeV.mjs.map → router-builder-JvMTp50k.mjs.map} +1 -1
- package/dist/{router-builder-D_SkWTfE.d.mts → router-builder-zWuVIOmd.d.mts} +2 -2
- package/dist/{router-builder-D_SkWTfE.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-DL_jrBGk.d.mts → upload-CiMtdXWD.d.mts} +2 -2
- package/dist/{upload-DL_jrBGk.d.mts.map → upload-CiMtdXWD.d.mts.map} +1 -1
- package/dist/{upload-config-BiL1QXkT.mjs → upload-config-Dz2INCaa.mjs} +2 -2
- package/dist/{upload-config-BiL1QXkT.mjs.map → upload-config-Dz2INCaa.mjs.map} +1 -1
- package/dist/web.d.mts +2 -2
- package/dist/web.mjs +2 -2
- package/package.json +1 -1
- package/dist/application-1Hln746x.d.mts.map +0 -1
- package/dist/bootstrap-2hi7Zhs8.mjs +0 -12
- package/dist/bootstrap-2hi7Zhs8.mjs.map +0 -1
- package/dist/session-4Tb6q0Ny.d.mts.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v10.
|
|
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{METADATA}from"./core/interfaces.mjs";import{a as getMethodMeta,c as pushClassMeta,l as pushMethodMeta,t as getClassMeta}from"./metadata-
|
|
12
|
-
//# sourceMappingURL=route-flag-
|
|
11
|
+
import{METADATA}from"./core/interfaces.mjs";import{a as getMethodMeta,c as pushClassMeta,l as pushMethodMeta,t as getClassMeta}from"./metadata-zvd6AWkE.mjs";const FLAG_UNSET=Symbol(`kick.flagUnset`);function isDecoratorApplication(args){let[target,propertyKey]=args;return typeof propertyKey==`string`||typeof propertyKey==`symbol`||args.length===1&&typeof target==`function`}function defineRouteFlag(name){if(name.startsWith(`!`))throw Error(`defineRouteFlag('${name}'): a flag name cannot start with '!' — that prefix is reserved for negated tests ('!${name.slice(1)}' means "does not carry ${name.slice(1)}"). Rename the flag.`);function apply(value,target,propertyKey){let declaration={name,value};propertyKey===void 0?pushClassMeta(METADATA.CLASS_FLAGS,target,declaration):pushMethodMeta(METADATA.METHOD_FLAGS,target.constructor,String(propertyKey),declaration)}let flag=(...args)=>{if(isDecoratorApplication(args)){apply(!0,args[0],args[1]);return}let value=args[0];return(target,propertyKey)=>{apply(value,target,propertyKey)}};return Object.defineProperty(flag,"flagName",{value:name,enumerable:!0}),Object.defineProperty(flag,"off",{value:(target,propertyKey)=>{apply(FLAG_UNSET,target,propertyKey)},enumerable:!0}),flag}function resolveRouteFlags(classDeclarations,methodDeclarations,mountDeclarations=[]){let resolved=/* @__PURE__ */ new Map;for(let{name,value}of[...mountDeclarations,...classDeclarations,...methodDeclarations])value===FLAG_UNSET?resolved.delete(name):resolved.set(name,value);return resolved}function matchesFlagTest(test,flags,route){let resolved=flags??EMPTY_FLAGS;if(typeof test==`function`)return test({flags:resolved,route});if(typeof test==`string`)return isNegated(test)?!resolved.has(stripNegation(test)):resolved.has(test);if(test.length===0)return!1;let negated=isNegated(test[0]);if(test.some(name=>isNegated(name)!==negated))throw Error(`Route flag test mixes polarities: [${test.map(n=>`'${n}'`).join(`, `)}]. A list is either all names ("carries any of these") or all negated ("carries none of these") — for anything else, pass a predicate.`);return negated?test.every(name=>!resolved.has(stripNegation(name))):test.some(name=>resolved.has(name))}function assertFlagTest(test,site){if(typeof test==`function`||typeof test==`string`||test.length===0)return;let negated=isNegated(test[0]);if(test.some(name=>isNegated(name)!==negated))throw Error(`${site}: route flag test mixes polarities: [${test.map(n=>`'${n}'`).join(`, `)}]. A list is either all names ("carries any of these") or all negated ("carries none of these") — for anything else, pass a predicate.`)}const isNegated=name=>name.startsWith(`!`),stripNegation=name=>name.slice(1),EMPTY_FLAGS=/* @__PURE__ */ new Map,ROUTE_SLOT=Symbol.for(`kick.route`);function getRouteFlags(controllerClass,handlerName){return resolveRouteFlags(getClassFlagDeclarations(controllerClass),getMethodFlagDeclarations(controllerClass,handlerName),getMountFlagDeclarations(controllerClass))}function getClassFlagDeclarations(controllerClass){return getClassMeta(METADATA.CLASS_FLAGS,controllerClass,[])}function getMethodFlagDeclarations(controllerClass,handlerName){return getMethodMeta(METADATA.METHOD_FLAGS,controllerClass,handlerName,[])}function toFlagDeclarations(flags,site){return flags?(Array.isArray(flags)?flags.map(name=>[name,!0]):Object.entries(flags)).map(([name,value])=>{if(name.startsWith(`!`))throw Error(`${site}: a flag name cannot start with '!' — that prefix is reserved for negated tests, and a declaration has no negative form. Remove the flag instead of declaring '${name}'.`);return{name,value}}):[]}const MOUNT_FLAGS=/* @__PURE__ */ new WeakMap;function registerMountFlags(controllerClass,declarations){declarations.length===0?MOUNT_FLAGS.delete(controllerClass):MOUNT_FLAGS.set(controllerClass,[...declarations])}function getMountFlagDeclarations(controllerClass){return MOUNT_FLAGS.get(controllerClass)??[]}export{getMethodFlagDeclarations as a,registerMountFlags as c,getClassFlagDeclarations as i,resolveRouteFlags as l,assertFlagTest as n,getRouteFlags as o,defineRouteFlag as r,matchesFlagTest as s,ROUTE_SLOT as t,toFlagDeclarations as u};
|
|
12
|
+
//# sourceMappingURL=route-flag-BWuvJsGZ.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-flag-jd8PDm20.mjs","names":[],"sources":["../src/core/route-flag.ts"],"sourcesContent":["/**\n * Route flags — a named, inheritable fact about a route.\n *\n * A flag carries no behaviour. It records something about the route that any\n * consumer may read: `auth.public`, `csrf.exempt`, `rate.limit`. Auth reads it\n * to skip a contributor, a guard reads it to return early, Swagger reads it to\n * mark a security scheme — none of them needs to know about the others.\n *\n * The rule that makes presence checks safe: a flag resolves to **absent**, or\n * **present with a value**. `@Public.off` on a method removes what the class\n * set, rather than storing something falsy — so `flags.has(name)` is always the\n * right question for a boolean flag. Without that, `has('auth.public')` could\n * answer `true` for the route that just opted back IN, which is an\n * authorization bypass rather than a papercut.\n */\nimport { METADATA } from './interfaces'\nimport { getClassMeta, getMethodMeta, pushClassMeta, pushMethodMeta } from './metadata'\n\n/**\n * Augmentable route-flag registry — the flag counterpart of `ContextMeta`.\n *\n * Declare a project's flags here and every use of them narrows: a misspelt\n * name is a compile error rather than a flag that silently never matches, and\n * `flags.get('rate.limit')` comes back typed instead of `unknown`.\n *\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface KickRouteFlags {\n * 'auth.public': true\n * 'rate.limit': { rpm: number }\n * }\n * }\n * ```\n *\n * The interface is empty by default, and everything below falls back to plain\n * `string` while it stays that way — a project that has not augmented anything\n * keeps compiling exactly as before. The narrowing switches on with the first\n * declaration.\n *\n * The framework itself declares nothing here: it ships the mechanism and names\n * no flags, so `auth.public` above is a name you chose.\n *\n * Two constraints on what you can declare:\n * - A name cannot start with `!` — that prefix means \"does not carry this flag\"\n * in a {@link RouteFlagTest}, and `defineRouteFlag` rejects it.\n * - Removal is `@Flag.off`, not `@Flag(false)` — so `false` stays usable as a\n * real value for a flag declared `boolean`.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\nexport interface KickRouteFlags {}\n\n/**\n * A flag name. Narrows to the declared keys once {@link KickRouteFlags} is\n * augmented, and stays `string` until then — the same\n * `[Known] extends [never]` fallback `ContextMetaKey` uses, for the same\n * reason: first-day code must keep compiling.\n */\nexport type RouteFlagName = [keyof KickRouteFlags] extends [never]\n ? string\n : keyof KickRouteFlags & string\n\n/**\n * The value type declared for a flag, defaulting to `true` for a bare flag and\n * `unknown` for a name the registry does not know.\n */\nexport type RouteFlagValue<K extends string, Fallback = unknown> = K extends keyof KickRouteFlags\n ? KickRouteFlags[K]\n : Fallback\n\n/**\n * Marks a declaration as \"remove this flag\" during resolution.\n *\n * A dedicated sentinel rather than `false`: `false` is a perfectly good flag\n * VALUE (a registry entry typed `boolean`), and overloading it meant\n * `@Enabled(false)` deleted the flag while `flags.get('enabled')` could never\n * return `false`. With the sentinel, `false` stores like any other value and\n * removal is spelled `@Enabled.off`.\n *\n * Module-private and NOT `Symbol.for`: a registry-global symbol is reachable\n * by application code, so a flag declared with a `symbol` value could be handed\n * this exact sentinel and be deleted instead of stored. It is created and\n * consumed in this module, so it never needs to cross a realm.\n */\nconst FLAG_UNSET: unique symbol = Symbol('kick.flagUnset') as never\n\n/** One `@Flag` application, before precedence resolution. */\nexport interface RouteFlagDeclaration {\n readonly name: string\n /** `false` means \"remove whatever a lower-precedence site declared\". */\n readonly value: unknown\n}\n\n/**\n * A flag decorator. Usable bare (`@Public`) or called with a value\n * (`@Public(false)`, `@RateLimit({ rpm: 10 })`), on a class or a method.\n */\n/**\n * The bare application forms — `@Public` with no call.\n *\n * Split out so they can be withheld from a flag whose declared value is not\n * assignable from `true`: applying `@Limit` bare would store `true` while\n * `flags.get('rate.limit')` promises `{ rpm: number }`, and nothing would catch\n * the mismatch until a consumer read the wrong shape at runtime.\n */\nexport interface BareRouteFlag {\n /**\n * Bare on a class. Typed against `Function` rather than `object` so a flag\n * VALUE that happens to be an object (`@Limit({ rpm: 10 })`) cannot match\n * this signature: overload resolution would otherwise pick it, return\n * `void`, and fail with \"Type 'void' has no call signatures\".\n */\n (target: Function): void\n /**\n * Bare on a method. `propertyKey` is required for the same reason — it makes\n * a one-argument call unambiguous.\n */\n (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void\n}\n\n/** The called form — `@Limit({ rpm: 10 })` — plus removal via `.off`. */\nexport interface ValuedRouteFlag<V> {\n (value: V): (target: object, propertyKey?: string | symbol) => void\n /**\n * Remove the flag on this route — `@Public.off` on a method drops what the\n * controller set.\n *\n * Applied bare, like the flag itself. Spelled as its own member rather than\n * `@Public(false)` so that `false` stays available as a real value: a flag\n * declared `boolean` can store it, and `flags.get()` can return it.\n */\n readonly off: BareRouteFlag\n readonly flagName: string\n}\n\n/**\n * A flag decorator.\n *\n * Bare application is available only when `true` is a valid value for the flag\n * — which is every flag until {@link KickRouteFlags} declares a value type for\n * it. Declare `'rate.limit': { rpm: number }` and `@RateLimit` on its own stops\n * compiling, because the value it would store is not the value the registry\n * promises readers.\n */\nexport type RouteFlag<V = true> = true extends V\n ? BareRouteFlag & ValuedRouteFlag<V>\n : ValuedRouteFlag<V>\n\n/**\n * True when the arguments look like a decorator application rather than a\n * value. The two forms are `@Public` (TS calls it for us) and `@Public(v)`\n * (we return the decorator).\n *\n * Discrimination has to be exact, because a value can be an object:\n * `@RateLimit({ rpm: 10 })` is a single object argument, and treating it as a\n * class-decorator target silently drops the flag.\n *\n * - method decorator → `(prototype, 'name', descriptor?)`, so arg 1 is a\n * string or symbol\n * - class decorator → `(constructor)`, a single *function* argument\n * - anything else → a value\n */\nfunction isDecoratorApplication(args: unknown[]): boolean {\n const [target, propertyKey] = args\n if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') return true\n return args.length === 1 && typeof target === 'function'\n}\n\n/**\n * Define a route flag.\n *\n * ```ts\n * export const Public = defineRouteFlag('auth.public')\n * export const RateLimit = defineRouteFlag<{ rpm: number }>('rate.limit')\n * ```\n *\n * Once {@link KickRouteFlags} declares the name, the value type comes from the\n * registry and the explicit generic is redundant:\n *\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface KickRouteFlags {\n * 'rate.limit': { rpm: number }\n * }\n * }\n *\n * const RateLimit = defineRouteFlag('rate.limit') // RouteFlag<{ rpm: number }>\n * ```\n */\nexport function defineRouteFlag<const N extends RouteFlagName>(\n name: N,\n): RouteFlag<RouteFlagValue<N, true>>\nexport function defineRouteFlag<V>(name: RouteFlagName): RouteFlag<V>\nexport function defineRouteFlag(name: string): RouteFlag<never> {\n // `!` is reserved: a test string starting with it means \"does NOT carry this\n // flag\", so a flag literally named '!x' would be indistinguishable from the\n // negation of 'x' — `exemptWhen: '!x'` would silently stop meaning what the\n // author wrote. Rejecting at definition is the only point where the two can\n // still be told apart.\n if (name.startsWith('!')) {\n throw new Error(\n `defineRouteFlag('${name}'): a flag name cannot start with '!' — that prefix is reserved ` +\n `for negated tests ('!${name.slice(1)}' means \"does not carry ${name.slice(1)}\"). ` +\n `Rename the flag.`,\n )\n }\n\n function apply(value: unknown, target: object, propertyKey?: string | symbol): void {\n const declaration: RouteFlagDeclaration = { name, value }\n if (propertyKey === undefined) {\n pushClassMeta<RouteFlagDeclaration>(METADATA.CLASS_FLAGS, target, declaration)\n return\n }\n // Method decorators receive the prototype; metadata helpers key off the\n // constructor so class + method lookups land in the same place.\n pushMethodMeta<RouteFlagDeclaration>(\n METADATA.METHOD_FLAGS,\n (target as { constructor: object }).constructor,\n String(propertyKey),\n declaration,\n )\n }\n\n const flag = (...args: unknown[]): unknown => {\n if (isDecoratorApplication(args)) {\n // Bare form: @Public\n apply(true, args[0] as object, args[1] as string | symbol | undefined)\n return undefined\n }\n // Called form: @Public(false) / @RateLimit({ rpm: 10 })\n const value = args[0]\n return (target: object, propertyKey?: string | symbol): void => {\n apply(value, target, propertyKey)\n }\n }\n\n Object.defineProperty(flag, 'flagName', { value: name, enumerable: true })\n // `.off` is the removal decorator: bare-applicable, and the only way to drop\n // an inherited flag now that `false` is an ordinary value.\n Object.defineProperty(flag, 'off', {\n value: (target: object, propertyKey?: string | symbol): void => {\n apply(FLAG_UNSET, target, propertyKey)\n },\n enumerable: true,\n })\n return flag as unknown as RouteFlag<never>\n}\n\n/**\n * Resolve the flags in force for one route.\n *\n * Precedence is method > class > mount, matching the contributor pipeline's top\n * three levels. Within a level the last declaration wins (decorators apply\n * bottom-up, so the one nearest the handler is applied last).\n *\n * A resolved `false` deletes the key rather than storing it — see the module\n * docblock.\n */\nexport function resolveRouteFlags(\n classDeclarations: readonly RouteFlagDeclaration[],\n methodDeclarations: readonly RouteFlagDeclaration[],\n mountDeclarations: readonly RouteFlagDeclaration[] = [],\n): RouteFlags {\n const resolved = new Map<string, unknown>()\n\n // Lowest precedence first, so higher levels overwrite. Mount comes first\n // despite being the last parameter — it was added after the other two, and\n // moving it to the front would break every existing caller for no gain.\n for (const { name, value } of [\n ...mountDeclarations,\n ...classDeclarations,\n ...methodDeclarations,\n ]) {\n if (value === FLAG_UNSET) {\n resolved.delete(name)\n continue\n }\n resolved.set(name, value)\n }\n\n // The map is built from declarations, so its value types are whatever the\n // registry says they are — this cast is the one place that knowledge crosses\n // from runtime data into the type system.\n return resolved as RouteFlags\n}\n\n/**\n * The resolved flags for a route.\n *\n * A `ReadonlyMap` whose `has` is narrowed to known names and whose `get`\n * returns the declared value type — so `flags.get('rate.limit')` is\n * `{ rpm: number } | undefined` rather than `unknown`, once the registry\n * declares it. Until then it behaves like `ReadonlyMap<string, unknown>`.\n */\nexport interface RouteFlags extends ReadonlyMap<string, unknown> {\n has(name: RouteFlagName): boolean\n get<K extends RouteFlagName>(name: K): RouteFlagValue<K> | undefined\n}\n\n/**\n * Argument handed to a flag predicate.\n *\n * `route` is undefined only where flags are consulted outside a matched route,\n * which today means a non-HTTP transport running the same contributor pipeline.\n */\nexport interface RouteFlagContext {\n readonly flags: RouteFlags\n readonly route?: {\n readonly method: string\n readonly path: string\n readonly controller?: unknown\n readonly handlerName?: string\n }\n}\n\n/**\n * A predicate form of a flag test, for conditions a single name cannot express:\n * two flags together, a flag's *value*, a path or controller check, an env\n * switch.\n *\n * ```ts\n * skipWhen: ({ flags }) => flags.has('auth.public') || flags.has('internal'),\n * skipWhen: ({ flags }) => (flags.get('rate.limit') as { rpm: number })?.rpm === 0,\n * skipWhen: ({ route }) => route?.path.startsWith('/webhooks') ?? false,\n * ```\n *\n * Keep predicates pure and cheap — they run per request, per contributor.\n */\nexport type RouteFlagPredicate = (ctx: RouteFlagContext) => boolean\n\n/**\n * How a consumer names the routes it cares about.\n *\n * A bare name for the common case, a list when several flags mean the same\n * thing (matched **any-of**), a predicate for everything else — all-of, a\n * flag's value, the route's path.\n *\n * ```ts\n * skipWhen: 'auth.public'\n * skipWhen: ['auth.public', 'health.probe'] // either one\n * skipWhen: ({ flags }) => flags.has('a') && flags.has('b') // both\n * ```\n */\n/**\n * A flag name with `!` in front — matches routes that do **not** carry it.\n *\n * Narrows with the registry like the positive form: once `KickRouteFlags`\n * declares `'auth.public'`, this is `'!auth.public' | …`, so a misspelt\n * negation is a compile error too.\n */\nexport type NegatedRouteFlagName = `!${RouteFlagName}`\n\n/**\n * How a consumer names the routes it cares about.\n *\n * - `'auth.public'` — carries the flag\n * - `'!auth.public'` — does **not** carry it\n * - `['a', 'b']` — carries **any** of them\n * - `['!a', '!b']` — carries **none** of them\n * - a predicate — anything else: all-of, a flag's value, the route's path\n *\n * Lists are single-polarity by construction: `['a', '!b']` matches neither\n * array member of this union, so it is a compile error. Mixed polarity has no\n * reading everyone agrees on — under any-of it means \"a present OR b absent\",\n * which most readers parse as \"and\" — and a predicate says it unambiguously.\n */\nexport type RouteFlagTest =\n | RouteFlagName\n | NegatedRouteFlagName\n | readonly RouteFlagName[]\n | readonly NegatedRouteFlagName[]\n | RouteFlagPredicate\n\n/**\n * Evaluate a {@link RouteFlagTest} against a route's resolved flags.\n *\n * Shared by every consumer that can be exempted per route — the contributor\n * runner, `csrfGuard`, `rateLimitGuard` — so \"which routes does this apply to\"\n * means the same thing everywhere, and a list is any-of in all of them.\n */\nexport function matchesFlagTest(\n test: RouteFlagTest,\n flags: RouteFlags | undefined,\n route?: RouteFlagContext['route'],\n): boolean {\n const resolved = flags ?? EMPTY_FLAGS\n if (typeof test === 'function') return test({ flags: resolved, route })\n\n if (typeof test === 'string') {\n return isNegated(test)\n ? !resolved.has(stripNegation(test))\n : resolved.has(test as RouteFlagName)\n }\n\n if (test.length === 0) return false\n\n // Single polarity per list — the type forbids mixing, and this guards the\n // untyped callers (plain JS, a value read from config) that the type cannot.\n const negated = isNegated(test[0])\n if (test.some((name) => isNegated(name) !== negated)) {\n throw new Error(\n `Route flag test mixes polarities: [${test.map((n) => `'${n}'`).join(', ')}]. ` +\n `A list is either all names (\"carries any of these\") or all negated ` +\n `(\"carries none of these\") — for anything else, pass a predicate.`,\n )\n }\n\n // Positive list is any-of; negated list is its complement, none-of. The two\n // read as opposites, which is what makes flipping every entry do what a\n // reader expects.\n return negated\n ? test.every((name) => !resolved.has(stripNegation(name)))\n : test.some((name) => resolved.has(name as RouteFlagName))\n}\n\n/**\n * Reject a mixed-polarity list at construction time.\n *\n * A list like `['auth.public', '!metered']` is a static mistake, so it fails\n * where it is written — when the contributor or guard is built — rather than\n * on the first request that reaches it. Same reasoning as the contributor\n * pipeline failing a dependency cycle at boot.\n *\n * Call from every factory that accepts a {@link RouteFlagTest}; the check in\n * {@link matchesFlagTest} stays as a backstop for direct callers.\n */\nexport function assertFlagTest(test: RouteFlagTest, site: string): void {\n if (typeof test === 'function' || typeof test === 'string' || test.length === 0) return\n const negated = isNegated(test[0])\n if (test.some((name) => isNegated(name) !== negated)) {\n throw new Error(\n `${site}: route flag test mixes polarities: [${test.map((n) => `'${n}'`).join(', ')}]. ` +\n `A list is either all names (\"carries any of these\") or all negated ` +\n `(\"carries none of these\") — for anything else, pass a predicate.`,\n )\n }\n}\n\nconst isNegated = (name: string): boolean => name.startsWith('!')\nconst stripNegation = (name: string): RouteFlagName => name.slice(1) as RouteFlagName\n\nconst EMPTY_FLAGS: RouteFlags = new Map()\n\n/**\n * Where the matched route is stashed for `ctx.route` to read.\n *\n * It lives on the **request object**, not on the RequestContext: the Express\n * runtime constructs a fresh `RequestContext` per middleware, so anything\n * written to one ctx is invisible to the next and to the handler. Fastify and\n * h3 build a single ctx per request and would not have shown the difference —\n * the cross-runtime test is what surfaced it.\n *\n * `Symbol.for` rather than a private symbol so a duplicated module instance\n * (pnpm hoisting, a bundled copy) still reads the same slot.\n */\nexport const ROUTE_SLOT: unique symbol = Symbol.for('kick.route') as never\n\n/**\n * Resolve the flags in force for one handler, straight from the controller\n * class — the same method-over-class result `buildRouteTable` computes.\n *\n * For consumers that see a controller and a method name rather than a live\n * request: an adapter's `onRouteMount`, an OpenAPI generator, a DevTools route\n * listing. Inside a request, read `ctx.route.flags` instead — it is already\n * resolved.\n *\n * ```ts\n * // Mark flagged routes public in the OpenAPI spec:\n * SwaggerAdapter({\n * bearerAuth: true,\n * securityResolver: ({ controllerClass, handlerName }) =>\n * getRouteFlags(controllerClass, handlerName).has('auth.public') ? null : undefined,\n * })\n * ```\n */\nexport function getRouteFlags(controllerClass: object, handlerName: string): RouteFlags {\n return resolveRouteFlags(\n getClassFlagDeclarations(controllerClass),\n getMethodFlagDeclarations(controllerClass, handlerName),\n getMountFlagDeclarations(controllerClass),\n )\n}\n\n/** Read the class-level declarations off a controller. */\nexport function getClassFlagDeclarations(controllerClass: object): RouteFlagDeclaration[] {\n return getClassMeta<RouteFlagDeclaration[]>(METADATA.CLASS_FLAGS, controllerClass, [])\n}\n\n/** Read the method-level declarations off a controller method. */\nexport function getMethodFlagDeclarations(\n controllerClass: object,\n handlerName: string,\n): RouteFlagDeclaration[] {\n return getMethodMeta<RouteFlagDeclaration[]>(\n METADATA.METHOD_FLAGS,\n controllerClass,\n handlerName,\n [],\n )\n}\n\n/**\n * Flags declared as data rather than as a decorator, keyed by name.\n *\n * ```ts\n * { 'auth.public': true, 'rate.limit': { rpm: 10 } }\n * ```\n */\nexport type RouteFlagRecord = { [K in RouteFlagName]?: RouteFlagValue<K, true> }\n\n/**\n * What a non-decorator registration site accepts.\n *\n * A list is the common case — bare flags, each stored as `true`:\n * `flags: ['auth.public']`. The record form exists for flags that carry a\n * value. Both narrow against {@link KickRouteFlags} once it is augmented.\n */\nexport type RouteFlagDeclarations = readonly RouteFlagName[] | RouteFlagRecord\n\n/**\n * Normalise either input form into declarations.\n *\n * @param site Where the flags came from, for the error message — a name is\n * validated here rather than at read time so a typo'd `!` fails at boot.\n */\nexport function toFlagDeclarations(\n flags: RouteFlagDeclarations | undefined,\n site: string,\n): RouteFlagDeclaration[] {\n if (!flags) return []\n const entries: [string, unknown][] = Array.isArray(flags)\n ? flags.map((name) => [name as string, true])\n : Object.entries(flags as Record<string, unknown>)\n\n return entries.map(([name, value]) => {\n // Same reservation as `defineRouteFlag`: `!name` is a negated *test*, and a\n // flag named that way could never be tested for. Declaring one is always a\n // mistake — usually someone reaching for \"not public\" on the wrong side.\n if (name.startsWith('!')) {\n throw new Error(\n `${site}: a flag name cannot start with '!' — that prefix is reserved for negated ` +\n `tests, and a declaration has no negative form. Remove the flag instead of ` +\n `declaring '${name}'.`,\n )\n }\n return { name, value }\n })\n}\n\n/**\n * Mount-level declarations, keyed by controller class.\n *\n * A module declares flags for every route it mounts (`ModuleRoutes.flags`), so\n * unlike class and method declarations they are not written by a decorator and\n * have nowhere on the class to live. They are recorded here at mount time so\n * that {@link getRouteFlags} — which sees only a controller and a method name —\n * reports the same flags the runtime resolved. Without it an OpenAPI spec or a\n * DevTools route listing would disagree with `ctx.route.flags`.\n *\n * A `WeakMap`, so a controller from a torn-down app (a test, an HMR reload) is\n * collectable. Mounting the same controller twice replaces rather than appends:\n * a reload re-declares the same flags, and appending would grow without bound.\n */\nconst MOUNT_FLAGS = new WeakMap<object, RouteFlagDeclaration[]>()\n\n/**\n * Record the flags a mount declares for `controllerClass`.\n *\n * @internal Called by the route-table builder; not part of the app-facing API.\n */\nexport function registerMountFlags(\n controllerClass: object,\n declarations: readonly RouteFlagDeclaration[],\n): void {\n if (declarations.length === 0) {\n MOUNT_FLAGS.delete(controllerClass)\n return\n }\n MOUNT_FLAGS.set(controllerClass, [...declarations])\n}\n\n/** Read the mount-level declarations recorded for a controller. */\nexport function getMountFlagDeclarations(controllerClass: object): RouteFlagDeclaration[] {\n return MOUNT_FLAGS.get(controllerClass) ?? []\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"route-flag-BWuvJsGZ.mjs","names":[],"sources":["../src/core/route-flag.ts"],"sourcesContent":["/**\n * Route flags — a named, inheritable fact about a route.\n *\n * A flag carries no behaviour. It records something about the route that any\n * consumer may read: `auth.public`, `csrf.exempt`, `rate.limit`. Auth reads it\n * to skip a contributor, a guard reads it to return early, Swagger reads it to\n * mark a security scheme — none of them needs to know about the others.\n *\n * The rule that makes presence checks safe: a flag resolves to **absent**, or\n * **present with a value**. `@Public.off` on a method removes what the class\n * set, rather than storing something falsy — so `flags.has(name)` is always the\n * right question for a boolean flag. Without that, `has('auth.public')` could\n * answer `true` for the route that just opted back IN, which is an\n * authorization bypass rather than a papercut.\n */\nimport { METADATA } from './interfaces'\nimport { getClassMeta, getMethodMeta, pushClassMeta, pushMethodMeta } from './metadata'\n\n/**\n * Augmentable route-flag registry — the flag counterpart of `ContextMeta`.\n *\n * Declare a project's flags here and every use of them narrows: a misspelt\n * name is a compile error rather than a flag that silently never matches, and\n * `flags.get('rate.limit')` comes back typed instead of `unknown`.\n *\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface KickRouteFlags {\n * 'auth.public': true\n * 'rate.limit': { rpm: number }\n * }\n * }\n * ```\n *\n * The interface is empty by default, and everything below falls back to plain\n * `string` while it stays that way — a project that has not augmented anything\n * keeps compiling exactly as before. The narrowing switches on with the first\n * declaration.\n *\n * The framework itself declares nothing here: it ships the mechanism and names\n * no flags, so `auth.public` above is a name you chose.\n *\n * Two constraints on what you can declare:\n * - A name cannot start with `!` — that prefix means \"does not carry this flag\"\n * in a {@link RouteFlagTest}, and `defineRouteFlag` rejects it.\n * - Removal is `@Flag.off`, not `@Flag(false)` — so `false` stays usable as a\n * real value for a flag declared `boolean`.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\nexport interface KickRouteFlags {}\n\n/**\n * A flag name. Narrows to the declared keys once {@link KickRouteFlags} is\n * augmented, and stays `string` until then — the same\n * `[Known] extends [never]` fallback `ContextMetaKey` uses, for the same\n * reason: first-day code must keep compiling.\n */\nexport type RouteFlagName = [keyof KickRouteFlags] extends [never]\n ? string\n : keyof KickRouteFlags & string\n\n/**\n * The value type declared for a flag, defaulting to `true` for a bare flag and\n * `unknown` for a name the registry does not know.\n */\nexport type RouteFlagValue<K extends string, Fallback = unknown> = K extends keyof KickRouteFlags\n ? KickRouteFlags[K]\n : Fallback\n\n/**\n * Marks a declaration as \"remove this flag\" during resolution.\n *\n * A dedicated sentinel rather than `false`: `false` is a perfectly good flag\n * VALUE (a registry entry typed `boolean`), and overloading it meant\n * `@Enabled(false)` deleted the flag while `flags.get('enabled')` could never\n * return `false`. With the sentinel, `false` stores like any other value and\n * removal is spelled `@Enabled.off`.\n *\n * Module-private and NOT `Symbol.for`: a registry-global symbol is reachable\n * by application code, so a flag declared with a `symbol` value could be handed\n * this exact sentinel and be deleted instead of stored. It is created and\n * consumed in this module, so it never needs to cross a realm.\n */\nconst FLAG_UNSET: unique symbol = Symbol('kick.flagUnset') as never\n\n/** One `@Flag` application, before precedence resolution. */\nexport interface RouteFlagDeclaration {\n readonly name: string\n /** `false` means \"remove whatever a lower-precedence site declared\". */\n readonly value: unknown\n}\n\n/**\n * A flag decorator. Usable bare (`@Public`) or called with a value\n * (`@Public(false)`, `@RateLimit({ rpm: 10 })`), on a class or a method.\n */\n/**\n * The bare application forms — `@Public` with no call.\n *\n * Split out so they can be withheld from a flag whose declared value is not\n * assignable from `true`: applying `@Limit` bare would store `true` while\n * `flags.get('rate.limit')` promises `{ rpm: number }`, and nothing would catch\n * the mismatch until a consumer read the wrong shape at runtime.\n */\nexport interface BareRouteFlag {\n /**\n * Bare on a class. Typed against `Function` rather than `object` so a flag\n * VALUE that happens to be an object (`@Limit({ rpm: 10 })`) cannot match\n * this signature: overload resolution would otherwise pick it, return\n * `void`, and fail with \"Type 'void' has no call signatures\".\n */\n (target: Function): void\n /**\n * Bare on a method. `propertyKey` is required for the same reason — it makes\n * a one-argument call unambiguous.\n */\n (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): void\n}\n\n/** The called form — `@Limit({ rpm: 10 })` — plus removal via `.off`. */\nexport interface ValuedRouteFlag<V> {\n (value: V): (target: object, propertyKey?: string | symbol) => void\n /**\n * Remove the flag on this route — `@Public.off` on a method drops what the\n * controller set.\n *\n * Applied bare, like the flag itself. Spelled as its own member rather than\n * `@Public(false)` so that `false` stays available as a real value: a flag\n * declared `boolean` can store it, and `flags.get()` can return it.\n */\n readonly off: BareRouteFlag\n readonly flagName: string\n}\n\n/**\n * A flag decorator.\n *\n * Bare application is available only when `true` is a valid value for the flag\n * — which is every flag until {@link KickRouteFlags} declares a value type for\n * it. Declare `'rate.limit': { rpm: number }` and `@RateLimit` on its own stops\n * compiling, because the value it would store is not the value the registry\n * promises readers.\n */\nexport type RouteFlag<V = true> = true extends V\n ? BareRouteFlag & ValuedRouteFlag<V>\n : ValuedRouteFlag<V>\n\n/**\n * True when the arguments look like a decorator application rather than a\n * value. The two forms are `@Public` (TS calls it for us) and `@Public(v)`\n * (we return the decorator).\n *\n * Discrimination has to be exact, because a value can be an object:\n * `@RateLimit({ rpm: 10 })` is a single object argument, and treating it as a\n * class-decorator target silently drops the flag.\n *\n * - method decorator → `(prototype, 'name', descriptor?)`, so arg 1 is a\n * string or symbol\n * - class decorator → `(constructor)`, a single *function* argument\n * - anything else → a value\n */\nfunction isDecoratorApplication(args: unknown[]): boolean {\n const [target, propertyKey] = args\n if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') return true\n return args.length === 1 && typeof target === 'function'\n}\n\n/**\n * Define a route flag.\n *\n * ```ts\n * export const Public = defineRouteFlag('auth.public')\n * export const RateLimit = defineRouteFlag<{ rpm: number }>('rate.limit')\n * ```\n *\n * Once {@link KickRouteFlags} declares the name, the value type comes from the\n * registry and the explicit generic is redundant:\n *\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface KickRouteFlags {\n * 'rate.limit': { rpm: number }\n * }\n * }\n *\n * const RateLimit = defineRouteFlag('rate.limit') // RouteFlag<{ rpm: number }>\n * ```\n */\nexport function defineRouteFlag<const N extends RouteFlagName>(\n name: N,\n): RouteFlag<RouteFlagValue<N, true>>\nexport function defineRouteFlag<V>(name: RouteFlagName): RouteFlag<V>\nexport function defineRouteFlag(name: string): RouteFlag<never> {\n // `!` is reserved: a test string starting with it means \"does NOT carry this\n // flag\", so a flag literally named '!x' would be indistinguishable from the\n // negation of 'x' — `exemptWhen: '!x'` would silently stop meaning what the\n // author wrote. Rejecting at definition is the only point where the two can\n // still be told apart.\n if (name.startsWith('!')) {\n throw new Error(\n `defineRouteFlag('${name}'): a flag name cannot start with '!' — that prefix is reserved ` +\n `for negated tests ('!${name.slice(1)}' means \"does not carry ${name.slice(1)}\"). ` +\n `Rename the flag.`,\n )\n }\n\n function apply(value: unknown, target: object, propertyKey?: string | symbol): void {\n const declaration: RouteFlagDeclaration = { name, value }\n if (propertyKey === undefined) {\n pushClassMeta<RouteFlagDeclaration>(METADATA.CLASS_FLAGS, target, declaration)\n return\n }\n // Method decorators receive the prototype; metadata helpers key off the\n // constructor so class + method lookups land in the same place.\n pushMethodMeta<RouteFlagDeclaration>(\n METADATA.METHOD_FLAGS,\n (target as { constructor: object }).constructor,\n String(propertyKey),\n declaration,\n )\n }\n\n const flag = (...args: unknown[]): unknown => {\n if (isDecoratorApplication(args)) {\n // Bare form: @Public\n apply(true, args[0] as object, args[1] as string | symbol | undefined)\n return undefined\n }\n // Called form: @Public(false) / @RateLimit({ rpm: 10 })\n const value = args[0]\n return (target: object, propertyKey?: string | symbol): void => {\n apply(value, target, propertyKey)\n }\n }\n\n Object.defineProperty(flag, 'flagName', { value: name, enumerable: true })\n // `.off` is the removal decorator: bare-applicable, and the only way to drop\n // an inherited flag now that `false` is an ordinary value.\n Object.defineProperty(flag, 'off', {\n value: (target: object, propertyKey?: string | symbol): void => {\n apply(FLAG_UNSET, target, propertyKey)\n },\n enumerable: true,\n })\n return flag as unknown as RouteFlag<never>\n}\n\n/**\n * Resolve the flags in force for one route.\n *\n * Precedence is method > class > mount, matching the contributor pipeline's top\n * three levels. Within a level the last declaration wins (decorators apply\n * bottom-up, so the one nearest the handler is applied last).\n *\n * A resolved `false` deletes the key rather than storing it — see the module\n * docblock.\n */\nexport function resolveRouteFlags(\n classDeclarations: readonly RouteFlagDeclaration[],\n methodDeclarations: readonly RouteFlagDeclaration[],\n mountDeclarations: readonly RouteFlagDeclaration[] = [],\n): RouteFlags {\n const resolved = new Map<string, unknown>()\n\n // Lowest precedence first, so higher levels overwrite. Mount comes first\n // despite being the last parameter — it was added after the other two, and\n // moving it to the front would break every existing caller for no gain.\n for (const { name, value } of [\n ...mountDeclarations,\n ...classDeclarations,\n ...methodDeclarations,\n ]) {\n if (value === FLAG_UNSET) {\n resolved.delete(name)\n continue\n }\n resolved.set(name, value)\n }\n\n // The map is built from declarations, so its value types are whatever the\n // registry says they are — this cast is the one place that knowledge crosses\n // from runtime data into the type system.\n return resolved as RouteFlags\n}\n\n/**\n * The resolved flags for a route.\n *\n * A `ReadonlyMap` whose `has` is narrowed to known names and whose `get`\n * returns the declared value type — so `flags.get('rate.limit')` is\n * `{ rpm: number } | undefined` rather than `unknown`, once the registry\n * declares it. Until then it behaves like `ReadonlyMap<string, unknown>`.\n */\nexport interface RouteFlags extends ReadonlyMap<string, unknown> {\n has(name: RouteFlagName): boolean\n get<K extends RouteFlagName>(name: K): RouteFlagValue<K> | undefined\n}\n\n/**\n * Argument handed to a flag predicate.\n *\n * `route` is undefined only where flags are consulted outside a matched route,\n * which today means a non-HTTP transport running the same contributor pipeline.\n */\nexport interface RouteFlagContext {\n readonly flags: RouteFlags\n readonly route?: {\n readonly method: string\n readonly path: string\n readonly controller?: unknown\n readonly handlerName?: string\n }\n}\n\n/**\n * A predicate form of a flag test, for conditions a single name cannot express:\n * two flags together, a flag's *value*, a path or controller check, an env\n * switch.\n *\n * ```ts\n * skipWhen: ({ flags }) => flags.has('auth.public') || flags.has('internal'),\n * skipWhen: ({ flags }) => (flags.get('rate.limit') as { rpm: number })?.rpm === 0,\n * skipWhen: ({ route }) => route?.path.startsWith('/webhooks') ?? false,\n * ```\n *\n * Keep predicates pure and cheap — they run per request, per contributor.\n */\nexport type RouteFlagPredicate = (ctx: RouteFlagContext) => boolean\n\n/**\n * How a consumer names the routes it cares about.\n *\n * A bare name for the common case, a list when several flags mean the same\n * thing (matched **any-of**), a predicate for everything else — all-of, a\n * flag's value, the route's path.\n *\n * ```ts\n * skipWhen: 'auth.public'\n * skipWhen: ['auth.public', 'health.probe'] // either one\n * skipWhen: ({ flags }) => flags.has('a') && flags.has('b') // both\n * ```\n */\n/**\n * A flag name with `!` in front — matches routes that do **not** carry it.\n *\n * Narrows with the registry like the positive form: once `KickRouteFlags`\n * declares `'auth.public'`, this is `'!auth.public' | …`, so a misspelt\n * negation is a compile error too.\n */\nexport type NegatedRouteFlagName = `!${RouteFlagName}`\n\n/**\n * How a consumer names the routes it cares about.\n *\n * - `'auth.public'` — carries the flag\n * - `'!auth.public'` — does **not** carry it\n * - `['a', 'b']` — carries **any** of them\n * - `['!a', '!b']` — carries **none** of them\n * - a predicate — anything else: all-of, a flag's value, the route's path\n *\n * Lists are single-polarity by construction: `['a', '!b']` matches neither\n * array member of this union, so it is a compile error. Mixed polarity has no\n * reading everyone agrees on — under any-of it means \"a present OR b absent\",\n * which most readers parse as \"and\" — and a predicate says it unambiguously.\n */\nexport type RouteFlagTest =\n | RouteFlagName\n | NegatedRouteFlagName\n | readonly RouteFlagName[]\n | readonly NegatedRouteFlagName[]\n | RouteFlagPredicate\n\n/**\n * Evaluate a {@link RouteFlagTest} against a route's resolved flags.\n *\n * Shared by every consumer that can be exempted per route — the contributor\n * runner, `csrfGuard`, `rateLimitGuard` — so \"which routes does this apply to\"\n * means the same thing everywhere, and a list is any-of in all of them.\n */\nexport function matchesFlagTest(\n test: RouteFlagTest,\n flags: RouteFlags | undefined,\n route?: RouteFlagContext['route'],\n): boolean {\n const resolved = flags ?? EMPTY_FLAGS\n if (typeof test === 'function') return test({ flags: resolved, route })\n\n if (typeof test === 'string') {\n return isNegated(test)\n ? !resolved.has(stripNegation(test))\n : resolved.has(test as RouteFlagName)\n }\n\n if (test.length === 0) return false\n\n // Single polarity per list — the type forbids mixing, and this guards the\n // untyped callers (plain JS, a value read from config) that the type cannot.\n const negated = isNegated(test[0])\n if (test.some((name) => isNegated(name) !== negated)) {\n throw new Error(\n `Route flag test mixes polarities: [${test.map((n) => `'${n}'`).join(', ')}]. ` +\n `A list is either all names (\"carries any of these\") or all negated ` +\n `(\"carries none of these\") — for anything else, pass a predicate.`,\n )\n }\n\n // Positive list is any-of; negated list is its complement, none-of. The two\n // read as opposites, which is what makes flipping every entry do what a\n // reader expects.\n return negated\n ? test.every((name) => !resolved.has(stripNegation(name)))\n : test.some((name) => resolved.has(name as RouteFlagName))\n}\n\n/**\n * Reject a mixed-polarity list at construction time.\n *\n * A list like `['auth.public', '!metered']` is a static mistake, so it fails\n * where it is written — when the contributor or guard is built — rather than\n * on the first request that reaches it. Same reasoning as the contributor\n * pipeline failing a dependency cycle at boot.\n *\n * Call from every factory that accepts a {@link RouteFlagTest}; the check in\n * {@link matchesFlagTest} stays as a backstop for direct callers.\n */\nexport function assertFlagTest(test: RouteFlagTest, site: string): void {\n if (typeof test === 'function' || typeof test === 'string' || test.length === 0) return\n const negated = isNegated(test[0])\n if (test.some((name) => isNegated(name) !== negated)) {\n throw new Error(\n `${site}: route flag test mixes polarities: [${test.map((n) => `'${n}'`).join(', ')}]. ` +\n `A list is either all names (\"carries any of these\") or all negated ` +\n `(\"carries none of these\") — for anything else, pass a predicate.`,\n )\n }\n}\n\nconst isNegated = (name: string): boolean => name.startsWith('!')\nconst stripNegation = (name: string): RouteFlagName => name.slice(1) as RouteFlagName\n\nconst EMPTY_FLAGS: RouteFlags = new Map()\n\n/**\n * Where the matched route is stashed for `ctx.route` to read.\n *\n * It lives on the **request object**, not on the RequestContext: the Express\n * runtime constructs a fresh `RequestContext` per middleware, so anything\n * written to one ctx is invisible to the next and to the handler. Fastify and\n * h3 build a single ctx per request and would not have shown the difference —\n * the cross-runtime test is what surfaced it.\n *\n * `Symbol.for` rather than a private symbol so a duplicated module instance\n * (pnpm hoisting, a bundled copy) still reads the same slot.\n */\nexport const ROUTE_SLOT: unique symbol = Symbol.for('kick.route') as never\n\n/**\n * Resolve the flags in force for one handler, straight from the controller\n * class — the same method-over-class result `buildRouteTable` computes.\n *\n * For consumers that see a controller and a method name rather than a live\n * request: an adapter's `onRouteMount`, an OpenAPI generator, a DevTools route\n * listing. Inside a request, read `ctx.route.flags` instead — it is already\n * resolved.\n *\n * ```ts\n * // Mark flagged routes public in the OpenAPI spec:\n * SwaggerAdapter({\n * bearerAuth: true,\n * securityResolver: ({ controllerClass, handlerName }) =>\n * getRouteFlags(controllerClass, handlerName).has('auth.public') ? null : undefined,\n * })\n * ```\n */\nexport function getRouteFlags(controllerClass: object, handlerName: string): RouteFlags {\n return resolveRouteFlags(\n getClassFlagDeclarations(controllerClass),\n getMethodFlagDeclarations(controllerClass, handlerName),\n getMountFlagDeclarations(controllerClass),\n )\n}\n\n/** Read the class-level declarations off a controller. */\nexport function getClassFlagDeclarations(controllerClass: object): RouteFlagDeclaration[] {\n return getClassMeta<RouteFlagDeclaration[]>(METADATA.CLASS_FLAGS, controllerClass, [])\n}\n\n/** Read the method-level declarations off a controller method. */\nexport function getMethodFlagDeclarations(\n controllerClass: object,\n handlerName: string,\n): RouteFlagDeclaration[] {\n return getMethodMeta<RouteFlagDeclaration[]>(\n METADATA.METHOD_FLAGS,\n controllerClass,\n handlerName,\n [],\n )\n}\n\n/**\n * Flags declared as data rather than as a decorator, keyed by name.\n *\n * ```ts\n * { 'auth.public': true, 'rate.limit': { rpm: 10 } }\n * ```\n */\nexport type RouteFlagRecord = { [K in RouteFlagName]?: RouteFlagValue<K, true> }\n\n/**\n * What a non-decorator registration site accepts.\n *\n * A list is the common case — bare flags, each stored as `true`:\n * `flags: ['auth.public']`. The record form exists for flags that carry a\n * value. Both narrow against {@link KickRouteFlags} once it is augmented.\n */\nexport type RouteFlagDeclarations = readonly RouteFlagName[] | RouteFlagRecord\n\n/**\n * Normalise either input form into declarations.\n *\n * @param site Where the flags came from, for the error message — a name is\n * validated here rather than at read time so a typo'd `!` fails at boot.\n */\nexport function toFlagDeclarations(\n flags: RouteFlagDeclarations | undefined,\n site: string,\n): RouteFlagDeclaration[] {\n if (!flags) return []\n const entries: [string, unknown][] = Array.isArray(flags)\n ? flags.map((name) => [name as string, true])\n : Object.entries(flags as Record<string, unknown>)\n\n return entries.map(([name, value]) => {\n // Same reservation as `defineRouteFlag`: `!name` is a negated *test*, and a\n // flag named that way could never be tested for. Declaring one is always a\n // mistake — usually someone reaching for \"not public\" on the wrong side.\n if (name.startsWith('!')) {\n throw new Error(\n `${site}: a flag name cannot start with '!' — that prefix is reserved for negated ` +\n `tests, and a declaration has no negative form. Remove the flag instead of ` +\n `declaring '${name}'.`,\n )\n }\n return { name, value }\n })\n}\n\n/**\n * Mount-level declarations, keyed by controller class.\n *\n * A module declares flags for every route it mounts (`ModuleRoutes.flags`), so\n * unlike class and method declarations they are not written by a decorator and\n * have nowhere on the class to live. They are recorded here at mount time so\n * that {@link getRouteFlags} — which sees only a controller and a method name —\n * reports the same flags the runtime resolved. Without it an OpenAPI spec or a\n * DevTools route listing would disagree with `ctx.route.flags`.\n *\n * A `WeakMap`, so a controller from a torn-down app (a test, an HMR reload) is\n * collectable. Mounting the same controller twice replaces rather than appends:\n * a reload re-declares the same flags, and appending would grow without bound.\n */\nconst MOUNT_FLAGS = new WeakMap<object, RouteFlagDeclaration[]>()\n\n/**\n * Record the flags a mount declares for `controllerClass`.\n *\n * @internal Called by the route-table builder; not part of the app-facing API.\n */\nexport function registerMountFlags(\n controllerClass: object,\n declarations: readonly RouteFlagDeclaration[],\n): void {\n if (declarations.length === 0) {\n MOUNT_FLAGS.delete(controllerClass)\n return\n }\n MOUNT_FLAGS.set(controllerClass, [...declarations])\n}\n\n/** Read the mount-level declarations recorded for a controller. */\nexport function getMountFlagDeclarations(controllerClass: object): RouteFlagDeclaration[] {\n return MOUNT_FLAGS.get(controllerClass) ?? []\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v10.
|
|
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 EMPTY=/* @__PURE__ */ new Map;function compile(path){let source=path.replace(/[.+?^${}()|[\]\\]/g,`\\$&`).replace(/\/:[^/]+/g,`/([^/]+)`).replace(/\*/g,`.*`);return RegExp(`^${source}/?$`)}var RoutePolicyTable=class{byMethod=/* @__PURE__ */ new Map;add(method,path,flags){if(!flags||flags.size===0)return;let verb=method.toUpperCase(),bucket=this.byMethod.get(verb)??[];bucket.push({pattern:compile(path),flags}),this.byMethod.set(verb,bucket)}lookup(method,pathname){let bucket=this.byMethod.get(method.toUpperCase());if(!bucket)return EMPTY;for(let entry of bucket)if(entry.pattern.test(pathname))return entry.flags;return EMPTY}clear(){this.byMethod.clear()}get size(){let total=0;for(let bucket of this.byMethod.values())total+=bucket.length;return total}};const ROUTE_POLICY_SLOT=Symbol.for(`kick.routePolicy`);function bindRoutePolicy(handler,receive){return Object.defineProperty(handler,ROUTE_POLICY_SLOT,{value:receive,enumerable:!1}),handler}function offerRoutePolicy(handler,table){let receive=handler?.[ROUTE_POLICY_SLOT];typeof receive==`function`&&receive(table)}export{bindRoutePolicy as n,offerRoutePolicy as r,RoutePolicyTable as t};
|
|
12
|
-
//# sourceMappingURL=route-policy-
|
|
12
|
+
//# sourceMappingURL=route-policy-B3vM6bSf.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-policy-
|
|
1
|
+
{"version":3,"file":"route-policy-B3vM6bSf.mjs","names":[],"sources":["../src/core/route-policy.ts"],"sourcesContent":["/**\n * The route policy table — route flags for pre-match middleware.\n *\n * Middleware mounted app-wide (the connect-style `rateLimit()`, `csrf()`) runs\n * *before* a route is matched, so it cannot read `ctx.route.flags`: there is no\n * route yet. That is also the reason to keep it there — an abuse control has to\n * see traffic that matches nothing, which a route-scoped guard never does.\n *\n * So the flags come to it instead. Every mounted route registers its method,\n * full path and resolved flags here at boot; a global middleware asks the table\n * what flags *would* apply to an incoming method + pathname.\n *\n * The table is per-Application, handed to middleware that opts in via\n * {@link bindRoutePolicy}. Nothing global: two apps in one process (a test file,\n * an embedded admin app) each get their own.\n */\n\nimport type { RouteFlags } from './route-flag'\n\n/** Flags for one mounted route, keyed for lookup by an incoming request. */\ninterface PolicyEntry {\n /** Compiled from the mounted path — `/users/:id` → `^/users/([^/]+)$`. */\n readonly pattern: RegExp\n readonly flags: RouteFlags\n}\n\nconst EMPTY: RouteFlags = new Map() as RouteFlags\n\n/**\n * Turn a mounted path into a matcher.\n *\n * Deliberately small: `:param` matches one segment, `*` matches the rest, and\n * everything else is literal. This is a *lookup* over paths the engine already\n * chose to mount, not a second router — if it ever disagrees with the engine,\n * the answer is fewer features here, not more.\n */\nfunction compile(path: string): RegExp {\n const source = path\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\/:[^/]+/g, '/([^/]+)')\n .replace(/\\*/g, '.*')\n return new RegExp(`^${source}/?$`)\n}\n\nexport class RoutePolicyTable {\n /** Bucketed by method: an incoming request only scans its own verb. */\n private readonly byMethod = new Map<string, PolicyEntry[]>()\n\n add(method: string, path: string, flags: RouteFlags | undefined): void {\n if (!flags || flags.size === 0) return // nothing to say about this route\n const verb = method.toUpperCase()\n const bucket = this.byMethod.get(verb) ?? []\n bucket.push({ pattern: compile(path), flags })\n this.byMethod.set(verb, bucket)\n }\n\n /**\n * Flags that apply to `method pathname`, or an empty map when the path\n * matches no flagged route — including when it matches no route at all,\n * which is exactly the traffic this exists to keep visible.\n */\n lookup(method: string, pathname: string): RouteFlags {\n const bucket = this.byMethod.get(method.toUpperCase())\n if (!bucket) return EMPTY\n for (const entry of bucket) {\n if (entry.pattern.test(pathname)) return entry.flags\n }\n return EMPTY\n }\n\n /** Drop everything — the Application rebuilds on every HMR reload. */\n clear(): void {\n this.byMethod.clear()\n }\n\n get size(): number {\n let total = 0\n for (const bucket of this.byMethod.values()) total += bucket.length\n return total\n }\n}\n\n/**\n * Slot a middleware exposes to say \"hand me the policy table once routes are\n * mounted\". The Application calls it during setup; middleware that never\n * declares it is untouched.\n */\nexport const ROUTE_POLICY_SLOT: unique symbol = Symbol.for('kick.routePolicy') as never\n\n/** A middleware that accepts the policy table. */\nexport interface RoutePolicyAware {\n [ROUTE_POLICY_SLOT]?: (table: RoutePolicyTable) => void\n}\n\n/**\n * Declare that `handler` wants the policy table.\n *\n * Explicit hand-off rather than a module-level singleton, so the table a\n * middleware reads is always the one belonging to the app that mounted it.\n */\nexport function bindRoutePolicy<T extends object>(\n handler: T,\n receive: (table: RoutePolicyTable) => void,\n): T {\n Object.defineProperty(handler, ROUTE_POLICY_SLOT, { value: receive, enumerable: false })\n return handler\n}\n\n/** Hand the table to a middleware if it declared the slot. */\nexport function offerRoutePolicy(handler: unknown, table: RoutePolicyTable): void {\n const receive = (handler as RoutePolicyAware | null)?.[ROUTE_POLICY_SLOT]\n if (typeof receive === 'function') receive(table)\n}\n"],"mappings":";;;;;;;;;;AA0BA,MAAM,sBAAoB,IAAI,IAU9B,SAAS,QAAQ,KAAsB,CACrC,IAAM,OAAS,KACZ,QAAQ,qBAAsB,MAAM,CAAC,CACrC,QAAQ,YAAa,UAAU,CAAC,CAChC,QAAQ,MAAO,IAAI,EACtB,OAAW,OAAO,IAAI,OAAO,IAAI,CACnC,CAEA,IAAa,iBAAb,KAA8B,CAE5B,yBAA4B,IAAI,IAEhC,IAAI,OAAgB,KAAc,MAAqC,CACrE,GAAI,CAAC,OAAS,MAAM,OAAS,EAAG,OAChC,IAAM,KAAO,OAAO,YAAY,EAC1B,OAAS,KAAK,SAAS,IAAI,IAAI,GAAK,CAAC,EAC3C,OAAO,KAAK,CAAE,QAAS,QAAQ,IAAI,EAAG,KAAM,CAAC,EAC7C,KAAK,SAAS,IAAI,KAAM,MAAM,CAChC,CAOA,OAAO,OAAgB,SAA8B,CACnD,IAAM,OAAS,KAAK,SAAS,IAAI,OAAO,YAAY,CAAC,EACrD,GAAI,CAAC,OAAQ,OAAO,MACpB,IAAK,IAAM,SAAS,OAClB,GAAI,MAAM,QAAQ,KAAK,QAAQ,EAAG,OAAO,MAAM,MAEjD,OAAO,KACT,CAGA,OAAc,CACZ,KAAK,SAAS,MAAM,CACtB,CAEA,IAAI,MAAe,CACjB,IAAI,MAAQ,EACZ,IAAK,IAAM,UAAU,KAAK,SAAS,OAAO,EAAG,OAAS,OAAO,OAC7D,OAAO,KACT,CACF,EAOA,MAAa,kBAAmC,OAAO,IAAI,kBAAkB,EAa7E,SAAgB,gBACd,QACA,QACG,CAEH,OADA,OAAO,eAAe,QAAS,kBAAmB,CAAE,MAAO,QAAS,WAAY,EAAM,CAAC,EAChF,OACT,CAGA,SAAgB,iBAAiB,QAAkB,MAA+B,CAChF,IAAM,QAAW,UAAsC,mBACnD,OAAO,SAAY,YAAY,QAAQ,KAAK,CAClD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v10.
|
|
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{METADATA}from"./core/interfaces.mjs";import{o as duplicateRouteError,t as Container}from"./container-
|
|
12
|
-
//# sourceMappingURL=router-builder-
|
|
11
|
+
import{METADATA}from"./core/interfaces.mjs";import{o as duplicateRouteError,t as Container}from"./container-CS9v9UFU.mjs";import{a as getMethodMeta,o as getMethodMetaOrUndefined,t as getClassMeta}from"./metadata-zvd6AWkE.mjs";import{a as getMethodFlagDeclarations,c as registerMountFlags,i as getClassFlagDeclarations,l as resolveRouteFlags,u as toFlagDeclarations}from"./route-flag-BWuvJsGZ.mjs";import{i as MissingContributorError,n as DuplicateContributorError,t as ContributorCycleError}from"./context-errors-7C5TBYn7.mjs";import{t as runContributors}from"./contributor-runner-DBpRR5dl.mjs";const PRECEDENCE_ORDER=[`method`,`class`,`module`,`adapter`,`global`];function buildPipeline(sources,options={}){let route=options.route,bySource=/* @__PURE__ */ new Map;for(let src of PRECEDENCE_ORDER)bySource.set(src,[]);for(let entry of sources)bySource.get(entry.source).push(entry);let winners=/* @__PURE__ */ new Map;for(let src of PRECEDENCE_ORDER){let rows=bySource.get(src),keysAtThisLevel=/* @__PURE__ */ new Map;for(let row of rows){let key=row.registration.key,existing=keysAtThisLevel.get(key)??[];existing.push(row),keysAtThisLevel.set(key,existing)}for(let[key,rowsForKey]of keysAtThisLevel){if(rowsForKey.length>1){let labels=rowsForKey.map((r,i)=>r.label??`${src}#${i}`);throw new DuplicateContributorError(key,labels)}winners.has(key)||winners.set(key,rowsForKey[0])}}let survivors=[...winners.values()].map(r=>r.registration),allKeys=new Set(survivors.map(r=>r.key));for(let reg of survivors)for(let dep of reg.dependsOn)if(!allKeys.has(dep))throw new MissingContributorError(dep,reg.key,route,reg.definedAt);let sorted=topoSort(survivors,route);return Object.freeze({contributors:Object.freeze(sorted),keys:allKeys})}function splitBeforeValidation(pipeline,route){let early=pipeline.contributors.filter(reg=>reg.beforeValidation);if(early.length===0)return{early:null,late:pipeline};let earlyKeys=new Set(early.map(reg=>reg.key));for(let reg of early){let lateDep=reg.dependsOn.find(dep=>!earlyKeys.has(dep));if(lateDep!==void 0)throw Error(`Contributor '${reg.key}'${route?` on ${route}`:``} sets beforeValidation but depends on '${lateDep}', which runs after validation. Set beforeValidation on '${lateDep}' too, or remove it from '${reg.key}'.`)}let late=pipeline.contributors.filter(reg=>!reg.beforeValidation);return{early:Object.freeze({contributors:Object.freeze(early),keys:earlyKeys}),late:late.length===0?null:Object.freeze({contributors:Object.freeze(late),keys:new Set(late.map(reg=>reg.key))})}}function topoSort(registrations,route){let indegree=/* @__PURE__ */ new Map,dependents=/* @__PURE__ */ new Map,byKey=/* @__PURE__ */ new Map;for(let reg of registrations)indegree.set(reg.key,reg.dependsOn.length),byKey.set(reg.key,reg);for(let reg of registrations)for(let dep of reg.dependsOn){let list=dependents.get(dep)??[];list.push(reg),dependents.set(dep,list)}let queue=[];for(let reg of registrations)indegree.get(reg.key)===0&&queue.push(reg);let sorted=[];for(;queue.length>0;){let node=queue.shift();sorted.push(node);for(let dependent of dependents.get(node.key)??[]){let next=(indegree.get(dependent.key)??0)-1;indegree.set(dependent.key,next),next===0&&queue.push(dependent)}}if(sorted.length!==registrations.length){let remaining=registrations.filter(r=>(indegree.get(r.key)??0)>0);throw new ContributorCycleError(reconstructCycle(remaining,byKey),route)}return sorted}function reconstructCycle(residual,byKey){let residualKeys=new Set(residual.map(r=>r.key)),start=residual[0].key,path=[start],seen=/* @__PURE__ */ new Set([start]),cursor=start;for(;;){let nextDep=byKey.get(cursor).dependsOn.find(d=>residualKeys.has(d));if(nextDep===void 0)break;if(seen.has(nextDep)){let cycleStart=path.indexOf(nextDep);return[...path.slice(cycleStart),nextDep]}path.push(nextDep),seen.add(nextDep),cursor=nextDep}return path}let _externalContributorSources=[];function _setExternalContributorSources(sources){_externalContributorSources=sources}function assertRouteUnique(registry,method,fullPath,owner){let key=`${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g,`:*`)}`,prior=registry.get(key);if(prior!==void 0)throw duplicateRouteError(method.toUpperCase(),fullPath,owner,prior);registry.set(key,owner)}function buildRouteTable(controllerClass,options={}){let container=Container.getInstance(),externalSources=options.externalSources??_externalContributorSources,routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classMiddlewares=getClassMeta(METADATA.CLASS_MIDDLEWARES,controllerClass,[]),classContributors=getClassMeta(METADATA.CLASS_CONTRIBUTORS,controllerClass,[]),classFlagDeclarations=getClassFlagDeclarations(controllerClass),mountFlagDeclarations=toFlagDeclarations(options.mountFlags,`${controllerClass?.name??`controller`} mount flags`);registerMountFlags(controllerClass,mountFlagDeclarations);let entries=[];for(let route of routes){let method=route.method.toUpperCase(),fullPath=route.path||`/`,methodMiddlewares=getMethodMeta(METADATA.METHOD_MIDDLEWARES,controllerClass,route.handlerName,[]),fileUploadConfig=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName),methodContributors=getMethodMeta(METADATA.METHOD_CONTRIBUTORS,controllerClass,route.handlerName,[]),flags=resolveRouteFlags(classFlagDeclarations,getMethodFlagDeclarations(controllerClass,route.handlerName),mountFlagDeclarations),contributorRunner=null,earlyContributorRunner=null;if(classContributors.length>0||methodContributors.length>0||externalSources.length>0){let sources=[...methodContributors.map((registration,i)=>({source:`method`,registration,label:`${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`})),...classContributors.map((registration,i)=>({source:`class`,registration,label:`${controllerClass.name}.@class#${i}(${registration.key})`})),...externalSources],routeLabel=`${method} ${fullPath}`,{early,late}=splitBeforeValidation(buildPipeline(sources,{route:routeLabel}),routeLabel);early&&(earlyContributorRunner=ctx=>runContributors({pipeline:early,ctx,container,flags})),late&&(contributorRunner=ctx=>runContributors({pipeline:late,ctx,container,flags}))}let handler=ctx=>container.resolve(controllerClass)[route.handlerName](ctx);entries.push({method,path:fullPath,middlewares:[...classMiddlewares,...methodMiddlewares],contributorRunner,earlyContributorRunner,handler,meta:{controller:controllerClass,handlerName:route.handlerName,validation:route.validation,upload:fileUploadConfig,flags}})}return entries}export{buildPipeline as i,assertRouteUnique as n,buildRouteTable as r,_setExternalContributorSources as t};
|
|
12
|
+
//# sourceMappingURL=router-builder-JvMTp50k.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router-builder-DZsCjpeV.mjs","names":[],"sources":["../src/core/contributor-pipeline.ts","../src/http/router-builder.ts"],"sourcesContent":["import type { AnyContributorRegistration } from './context-decorator'\nimport {\n ContributorCycleError,\n DuplicateContributorError,\n MissingContributorError,\n} from './context-errors'\n\n/**\n * Where a contributor was registered, ordered by precedence\n * (lower index wins). Mirrors `architecture.md` §20.4.\n *\n * Levels (highest → lowest precedence):\n * - `'method'` — `@LoadX` on a controller method.\n * - `'class'` — `@LoadX` on a controller class.\n * - `'module'` — returned by `AppModule.contributors?()`. Per-module scope.\n * - `'adapter'` — returned by `AppAdapter.contributors?()`. Cross-cutting, applies\n * to every module's routes.\n * - `'global'` — `ApplicationOptions.contributors`. App-wide default.\n */\nexport type ContributorSource = 'method' | 'class' | 'module' | 'adapter' | 'global'\n\n/** Precedence-ranked source list. Index = precedence; method (0) wins ties. */\nconst PRECEDENCE_ORDER: readonly ContributorSource[] = [\n 'method',\n 'class',\n 'module',\n 'adapter',\n 'global',\n]\n\n/**\n * Input row to {@link buildPipeline}. Pairs a registration with its origin\n * so dedup can apply the precedence rule and diagnostic errors can name\n * the offending site.\n *\n * `registration` is the type-erased {@link AnyContributorRegistration}\n * — the runner doesn't care about each registration's narrow `D` /\n * `Ctx` shape, and adopters frequently mix HTTP / transport-agnostic\n * contributors in the same `externalSources` / `contributors()`\n * array. The narrow generics from `defineContextDecorator` survive\n * at the *definition* site (where `resolve` is typed); they're erased\n * here so collections compose without `as any` casts.\n */\nexport interface SourcedRegistration {\n registration: AnyContributorRegistration\n source: ContributorSource\n /** Human label surfaced in DuplicateContributorError messages. */\n label?: string\n}\n\n/**\n * Resolved, validated, topo-sorted pipeline ready for execution.\n *\n * `contributors` is in execution order — every entry's `dependsOn`\n * keys appear earlier. `keys` is the set of all keys this pipeline\n * produces; the runner uses it for fast existence checks and the\n * builder uses it for missing-dep validation.\n */\nexport interface ContributorPipeline {\n readonly contributors: readonly AnyContributorRegistration[]\n readonly keys: ReadonlySet<string>\n}\n\nexport interface BuildPipelineOptions {\n /** Optional route identifier surfaced in diagnostic errors (e.g., 'GET /projects/:id'). */\n route?: string\n}\n\n/**\n * Build, validate, and topo-sort a contributor pipeline.\n *\n * Pipeline:\n *\n * 1. Group sources by precedence (method > class > module > adapter > global).\n * 2. Within each precedence level, two contributors with the same `key`\n * raise {@link DuplicateContributorError}.\n * 3. Across levels, the highest-precedence contributor wins each key\n * (lower-precedence duplicates are silently dropped — that is the\n * documented override mechanism, not an error).\n * 4. Every key referenced by `dependsOn` must be produced by the\n * surviving set, otherwise {@link MissingContributorError}.\n * 5. Kahn's algorithm topo-sorts the survivors. Any nodes left after\n * the queue drains form a cycle → {@link ContributorCycleError}\n * with a reconstructed path.\n *\n * Pure function — no DI, no I/O. Safe to call once at startup per\n * route and cache the result.\n */\nexport function buildPipeline(\n sources: readonly SourcedRegistration[],\n options: BuildPipelineOptions = {},\n): ContributorPipeline {\n const route = options.route\n\n // Step 1: group by source.\n const bySource = new Map<ContributorSource, SourcedRegistration[]>()\n for (const src of PRECEDENCE_ORDER) bySource.set(src, [])\n for (const entry of sources) bySource.get(entry.source)!.push(entry)\n\n // Step 2 + 3: dedup with precedence. We walk highest precedence first,\n // claim its keys, and drop lower-precedence rows that conflict.\n const winners = new Map<string, SourcedRegistration>()\n for (const src of PRECEDENCE_ORDER) {\n const rows = bySource.get(src)!\n const keysAtThisLevel = new Map<string, SourcedRegistration[]>()\n for (const row of rows) {\n const key = row.registration.key\n const existing = keysAtThisLevel.get(key) ?? []\n existing.push(row)\n keysAtThisLevel.set(key, existing)\n }\n for (const [key, rowsForKey] of keysAtThisLevel) {\n if (rowsForKey.length > 1) {\n const labels = rowsForKey.map((r, i) => r.label ?? `${src}#${i}`)\n throw new DuplicateContributorError(key, labels)\n }\n if (!winners.has(key)) winners.set(key, rowsForKey[0])\n }\n }\n\n const survivors = [...winners.values()].map((r) => r.registration)\n const allKeys = new Set(survivors.map((r) => r.key))\n\n // Step 4: dependsOn validation.\n for (const reg of survivors) {\n for (const dep of reg.dependsOn) {\n if (!allKeys.has(dep)) {\n throw new MissingContributorError(dep, reg.key, route, reg.definedAt)\n }\n }\n }\n\n // Step 5: Kahn topo-sort.\n const sorted = topoSort(survivors, route)\n\n return Object.freeze({\n contributors: Object.freeze(sorted),\n keys: allKeys,\n })\n}\n\n/**\n * Split a built pipeline into the contributors that run before request\n * validation (`beforeValidation: true`) and the rest, which keep their place\n * after `@Middleware()`. Both halves stay in topo order.\n *\n * An early contributor can only depend on other early ones — its dependency\n * would not have run yet otherwise — so that fails here, at boot.\n */\nexport function splitBeforeValidation(\n pipeline: ContributorPipeline,\n route?: string,\n): { early: ContributorPipeline | null; late: ContributorPipeline | null } {\n const early = pipeline.contributors.filter((reg) => reg.beforeValidation)\n if (early.length === 0) return { early: null, late: pipeline }\n\n const earlyKeys = new Set(early.map((reg) => reg.key))\n for (const reg of early) {\n const lateDep = reg.dependsOn.find((dep) => !earlyKeys.has(dep))\n if (lateDep !== undefined) {\n throw new Error(\n `Contributor '${reg.key}'${route ? ` on ${route}` : ''} sets beforeValidation but ` +\n `depends on '${lateDep}', which runs after validation. Set beforeValidation on ` +\n `'${lateDep}' too, or remove it from '${reg.key}'.`,\n )\n }\n }\n\n const late = pipeline.contributors.filter((reg) => !reg.beforeValidation)\n return {\n early: Object.freeze({ contributors: Object.freeze(early), keys: earlyKeys }),\n late:\n late.length === 0\n ? null\n : Object.freeze({\n contributors: Object.freeze(late),\n keys: new Set(late.map((reg) => reg.key)),\n }),\n }\n}\n\n/**\n * Kahn's algorithm with deterministic ordering on ties.\n *\n * Nodes with no remaining incoming edges enter the queue in input\n * order. Cycles surface as nodes still present after the queue\n * drains; we reconstruct one cycle path for diagnostics.\n */\nfunction topoSort(\n registrations: readonly AnyContributorRegistration[],\n route: string | undefined,\n): AnyContributorRegistration[] {\n // indegree[key] = number of unresolved dependsOn entries for key\n const indegree = new Map<string, number>()\n // dependents[depKey] = list of registrations that depend on depKey\n const dependents = new Map<string, AnyContributorRegistration[]>()\n const byKey = new Map<string, AnyContributorRegistration>()\n\n for (const reg of registrations) {\n indegree.set(reg.key, reg.dependsOn.length)\n byKey.set(reg.key, reg)\n }\n for (const reg of registrations) {\n for (const dep of reg.dependsOn) {\n const list = dependents.get(dep) ?? []\n list.push(reg)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: registrations with zero indegree, preserving input order.\n const queue: AnyContributorRegistration[] = []\n for (const reg of registrations) {\n if (indegree.get(reg.key) === 0) queue.push(reg)\n }\n\n const sorted: AnyContributorRegistration[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.key) ?? []) {\n const next = (indegree.get(dependent.key) ?? 0) - 1\n indegree.set(dependent.key, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== registrations.length) {\n const remaining = registrations.filter((r) => (indegree.get(r.key) ?? 0) > 0)\n throw new ContributorCycleError(reconstructCycle(remaining, byKey), route)\n }\n\n return sorted\n}\n\n/**\n * Reconstruct one cycle path from the residual graph.\n *\n * Walks from any remaining node, following its first dependency that's\n * still in the residual set, until we revisit a node. The path returned\n * starts and ends with the same key so the message reads naturally:\n * `tenant → project → tenant`.\n */\nfunction reconstructCycle(\n residual: readonly AnyContributorRegistration[],\n byKey: ReadonlyMap<string, AnyContributorRegistration>,\n): string[] {\n const residualKeys = new Set(residual.map((r) => r.key))\n const start = residual[0].key\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor: string = start\n\n while (true) {\n const reg = byKey.get(cursor)!\n const nextDep = reg.dependsOn.find((d) => residualKeys.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n // Trim the prefix that isn't part of the cycle.\n const cycleStart = path.indexOf(nextDep)\n return [...path.slice(cycleStart), nextDep]\n }\n path.push(nextDep)\n seen.add(nextDep)\n cursor = nextDep\n }\n\n // Fallback — couldn't reconstruct a strict cycle, return what we walked.\n return path\n}\n","// Concrete-module imports (NOT the `../core` barrel): the barrel re-exports\n// the asset manager, whose eager `node:fs` import would poison the edge-safe\n// `@forinda/kickjs/web` entry graph that flows through this file.\nimport {\n buildPipeline,\n splitBeforeValidation,\n type SourcedRegistration,\n} from '../core/contributor-pipeline'\nimport { runContributors } from '../core/contributor-runner'\nimport { Container } from '../core/container'\nimport { METADATA } from '../core/interfaces'\nimport type { ContributorRegistration } from '../core/context-decorator'\nimport type { FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core/decorators'\nimport { getClassMeta, getMethodMeta, getMethodMetaOrUndefined } from '../core/metadata'\nimport {\n getClassFlagDeclarations,\n getMethodFlagDeclarations,\n registerMountFlags,\n resolveRouteFlags,\n toFlagDeclarations,\n type RouteFlagDeclarations,\n} from '../core/route-flag'\nimport { duplicateRouteError } from '../core/kick-errors'\nimport type { CtxHandler, RouteEntry, RouteMethod } from './runtime'\n\n/**\n * Per-module SourcedRegistration[] threaded through the route-mount loop by\n * Application.setup(). Carries module + adapter + global contributors so\n * buildRouteTable() can merge them with class + method ones into a single pipeline.\n *\n * Module setup is sequential, so this slot is race-free. Cleared in a finally\n * block after each module mounts. Outside of Application setup the slot is\n * empty — direct buildRouteTable()/buildRoutes() callers (mostly tests) see only\n * class + method contributors unless they pass `externalSources` explicitly.\n *\n * Same idiom as `Container._requestStoreProvider`: an internal escape hatch for\n * cross-module wiring without inversion-of-control gymnastics.\n *\n * @internal\n */\nlet _externalContributorSources: readonly SourcedRegistration[] = []\n\n/** @internal — set by Application.setup() before each `mod.routes()` call. */\nexport function _setExternalContributorSources(sources: readonly SourcedRegistration[]): void {\n _externalContributorSources = sources\n}\n\n/**\n * Boot-time duplicate-route guard shared by the node and web mount loops.\n *\n * Registers `METHOD /mounted/path` into `registry` and throws\n * {@link duplicateRouteError} (KICK006) if another handler already claimed\n * it. Param *names* are ignored — `/tasks/:id` and `/tasks/:taskId` are the\n * same route at dispatch time, so they collide here too.\n *\n * @internal\n */\nexport function assertRouteUnique(\n registry: Map<string, string>,\n method: string,\n fullPath: string,\n owner: string,\n): void {\n const key = `${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g, ':*')}`\n const prior = registry.get(key)\n if (prior !== undefined) {\n throw duplicateRouteError(method.toUpperCase(), fullPath, owner, prior)\n }\n registry.set(key, owner)\n}\n\nexport interface BuildRoutesOptions {\n /**\n * Extra contributors to merge into the per-route pipeline at their declared\n * precedence levels. Pass explicitly when calling buildRouteTable/buildRoutes\n * outside the Application route-mount loop (typically in tests). When omitted,\n * falls back to the slot set by Application.setup().\n */\n externalSources?: readonly SourcedRegistration[]\n /**\n * Flags the mounting module declares for every route on this controller\n * (`ModuleRoutes.flags`). Lowest precedence — a class or method declaration\n * of the same name wins, and `@Flag.off` on a method removes it.\n */\n mountFlags?: RouteFlagDeclarations\n}\n\n/**\n * Turn a controller class decorated with @Get, @Post, etc. into a plain-data\n * {@link RouteEntry}[] — the engine-neutral route table an {@link HttpRuntime}\n * materializes.\n *\n * What used to be per-handler Express `(req, res, next)` closures is now\n * captured as data: `middlewares` keep their `(ctx, next)` shape, the contributor\n * pipeline is pre-built into a `contributorRunner` closure, validation / upload\n * stay as metadata, and the terminal `handler` resolves the controller per-request\n * (to respect DI scoping) and invokes it. The runtime owns how these get wrapped\n * onto its engine.\n *\n * Routes use only the method-level decorator paths (e.g. @Get('/me') → '/me').\n * The @Controller path is NOT baked in — it is metadata for Swagger/introspection.\n * The module's routes().path is the single source of truth for the mount prefix,\n * which avoids path doubling when both the module and controller specify the path.\n *\n * The contributor pipeline is built here, so a cycle or missing `dependsOn`\n * throws at table-build time (boot) rather than on first request.\n */\nexport function buildRouteTable(\n controllerClass: any,\n options: BuildRoutesOptions = {},\n): RouteEntry[] {\n const container = Container.getInstance()\n const externalSources = options.externalSources ?? _externalContributorSources\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n // Class-level middleware\n const classMiddlewares: MiddlewareHandler[] = getClassMeta<MiddlewareHandler[]>(\n METADATA.CLASS_MIDDLEWARES,\n controllerClass,\n [],\n )\n\n // Class-level Context Contributors (#107) — applied to every method on the\n // controller. Method-level contributors are added per-route below.\n const classContributors: ContributorRegistration[] = getClassMeta<ContributorRegistration[]>(\n METADATA.CLASS_CONTRIBUTORS,\n controllerClass,\n [],\n )\n\n // Route flags declared on the controller — inherited by every method below\n // unless a method declares the same flag `false` (see core/route-flag.ts).\n const classFlagDeclarations = getClassFlagDeclarations(controllerClass)\n\n // Flags the module declared for this whole mount. Recorded against the class\n // so `getRouteFlags()` — the resolver Swagger and DevTools use, which sees no\n // mount — reports what the runtime actually resolved.\n const mountFlagDeclarations = toFlagDeclarations(\n options.mountFlags,\n `${controllerClass?.name ?? 'controller'} mount flags`,\n )\n registerMountFlags(controllerClass, mountFlagDeclarations)\n\n const entries: RouteEntry[] = []\n\n for (const route of routes) {\n const method = route.method.toUpperCase() as RouteMethod\n const fullPath = route.path || '/'\n\n // Method-level middleware\n const methodMiddlewares: MiddlewareHandler[] = getMethodMeta<MiddlewareHandler[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // @FileUpload decorator — runtime supplies the upload backend.\n const fileUploadConfig = getMethodMetaOrUndefined<FileUploadConfig>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n\n // Context Contributor pipeline (#107) — class + method contributors,\n // built once at mount time so cycle/missing-dep failures abort boot.\n const methodContributors: ContributorRegistration[] = getMethodMeta<ContributorRegistration[]>(\n METADATA.METHOD_CONTRIBUTORS,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // Flags in force for THIS route: class declarations overridden by method\n // ones. Resolved at build time so the per-request cost is a map lookup.\n const flags = resolveRouteFlags(\n classFlagDeclarations,\n getMethodFlagDeclarations(controllerClass, route.handlerName),\n mountFlagDeclarations,\n )\n\n let contributorRunner: CtxHandler | null = null\n let earlyContributorRunner: CtxHandler | null = null\n if (\n classContributors.length > 0 ||\n methodContributors.length > 0 ||\n externalSources.length > 0\n ) {\n // Labels are surfaced in DuplicateContributorError messages — include\n // the registration's key + the original index so a same-key collision\n // at one precedence level points at the conflicting decorator slot,\n // not just the host method/class.\n const sources: SourcedRegistration[] = [\n ...methodContributors.map((registration, i): SourcedRegistration => ({\n source: 'method',\n registration,\n label: `${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`,\n })),\n ...classContributors.map((registration, i): SourcedRegistration => ({\n source: 'class',\n registration,\n label: `${controllerClass.name}.@class#${i}(${registration.key})`,\n })),\n ...externalSources,\n ]\n const routeLabel = `${method} ${fullPath}`\n const { early, late } = splitBeforeValidation(\n buildPipeline(sources, { route: routeLabel }),\n routeLabel,\n )\n if (early) {\n earlyContributorRunner = (ctx) =>\n runContributors({ pipeline: early, ctx, container, flags })\n }\n if (late) {\n contributorRunner = (ctx) => runContributors({ pipeline: late, ctx, container, flags })\n }\n }\n\n // Terminal handler — resolve controller per-request to respect DI scoping.\n const handler: CtxHandler = (ctx) => container.resolve(controllerClass)[route.handlerName](ctx)\n\n entries.push({\n method,\n path: fullPath,\n middlewares: [...classMiddlewares, ...methodMiddlewares],\n contributorRunner,\n earlyContributorRunner,\n handler,\n meta: {\n controller: controllerClass,\n handlerName: route.handlerName,\n validation: route.validation,\n upload: fileUploadConfig,\n flags,\n },\n })\n }\n\n return entries\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"router-builder-JvMTp50k.mjs","names":[],"sources":["../src/core/contributor-pipeline.ts","../src/http/router-builder.ts"],"sourcesContent":["import type { AnyContributorRegistration } from './context-decorator'\nimport {\n ContributorCycleError,\n DuplicateContributorError,\n MissingContributorError,\n} from './context-errors'\n\n/**\n * Where a contributor was registered, ordered by precedence\n * (lower index wins). Mirrors `architecture.md` §20.4.\n *\n * Levels (highest → lowest precedence):\n * - `'method'` — `@LoadX` on a controller method.\n * - `'class'` — `@LoadX` on a controller class.\n * - `'module'` — returned by `AppModule.contributors?()`. Per-module scope.\n * - `'adapter'` — returned by `AppAdapter.contributors?()`. Cross-cutting, applies\n * to every module's routes.\n * - `'global'` — `ApplicationOptions.contributors`. App-wide default.\n */\nexport type ContributorSource = 'method' | 'class' | 'module' | 'adapter' | 'global'\n\n/** Precedence-ranked source list. Index = precedence; method (0) wins ties. */\nconst PRECEDENCE_ORDER: readonly ContributorSource[] = [\n 'method',\n 'class',\n 'module',\n 'adapter',\n 'global',\n]\n\n/**\n * Input row to {@link buildPipeline}. Pairs a registration with its origin\n * so dedup can apply the precedence rule and diagnostic errors can name\n * the offending site.\n *\n * `registration` is the type-erased {@link AnyContributorRegistration}\n * — the runner doesn't care about each registration's narrow `D` /\n * `Ctx` shape, and adopters frequently mix HTTP / transport-agnostic\n * contributors in the same `externalSources` / `contributors()`\n * array. The narrow generics from `defineContextDecorator` survive\n * at the *definition* site (where `resolve` is typed); they're erased\n * here so collections compose without `as any` casts.\n */\nexport interface SourcedRegistration {\n registration: AnyContributorRegistration\n source: ContributorSource\n /** Human label surfaced in DuplicateContributorError messages. */\n label?: string\n}\n\n/**\n * Resolved, validated, topo-sorted pipeline ready for execution.\n *\n * `contributors` is in execution order — every entry's `dependsOn`\n * keys appear earlier. `keys` is the set of all keys this pipeline\n * produces; the runner uses it for fast existence checks and the\n * builder uses it for missing-dep validation.\n */\nexport interface ContributorPipeline {\n readonly contributors: readonly AnyContributorRegistration[]\n readonly keys: ReadonlySet<string>\n}\n\nexport interface BuildPipelineOptions {\n /** Optional route identifier surfaced in diagnostic errors (e.g., 'GET /projects/:id'). */\n route?: string\n}\n\n/**\n * Build, validate, and topo-sort a contributor pipeline.\n *\n * Pipeline:\n *\n * 1. Group sources by precedence (method > class > module > adapter > global).\n * 2. Within each precedence level, two contributors with the same `key`\n * raise {@link DuplicateContributorError}.\n * 3. Across levels, the highest-precedence contributor wins each key\n * (lower-precedence duplicates are silently dropped — that is the\n * documented override mechanism, not an error).\n * 4. Every key referenced by `dependsOn` must be produced by the\n * surviving set, otherwise {@link MissingContributorError}.\n * 5. Kahn's algorithm topo-sorts the survivors. Any nodes left after\n * the queue drains form a cycle → {@link ContributorCycleError}\n * with a reconstructed path.\n *\n * Pure function — no DI, no I/O. Safe to call once at startup per\n * route and cache the result.\n */\nexport function buildPipeline(\n sources: readonly SourcedRegistration[],\n options: BuildPipelineOptions = {},\n): ContributorPipeline {\n const route = options.route\n\n // Step 1: group by source.\n const bySource = new Map<ContributorSource, SourcedRegistration[]>()\n for (const src of PRECEDENCE_ORDER) bySource.set(src, [])\n for (const entry of sources) bySource.get(entry.source)!.push(entry)\n\n // Step 2 + 3: dedup with precedence. We walk highest precedence first,\n // claim its keys, and drop lower-precedence rows that conflict.\n const winners = new Map<string, SourcedRegistration>()\n for (const src of PRECEDENCE_ORDER) {\n const rows = bySource.get(src)!\n const keysAtThisLevel = new Map<string, SourcedRegistration[]>()\n for (const row of rows) {\n const key = row.registration.key\n const existing = keysAtThisLevel.get(key) ?? []\n existing.push(row)\n keysAtThisLevel.set(key, existing)\n }\n for (const [key, rowsForKey] of keysAtThisLevel) {\n if (rowsForKey.length > 1) {\n const labels = rowsForKey.map((r, i) => r.label ?? `${src}#${i}`)\n throw new DuplicateContributorError(key, labels)\n }\n if (!winners.has(key)) winners.set(key, rowsForKey[0])\n }\n }\n\n const survivors = [...winners.values()].map((r) => r.registration)\n const allKeys = new Set(survivors.map((r) => r.key))\n\n // Step 4: dependsOn validation.\n for (const reg of survivors) {\n for (const dep of reg.dependsOn) {\n if (!allKeys.has(dep)) {\n throw new MissingContributorError(dep, reg.key, route, reg.definedAt)\n }\n }\n }\n\n // Step 5: Kahn topo-sort.\n const sorted = topoSort(survivors, route)\n\n return Object.freeze({\n contributors: Object.freeze(sorted),\n keys: allKeys,\n })\n}\n\n/**\n * Split a built pipeline into the contributors that run before request\n * validation (`beforeValidation: true`) and the rest, which keep their place\n * after `@Middleware()`. Both halves stay in topo order.\n *\n * An early contributor can only depend on other early ones — its dependency\n * would not have run yet otherwise — so that fails here, at boot.\n */\nexport function splitBeforeValidation(\n pipeline: ContributorPipeline,\n route?: string,\n): { early: ContributorPipeline | null; late: ContributorPipeline | null } {\n const early = pipeline.contributors.filter((reg) => reg.beforeValidation)\n if (early.length === 0) return { early: null, late: pipeline }\n\n const earlyKeys = new Set(early.map((reg) => reg.key))\n for (const reg of early) {\n const lateDep = reg.dependsOn.find((dep) => !earlyKeys.has(dep))\n if (lateDep !== undefined) {\n throw new Error(\n `Contributor '${reg.key}'${route ? ` on ${route}` : ''} sets beforeValidation but ` +\n `depends on '${lateDep}', which runs after validation. Set beforeValidation on ` +\n `'${lateDep}' too, or remove it from '${reg.key}'.`,\n )\n }\n }\n\n const late = pipeline.contributors.filter((reg) => !reg.beforeValidation)\n return {\n early: Object.freeze({ contributors: Object.freeze(early), keys: earlyKeys }),\n late:\n late.length === 0\n ? null\n : Object.freeze({\n contributors: Object.freeze(late),\n keys: new Set(late.map((reg) => reg.key)),\n }),\n }\n}\n\n/**\n * Kahn's algorithm with deterministic ordering on ties.\n *\n * Nodes with no remaining incoming edges enter the queue in input\n * order. Cycles surface as nodes still present after the queue\n * drains; we reconstruct one cycle path for diagnostics.\n */\nfunction topoSort(\n registrations: readonly AnyContributorRegistration[],\n route: string | undefined,\n): AnyContributorRegistration[] {\n // indegree[key] = number of unresolved dependsOn entries for key\n const indegree = new Map<string, number>()\n // dependents[depKey] = list of registrations that depend on depKey\n const dependents = new Map<string, AnyContributorRegistration[]>()\n const byKey = new Map<string, AnyContributorRegistration>()\n\n for (const reg of registrations) {\n indegree.set(reg.key, reg.dependsOn.length)\n byKey.set(reg.key, reg)\n }\n for (const reg of registrations) {\n for (const dep of reg.dependsOn) {\n const list = dependents.get(dep) ?? []\n list.push(reg)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: registrations with zero indegree, preserving input order.\n const queue: AnyContributorRegistration[] = []\n for (const reg of registrations) {\n if (indegree.get(reg.key) === 0) queue.push(reg)\n }\n\n const sorted: AnyContributorRegistration[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.key) ?? []) {\n const next = (indegree.get(dependent.key) ?? 0) - 1\n indegree.set(dependent.key, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== registrations.length) {\n const remaining = registrations.filter((r) => (indegree.get(r.key) ?? 0) > 0)\n throw new ContributorCycleError(reconstructCycle(remaining, byKey), route)\n }\n\n return sorted\n}\n\n/**\n * Reconstruct one cycle path from the residual graph.\n *\n * Walks from any remaining node, following its first dependency that's\n * still in the residual set, until we revisit a node. The path returned\n * starts and ends with the same key so the message reads naturally:\n * `tenant → project → tenant`.\n */\nfunction reconstructCycle(\n residual: readonly AnyContributorRegistration[],\n byKey: ReadonlyMap<string, AnyContributorRegistration>,\n): string[] {\n const residualKeys = new Set(residual.map((r) => r.key))\n const start = residual[0].key\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor: string = start\n\n while (true) {\n const reg = byKey.get(cursor)!\n const nextDep = reg.dependsOn.find((d) => residualKeys.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n // Trim the prefix that isn't part of the cycle.\n const cycleStart = path.indexOf(nextDep)\n return [...path.slice(cycleStart), nextDep]\n }\n path.push(nextDep)\n seen.add(nextDep)\n cursor = nextDep\n }\n\n // Fallback — couldn't reconstruct a strict cycle, return what we walked.\n return path\n}\n","// Concrete-module imports (NOT the `../core` barrel): the barrel re-exports\n// the asset manager, whose eager `node:fs` import would poison the edge-safe\n// `@forinda/kickjs/web` entry graph that flows through this file.\nimport {\n buildPipeline,\n splitBeforeValidation,\n type SourcedRegistration,\n} from '../core/contributor-pipeline'\nimport { runContributors } from '../core/contributor-runner'\nimport { Container } from '../core/container'\nimport { METADATA } from '../core/interfaces'\nimport type { ContributorRegistration } from '../core/context-decorator'\nimport type { FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core/decorators'\nimport { getClassMeta, getMethodMeta, getMethodMetaOrUndefined } from '../core/metadata'\nimport {\n getClassFlagDeclarations,\n getMethodFlagDeclarations,\n registerMountFlags,\n resolveRouteFlags,\n toFlagDeclarations,\n type RouteFlagDeclarations,\n} from '../core/route-flag'\nimport { duplicateRouteError } from '../core/kick-errors'\nimport type { CtxHandler, RouteEntry, RouteMethod } from './runtime'\n\n/**\n * Per-module SourcedRegistration[] threaded through the route-mount loop by\n * Application.setup(). Carries module + adapter + global contributors so\n * buildRouteTable() can merge them with class + method ones into a single pipeline.\n *\n * Module setup is sequential, so this slot is race-free. Cleared in a finally\n * block after each module mounts. Outside of Application setup the slot is\n * empty — direct buildRouteTable()/buildRoutes() callers (mostly tests) see only\n * class + method contributors unless they pass `externalSources` explicitly.\n *\n * Same idiom as `Container._requestStoreProvider`: an internal escape hatch for\n * cross-module wiring without inversion-of-control gymnastics.\n *\n * @internal\n */\nlet _externalContributorSources: readonly SourcedRegistration[] = []\n\n/** @internal — set by Application.setup() before each `mod.routes()` call. */\nexport function _setExternalContributorSources(sources: readonly SourcedRegistration[]): void {\n _externalContributorSources = sources\n}\n\n/**\n * Boot-time duplicate-route guard shared by the node and web mount loops.\n *\n * Registers `METHOD /mounted/path` into `registry` and throws\n * {@link duplicateRouteError} (KICK006) if another handler already claimed\n * it. Param *names* are ignored — `/tasks/:id` and `/tasks/:taskId` are the\n * same route at dispatch time, so they collide here too.\n *\n * @internal\n */\nexport function assertRouteUnique(\n registry: Map<string, string>,\n method: string,\n fullPath: string,\n owner: string,\n): void {\n const key = `${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g, ':*')}`\n const prior = registry.get(key)\n if (prior !== undefined) {\n throw duplicateRouteError(method.toUpperCase(), fullPath, owner, prior)\n }\n registry.set(key, owner)\n}\n\nexport interface BuildRoutesOptions {\n /**\n * Extra contributors to merge into the per-route pipeline at their declared\n * precedence levels. Pass explicitly when calling buildRouteTable/buildRoutes\n * outside the Application route-mount loop (typically in tests). When omitted,\n * falls back to the slot set by Application.setup().\n */\n externalSources?: readonly SourcedRegistration[]\n /**\n * Flags the mounting module declares for every route on this controller\n * (`ModuleRoutes.flags`). Lowest precedence — a class or method declaration\n * of the same name wins, and `@Flag.off` on a method removes it.\n */\n mountFlags?: RouteFlagDeclarations\n}\n\n/**\n * Turn a controller class decorated with @Get, @Post, etc. into a plain-data\n * {@link RouteEntry}[] — the engine-neutral route table an {@link HttpRuntime}\n * materializes.\n *\n * What used to be per-handler Express `(req, res, next)` closures is now\n * captured as data: `middlewares` keep their `(ctx, next)` shape, the contributor\n * pipeline is pre-built into a `contributorRunner` closure, validation / upload\n * stay as metadata, and the terminal `handler` resolves the controller per-request\n * (to respect DI scoping) and invokes it. The runtime owns how these get wrapped\n * onto its engine.\n *\n * Routes use only the method-level decorator paths (e.g. @Get('/me') → '/me').\n * The @Controller path is NOT baked in — it is metadata for Swagger/introspection.\n * The module's routes().path is the single source of truth for the mount prefix,\n * which avoids path doubling when both the module and controller specify the path.\n *\n * The contributor pipeline is built here, so a cycle or missing `dependsOn`\n * throws at table-build time (boot) rather than on first request.\n */\nexport function buildRouteTable(\n controllerClass: any,\n options: BuildRoutesOptions = {},\n): RouteEntry[] {\n const container = Container.getInstance()\n const externalSources = options.externalSources ?? _externalContributorSources\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n // Class-level middleware\n const classMiddlewares: MiddlewareHandler[] = getClassMeta<MiddlewareHandler[]>(\n METADATA.CLASS_MIDDLEWARES,\n controllerClass,\n [],\n )\n\n // Class-level Context Contributors (#107) — applied to every method on the\n // controller. Method-level contributors are added per-route below.\n const classContributors: ContributorRegistration[] = getClassMeta<ContributorRegistration[]>(\n METADATA.CLASS_CONTRIBUTORS,\n controllerClass,\n [],\n )\n\n // Route flags declared on the controller — inherited by every method below\n // unless a method declares the same flag `false` (see core/route-flag.ts).\n const classFlagDeclarations = getClassFlagDeclarations(controllerClass)\n\n // Flags the module declared for this whole mount. Recorded against the class\n // so `getRouteFlags()` — the resolver Swagger and DevTools use, which sees no\n // mount — reports what the runtime actually resolved.\n const mountFlagDeclarations = toFlagDeclarations(\n options.mountFlags,\n `${controllerClass?.name ?? 'controller'} mount flags`,\n )\n registerMountFlags(controllerClass, mountFlagDeclarations)\n\n const entries: RouteEntry[] = []\n\n for (const route of routes) {\n const method = route.method.toUpperCase() as RouteMethod\n const fullPath = route.path || '/'\n\n // Method-level middleware\n const methodMiddlewares: MiddlewareHandler[] = getMethodMeta<MiddlewareHandler[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // @FileUpload decorator — runtime supplies the upload backend.\n const fileUploadConfig = getMethodMetaOrUndefined<FileUploadConfig>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n\n // Context Contributor pipeline (#107) — class + method contributors,\n // built once at mount time so cycle/missing-dep failures abort boot.\n const methodContributors: ContributorRegistration[] = getMethodMeta<ContributorRegistration[]>(\n METADATA.METHOD_CONTRIBUTORS,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // Flags in force for THIS route: class declarations overridden by method\n // ones. Resolved at build time so the per-request cost is a map lookup.\n const flags = resolveRouteFlags(\n classFlagDeclarations,\n getMethodFlagDeclarations(controllerClass, route.handlerName),\n mountFlagDeclarations,\n )\n\n let contributorRunner: CtxHandler | null = null\n let earlyContributorRunner: CtxHandler | null = null\n if (\n classContributors.length > 0 ||\n methodContributors.length > 0 ||\n externalSources.length > 0\n ) {\n // Labels are surfaced in DuplicateContributorError messages — include\n // the registration's key + the original index so a same-key collision\n // at one precedence level points at the conflicting decorator slot,\n // not just the host method/class.\n const sources: SourcedRegistration[] = [\n ...methodContributors.map((registration, i): SourcedRegistration => ({\n source: 'method',\n registration,\n label: `${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`,\n })),\n ...classContributors.map((registration, i): SourcedRegistration => ({\n source: 'class',\n registration,\n label: `${controllerClass.name}.@class#${i}(${registration.key})`,\n })),\n ...externalSources,\n ]\n const routeLabel = `${method} ${fullPath}`\n const { early, late } = splitBeforeValidation(\n buildPipeline(sources, { route: routeLabel }),\n routeLabel,\n )\n if (early) {\n earlyContributorRunner = (ctx) =>\n runContributors({ pipeline: early, ctx, container, flags })\n }\n if (late) {\n contributorRunner = (ctx) => runContributors({ pipeline: late, ctx, container, flags })\n }\n }\n\n // Terminal handler — resolve controller per-request to respect DI scoping.\n const handler: CtxHandler = (ctx) => container.resolve(controllerClass)[route.handlerName](ctx)\n\n entries.push({\n method,\n path: fullPath,\n middlewares: [...classMiddlewares, ...methodMiddlewares],\n contributorRunner,\n earlyContributorRunner,\n handler,\n meta: {\n controller: controllerClass,\n handlerName: route.handlerName,\n validation: route.validation,\n upload: fileUploadConfig,\n flags,\n },\n })\n }\n\n return entries\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { B as SourcedRegistration, g as RouteEntry } from "./adapter-
|
|
2
|
+
import { B as SourcedRegistration, g as RouteEntry } from "./adapter-ClEh-Oos.mjs";
|
|
3
3
|
import { s as RouteFlagDeclarations } from "./route-flag-B1lQht6v.mjs";
|
|
4
4
|
//#region src/http/router-builder.d.ts
|
|
5
5
|
/** @internal — set by Application.setup() before each `mod.routes()` call. */
|
|
@@ -53,4 +53,4 @@ interface BuildRoutesOptions {
|
|
|
53
53
|
declare function buildRouteTable(controllerClass: any, options?: BuildRoutesOptions): RouteEntry[];
|
|
54
54
|
//#endregion
|
|
55
55
|
export { buildRouteTable as i, _setExternalContributorSources as n, assertRouteUnique as r, BuildRoutesOptions as t };
|
|
56
|
-
//# sourceMappingURL=router-builder-
|
|
56
|
+
//# sourceMappingURL=router-builder-zWuVIOmd.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router-builder-
|
|
1
|
+
{"version":3,"file":"router-builder-zWuVIOmd.d.mts","names":[],"sources":["../src/http/router-builder.ts"],"mappings":";;;;;iBA2CgB,+BAA+B,kBAAkB;;;;;;;;;;;iBAcjD,kBACd,UAAU,qBACV,gBACA,kBACA;UAUe;;;;;;;EAOf,2BAA2B;;;;;;EAM3B,aAAa;;;;;;;;;;;;;;;;;;;;;;iBAuBC,gBACd,sBACA,UAAS,qBACR"}
|
|
@@ -46,8 +46,17 @@ interface SessionOptions {
|
|
|
46
46
|
path?: string;
|
|
47
47
|
domain?: string;
|
|
48
48
|
};
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Where session data lives. Default: in memory, in this process.
|
|
51
|
+
*
|
|
52
|
+
* `'cookie'` keeps it in the session cookie itself, encrypted with
|
|
53
|
+
* AES-256-GCM under a key derived from `secret`: every instance reads the
|
|
54
|
+
* same session with nothing shared between them — serverless functions,
|
|
55
|
+
* several containers. The data has to fit a cookie (about 4 KB).
|
|
56
|
+
*
|
|
57
|
+
* Or pass a store shared between instances (`KvSessionStore`, Redis).
|
|
58
|
+
*/
|
|
59
|
+
store?: SessionStore | 'cookie';
|
|
51
60
|
}
|
|
52
61
|
/**
|
|
53
62
|
* Session management middleware.
|
|
@@ -73,7 +82,7 @@ interface SessionOptions {
|
|
|
73
82
|
* })
|
|
74
83
|
* ```
|
|
75
84
|
*/
|
|
76
|
-
declare function session(options: SessionOptions): (req: Request, res: Response, next: NextFunction) =>
|
|
85
|
+
declare function session(options: SessionOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
77
86
|
//#endregion
|
|
78
87
|
export { session as a, SessionStore as i, SessionData as n, SessionOptions as r, Session as t };
|
|
79
|
-
//# sourceMappingURL=session-
|
|
88
|
+
//# sourceMappingURL=session-B4hOZj90.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-B4hOZj90.d.mts","names":[],"sources":["../src/http/middleware/session.ts"],"mappings":";;;;;;;;;;;;;UAyBiB;GACd;;UAGc;EACf,IAAI,cAAc,QAAQ;EAC1B,IAAI,aAAa,MAAM,aAAa,iBAAiB;EACrD,QAAQ,cAAc;EACtB,OAAO,aAAa,iBAAiB;;UAGtB;EACf;EACA,MAAM;EACN,cAAc;EACd,WAAW;EACX,QAAQ;;UAGO;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;IACE;IACA;IACA;IACA;IACA;;;;;;;;;;;;EAYF,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiHM,QAAQ,SAAS,kBAAc,KA2LhC,SAAO,KAAO,UAAQ,MAAQ"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { g as FileUploadConfig, o as BaseUploadOptions } from "./decorators-BxNSz9Qt.mjs";
|
|
3
|
-
import "./adapter-
|
|
3
|
+
import "./adapter-ClEh-Oos.mjs";
|
|
4
4
|
import { NextFunction, Request, RequestHandler, Response } from "express";
|
|
5
5
|
import { Options } from "multer";
|
|
6
6
|
//#region src/http/middleware/upload-config.d.ts
|
|
@@ -101,4 +101,4 @@ declare const upload: {
|
|
|
101
101
|
};
|
|
102
102
|
//#endregion
|
|
103
103
|
export { RawUploadPart as a, buildFileTypeFilter as c, upload as i, resolveMimeTypes as l, buildUploadMiddleware as n, UploadedFileLike as o, cleanupFiles as r, applyUploadConfig as s, UploadOptions as t };
|
|
104
|
-
//# sourceMappingURL=upload-
|
|
104
|
+
//# sourceMappingURL=upload-CiMtdXWD.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-
|
|
1
|
+
{"version":3,"file":"upload-CiMtdXWD.d.mts","names":["MulterOptions"],"sources":["../src/http/middleware/upload-config.ts","../src/http/middleware/upload.ts"],"mappings":";;;;;;;;;;;;;;;;iBAuEgB,iBAAiB;;;;;;;;iBAcjB,oBACd,cAAc,mCACd,gBAAgB,0BACd,kBAAkB;;UAYL;EACf;EACA;EACA;EACA;EACA;EACA,QAAQ;;;UAIO;EACf;EACA;EACA;EACA,QAAQ;;;;;;;;;iBAYM,kBACd,OAAO,iBACP,QAAQ;EACL,OAAO;EAAkB,QAAQ;;;;;;;;;UC/ErB,sBAAsB;;EAErC,UAAUA;;EAEV;;iBAwGO,OAAO,mBAAmB,UAAS,gBAAqB;;;;iBAQxD,MAAM,mBAAmB,mBAAe,UAAS,gBAAqB;;;;iBAUtE,KAAK,UAAS,gBAAqB;;;;;;;;;;;;;;;iBAmB5B,iBAAY,KACb,SAAO,KAAO,UAAQ,MAAQ;;;;;;iBA+B7B,sBAAsB,QAAQ,mBAAmB;;cAmBpD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v10.
|
|
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
|
import{HttpException,HttpStatus}from"./core/errors.mjs";const MIME_MAP={jpg:`image/jpeg`,jpeg:`image/jpeg`,png:`image/png`,gif:`image/gif`,webp:`image/webp`,svg:`image/svg+xml`,bmp:`image/bmp`,ico:`image/x-icon`,tiff:`image/tiff`,tif:`image/tiff`,avif:`image/avif`,pdf:`application/pdf`,doc:`application/msword`,docx:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,xls:`application/vnd.ms-excel`,xlsx:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,ppt:`application/vnd.ms-powerpoint`,pptx:`application/vnd.openxmlformats-officedocument.presentationml.presentation`,odt:`application/vnd.oasis.opendocument.text`,ods:`application/vnd.oasis.opendocument.spreadsheet`,odp:`application/vnd.oasis.opendocument.presentation`,rtf:`application/rtf`,txt:`text/plain`,csv:`text/csv`,zip:`application/zip`,gz:`application/gzip`,tar:`application/x-tar`,rar:`application/vnd.rar`,"7z":`application/x-7z-compressed`,mp3:`audio/mpeg`,wav:`audio/wav`,ogg:`audio/ogg`,flac:`audio/flac`,aac:`audio/aac`,mp4:`video/mp4`,webm:`video/webm`,avi:`video/x-msvideo`,mov:`video/quicktime`,mkv:`video/x-matroska`,json:`application/json`,xml:`application/xml`,html:`text/html`};function resolveMimeTypes(types){return types.map(t=>{let lower=t.toLowerCase().replace(/^\./,``);return MIME_MAP[lower]??t})}function buildFileTypeFilter(allowedTypes,customMimeMap){if(!allowedTypes)return()=>!0;if(typeof allowedTypes==`function`)return allowedTypes;let mimeMap=customMimeMap?{...MIME_MAP,...customMimeMap}:MIME_MAP,resolved=allowedTypes.map(t=>mimeMap[t.toLowerCase().replace(/^\./,``)]??t);return mimetype=>resolved.some(type=>type.endsWith(`/*`)?mimetype.startsWith(type.replace(`/*`,`/`)):mimetype===type)}function applyUploadConfig(parts,config){if(config.mode===`none`)return{};let field=config.fieldName??`file`,maxSize=config.maxSize??5242880,typeAllowed=buildFileTypeFilter(config.allowedTypes,config.customMimeMap),files=parts.filter(p=>p.fieldname===field).map(p=>{if(!typeAllowed(p.mimetype,p.filename))throw new HttpException(HttpStatus.UNSUPPORTED_MEDIA_TYPE,`File type ${p.mimetype} is not allowed`);if(p.buffer.length>maxSize)throw new HttpException(HttpStatus.PAYLOAD_TOO_LARGE,`File ${p.filename} exceeds the ${maxSize}-byte limit`);return{fieldname:p.fieldname,originalname:p.filename,encoding:`7bit`,mimetype:p.mimetype,size:p.buffer.length,buffer:p.buffer}});if(config.mode===`single`)return{file:files[0]};let maxCount=config.maxCount??10;return{files:files.slice(0,maxCount)}}export{buildFileTypeFilter as n,resolveMimeTypes as r,applyUploadConfig as t};
|
|
12
|
-
//# sourceMappingURL=upload-config-
|
|
12
|
+
//# sourceMappingURL=upload-config-Dz2INCaa.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-config-
|
|
1
|
+
{"version":3,"file":"upload-config-Dz2INCaa.mjs","names":[],"sources":["../src/http/middleware/upload-config.ts"],"sourcesContent":["// Pure (edge-safe) upload core — the engine-neutral half of the upload\n// middleware. Extracted from `upload.ts` so the `@forinda/kickjs/web` entry\n// graph can validate/shape buffered multipart parts WITHOUT dragging in\n// upload.ts's node-only surface (multer peer loading via `node:module`,\n// disk cleanup via `node:fs/promises`). `upload.ts` re-exports everything\n// here, so existing import sites are unchanged.\n\nimport { HttpException, HttpStatus } from '../../core/errors'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core/decorators'\n\nexport const MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * Build a runtime-agnostic `(mimetype, filename) => boolean` file-type filter\n * from `allowedTypes` (a predicate, or a list of extensions / MIME types /\n * wildcards). Returned by both the multer middleware (Express) and the\n * Fastify / h3 / web multipart paths so the accept/reject rule stays\n * identical across engines. No `allowedTypes` → accept everything.\n */\nexport function buildFileTypeFilter(\n allowedTypes: BaseUploadOptions['allowedTypes'],\n customMimeMap?: Record<string, string>,\n): (mimetype: string, filename: string) => boolean {\n if (!allowedTypes) return () => true\n if (typeof allowedTypes === 'function') return allowedTypes\n const mimeMap = customMimeMap ? { ...MIME_MAP, ...customMimeMap } : MIME_MAP\n const resolved = allowedTypes.map((t) => mimeMap[t.toLowerCase().replace(/^\\./, '')] ?? t)\n return (mimetype: string) =>\n resolved.some((type) =>\n type.endsWith('/*') ? mimetype.startsWith(type.replace('/*', '/')) : mimetype === type,\n )\n}\n\n/** A multer-shaped uploaded file — what `ctx.file` / `ctx.files` expose. */\nexport interface UploadedFileLike {\n fieldname: string\n originalname: string\n encoding: string\n mimetype: string\n size: number\n buffer: Buffer\n}\n\n/** A buffered multipart file part, as the runtime hands it to {@link applyUploadConfig}. */\nexport interface RawUploadPart {\n fieldname: string\n filename: string\n mimetype: string\n buffer: Buffer\n}\n\nconst DEFAULT_MAX_SIZE = 5 * 1024 * 1024\n\n/**\n * Apply a `@FileUpload` config to already-buffered multipart parts and return\n * the multer-shaped `file` / `files` for `ctx`. Enforces the field name, type\n * filter, per-file size limit, and (array mode) max count — throwing\n * `HttpException` on a violation, mirroring the Express/multer error path.\n * Used by the Fastify, h3 and web runtimes; Express keeps using multer directly.\n */\nexport function applyUploadConfig(\n parts: RawUploadPart[],\n config: FileUploadConfig,\n): { file?: UploadedFileLike; files?: UploadedFileLike[] } {\n if (config.mode === 'none') return {}\n\n const field = config.fieldName ?? 'file'\n const maxSize = config.maxSize ?? DEFAULT_MAX_SIZE\n const typeAllowed = buildFileTypeFilter(config.allowedTypes, config.customMimeMap)\n\n const matched = parts.filter((p) => p.fieldname === field)\n const files: UploadedFileLike[] = matched.map((p) => {\n if (!typeAllowed(p.mimetype, p.filename)) {\n throw new HttpException(\n HttpStatus.UNSUPPORTED_MEDIA_TYPE,\n `File type ${p.mimetype} is not allowed`,\n )\n }\n if (p.buffer.length > maxSize) {\n throw new HttpException(\n HttpStatus.PAYLOAD_TOO_LARGE,\n `File ${p.filename} exceeds the ${maxSize}-byte limit`,\n )\n }\n return {\n fieldname: p.fieldname,\n originalname: p.filename,\n encoding: '7bit',\n mimetype: p.mimetype,\n size: p.buffer.length,\n buffer: p.buffer,\n }\n })\n\n if (config.mode === 'single') return { file: files[0] }\n const maxCount = config.maxCount ?? 10\n return { files: files.slice(0, maxCount) }\n}\n"],"mappings":";;;;;;;;;;wDAUA,MAAa,SAAmC,CAE9C,IAAK,aACL,KAAM,aACN,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,gBACL,IAAK,YACL,IAAK,eACL,KAAM,aACN,IAAK,aACL,KAAM,aAEN,IAAK,kBACL,IAAK,qBACL,KAAM,0EACN,IAAK,2BACL,KAAM,oEACN,IAAK,gCACL,KAAM,4EACN,IAAK,0CACL,IAAK,iDACL,IAAK,kDACL,IAAK,kBACL,IAAK,aACL,IAAK,WAEL,IAAK,kBACL,GAAI,mBACJ,IAAK,oBACL,IAAK,sBACL,KAAM,8BAEN,IAAK,aACL,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,YAEL,IAAK,YACL,KAAM,aACN,IAAK,kBACL,IAAK,kBACL,IAAK,mBAEL,KAAM,mBACN,IAAK,kBACL,KAAM,WACR,EAYA,SAAgB,iBAAiB,MAA2B,CAC1D,OAAO,MAAM,IAAK,GAAM,CACtB,IAAM,MAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,EAC/C,OAAO,SAAS,QAAU,CAC5B,CAAC,CACH,CASA,SAAgB,oBACd,aACA,cACiD,CACjD,GAAI,CAAC,aAAc,UAAa,GAChC,GAAI,OAAO,cAAiB,WAAY,OAAO,aAC/C,IAAM,QAAU,cAAgB,CAAE,GAAG,SAAU,GAAG,aAAc,EAAI,SAC9D,SAAW,aAAa,IAAK,GAAM,QAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,IAAM,CAAC,EACzF,MAAQ,WACN,SAAS,KAAM,MACb,KAAK,SAAS,IAAI,EAAI,SAAS,WAAW,KAAK,QAAQ,KAAM,GAAG,CAAC,EAAI,WAAa,IACpF,CACJ,CA6BA,SAAgB,kBACd,MACA,OACyD,CACzD,GAAI,OAAO,OAAS,OAAQ,MAAO,CAAC,EAEpC,IAAM,MAAQ,OAAO,WAAa,OAC5B,QAAU,OAAO,SAAW,QAC5B,YAAc,oBAAoB,OAAO,aAAc,OAAO,aAAa,EAG3E,MADU,MAAM,OAAQ,GAAM,EAAE,YAAc,KACZ,CAAC,CAAC,IAAK,GAAM,CACnD,GAAI,CAAC,YAAY,EAAE,SAAU,EAAE,QAAQ,EACrC,MAAM,IAAI,cACR,WAAW,uBACX,aAAa,EAAE,SAAS,gBAC1B,EAEF,GAAI,EAAE,OAAO,OAAS,QACpB,MAAM,IAAI,cACR,WAAW,kBACX,QAAQ,EAAE,SAAS,eAAe,QAAQ,YAC5C,EAEF,MAAO,CACL,UAAW,EAAE,UACb,aAAc,EAAE,SAChB,SAAU,OACV,SAAU,EAAE,SACZ,KAAM,EAAE,OAAO,OACf,OAAQ,EAAE,MACZ,CACF,CAAC,EAED,GAAI,OAAO,OAAS,SAAU,MAAO,CAAE,KAAM,MAAM,EAAG,EACtD,IAAM,SAAW,OAAO,UAAY,GACpC,MAAO,CAAE,MAAO,MAAM,MAAM,EAAG,QAAQ,CAAE,CAC3C"}
|
package/dist/web.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import { x as MiddlewareHandler } from "./decorators-BxNSz9Qt.mjs";
|
|
3
|
-
import { S as RuntimeResponse, g as RouteEntry, k as SseHandler } from "./adapter-
|
|
3
|
+
import { S as RuntimeResponse, g as RouteEntry, k as SseHandler } from "./adapter-ClEh-Oos.mjs";
|
|
4
4
|
import { m as ContributorRegistrations, r as AppModuleEntry } from "./app-module-DCA65GZZ.mjs";
|
|
5
5
|
import { f as runCronJobs, o as CronRun } from "./cron-BTGytNg-.mjs";
|
|
6
|
-
import { C as JobContextCarrier, E as JobLike, M as registerJobContext, N as runJob, P as stampJobContext, a as KvSessionStoreOptions, c as InferHandlerResponse, g as PlatformContext, h as reply, i as KvSessionStore, n as KvRateLimitStore, o as RateLimitGuardOptions, p as isReply, r as KvRateLimitStoreOptions, s as rateLimitGuard, t as KvLike, u as Reply, v as waitUntil, y as JOB_CONTEXT_FIELD } from "./kv-stores-
|
|
6
|
+
import { C as JobContextCarrier, E as JobLike, M as registerJobContext, N as runJob, P as stampJobContext, a as KvSessionStoreOptions, c as InferHandlerResponse, g as PlatformContext, h as reply, i as KvSessionStore, n as KvRateLimitStore, o as RateLimitGuardOptions, p as isReply, r as KvRateLimitStoreOptions, s as rateLimitGuard, t as KvLike, u as Reply, v as waitUntil, y as JOB_CONTEXT_FIELD } from "./kv-stores-Co5k2PYA.mjs";
|
|
7
7
|
//#region src/http/web/driver.d.ts
|
|
8
8
|
/**
|
|
9
9
|
* Presents a web `Request` as the express-shaped `req` object that
|
package/dist/web.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v10.
|
|
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{t as Container}from"./container-
|
|
11
|
+
import{t as Container}from"./container-CS9v9UFU.mjs";import{t as MutableModuleRegistry}from"./module-registry-Daf_P2oD.mjs";import{runCronJobs}from"./core/cron.mjs";import{d as stampJobContext,l as registerJobContext,t as JOB_CONTEXT_FIELD,u as runJob}from"./jobs-BQ4t3TAb.mjs";import{buildMountPath}from"./core/path.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-JvMTp50k.mjs";import{a as reply,r as isReply}from"./reply-BDK9xt7Z.mjs";import{a as forwardBackgroundWork,c as waitUntil,d as requestStore}from"./context-CtL9RxXp.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-DsfVy2fk.mjs";import{i as WebResponseDriver,r as WebRequestShim,t as compileWebRoute}from"./handler-zKfIgNo4.mjs";function createWebApp(options){let h3mod=options.h3;if(typeof h3mod?.H3!=`function`)throw Error(`@forinda/kickjs/web: pass the h3 v2 module — \`createWebApp({ h3: await import("h3"), ... })\`. The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.`);let container=Container.getInstance();if(options.env){let env=options.env;Container._envResolver=key=>env[key]}Container._requestStoreProvider=()=>requestStore.getStore()??null;let registry=new MutableModuleRegistry;for(let m of options.modules)registry.mount(m);let modules=registry.entries.map(entry=>{let mod=typeof entry==`function`?new entry:entry;return mod.register?.(container),mod});container.bootstrap();let app=new h3mod.H3,apiPrefix=options.apiPrefix??`/api`,defaultVersion=options.defaultVersion??1,globalMiddleware=options.middlewares??[],globalSources=(options.contributors??[]).map(registration=>({source:`global`,registration,label:`createWebApp`})),seenRoutes=/* @__PURE__ */ new Map;for(let mod of modules){let declaredName=mod.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:mod.constructor?.name??`module`,moduleSources=(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel}));_setExternalContributorSources([...moduleSources,...globalSources]);try{let result=mod.routes();if(!result)continue;let routeSets=Array.isArray(result)?result:[result];for(let route of routeSets){if(!route.controller)throw Error(`@forinda/kickjs/web: module route at '${route.path}' has no controller. Express-style \`router\` mounts are not supported on the web entry.`);let version=route.version??defaultVersion,mountPath=buildMountPath(apiPrefix,version,route.path);for(let entry of buildRouteTable(route.controller,{mountFlags:route.flags})){let url=joinPath(mountPath,entry.path),owner=`${route.controller.name??`controller`}.${String(entry.meta.handlerName??`?`)}`;assertRouteUnique(seenRoutes,entry.method,url,owner),globalMiddleware.length>0&&(entry.middlewares=[...globalMiddleware,...entry.middlewares]);let run=compileWebRoute(entry);app.on(entry.method,url,event=>run({request:event.req,url:event.url,params:event.context.params??{}}))}}}finally{_setExternalContributorSources([])}}return{fetch:async(request,platform)=>{let response=await app.fetch(request);return forwardBackgroundWork(platform),response},h3:app}}function createFetchHandler(build){let app;return{fetch:(request,env={},ctx)=>(app??=createWebApp(build(env)),app.fetch(request,ctx)),scheduled:async(event,env={},ctx)=>{app??=createWebApp(build(env));let run=runCronJobs(Container.getInstance(),{expression:event.cron},`workers`).then(()=>{});ctx?.waitUntil?.(run),await run},queue:async(batch,env={})=>{app??=createWebApp(build(env));let container=Container.getInstance();await Promise.all(batch.messages.map(async message=>{let body=message.body,named=typeof body==`object`&&!!body&&typeof body.name==`string`;try{await runJob(container,batch.queue,{name:named?body.name:``,data:named?body.data:message.body,id:message.id}),message.ack()}catch{message.retry()}}))}}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{JOB_CONTEXT_FIELD,KvRateLimitStore,KvSessionStore,WebRequestShim,WebResponseDriver,compileWebRoute,createFetchHandler,createWebApp,isReply,rateLimitGuard,registerJobContext,reply,runCronJobs,runJob,stampJobContext,waitUntil};
|
|
12
12
|
//# sourceMappingURL=web.mjs.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"application-1Hln746x.d.mts","names":["_$__0.AdapterFactory","Request","Response","Request","Response","_$_____0.AdapterFactory","_$_____0.AdapterFactory","_$__0.InjectionToken","_$__reply0.Reply","_$__0.ModuleFactory"],"sources":["../src/core/disposables.ts","../src/core/kick-error.ts","../src/core/circuit-breaker.ts","../src/core/context-errors.ts","../src/core/mount-sort.ts","../src/core/asset-keys.ts","../src/core/route-policy.ts","../src/http/cron-adapter.ts","../src/http/tracing.ts","../src/http/bootstrap.ts","../src/http/handler.ts","../src/http/cluster.ts","../src/http/define-http-context-decorator.ts","../src/http/runtimes/express.ts","../src/http/request-store.ts","../src/http/middleware/request-scope.ts","../src/http/middleware/trace-context.ts","../src/http/middleware/views.ts","../src/http/middleware/spa.ts","../src/http/health-module.ts","../src/http/application.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAaY,0BAA0B;;iBAKtB,mBAAmB,IAAI;;iBA6BjB,cAAc;;;;;;;;;;;;;;;;;;;;UC9BnB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,UAAU;;cAGC,kBAAkB;WACb;WACA;WACA;WACA;WACA;WACA,UAAU;EAE1B,YAAY,MAAM;;UAiDH;;;;;;EAMf;;;;;;;iBAQc,gBACd,KAAK,gBAAgB,WACrB,UAAS;;;;;;;;;;;;;;;;;;;;KCvFC;UAEK;;EAEf;;EAEA;;EAEA;;UAGe;EACf;EACA;EACA,OAAO;EACP,cAAc;;;;;cAMH,yBAAyB;WACR;EAA5B,YAA4B;;cAMjB;WAaO;UAZV;UACA;UACA;UACA;UACA;UACA;mBAES;mBACA;mBACA;EAEjB,YACkB,cAChB,SAAS;;;;;EAWL,QAAQ,GAAG,UAAU,QAAQ,KAAK,QAAQ;;EA0BhD,YAAY;;EAMZ,YAAY;;EAWZ;UASQ;UAWA;UAeA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cC1HG,gCAAgC;WAClC;WACA;WACA;WACA;EAET,YAAY,aAAa,mBAAmB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA0CjD,iCAAiC;WACnC;WACA;EAET,YAAY,aAAa;;;;;;;;;;;;cA6Cd,8BAA8B;WAChC;WACA;EAET,YAAY,0BAA0B;;;;;;;;;;;;;;;;;;;cA0B3B,kCAAkC;WACpC;WACA;EAET,YAAY,aAAa;;;;;;;;;;;;;;;;;;;;;UC5IV;;EAEf;;;;;;EAMA;;;KAIU;;;;;cAMC,gCAAgC;WAClC,MAAM;WACN;EAET,YAAY,MAAM,WAAW;;;;;;;;cAclB,6BAA6B;WAC/B,MAAM;WACN;WACA;EAET,YAAY,MAAM,WAAW,iBAAiB;;;;;;;cAiBnC,wBAAwB;WAC1B,MAAM;WACN;EAET,YAAY,MAAM,WAAW;;;;;;;;;;;;;;;;iBAsBf,UAAU,UAAU,eAAe,gBAAgB,KAAK,MAAM,YAAY;;;KC/E9E;UAEK;;EAEf;;EAEA;;UAGe;;;;;;;;EAQf,WAAW;;UAGI;EACf,OAAO;;;;;;;;EAQP;;;;;;;;;;;;iBAac,eACd,mBACA,0BACA,OAAM,wBACL;;iBA2Da,SAAS;;;cCpFZ;;mBAEM;EAEjB,IAAI,gBAAgB,cAAc,OAAO;;;;;;EAazC,OAAO,gBAAgB,mBAAmB;;EAU1C;MAII;;;;;;;;iBAyBU,gBAAgB,kBAC9B,SAAS,GACT,UAAU,OAAO,4BAChB;;iBAMa,iBAAiB,kBAAkB,OAAO;;;;cCtE7C;UAEI;;EAEf;;cAsBW,iBAAeA,eAAA;;;cCzCf;cACA;cACA;;UAGI;EACf;;EAEA;EACA;EACA;EACA;EACA,KAAK;;;;;;;;;;;;;;;;;;;;;;;;iBCEe,UAAU,SAAS,qBAAqB,QAAQ;;;;UC1BrD;;;;;;;;;EASf,MAAM,SAAS,SAAS,WAAW,kBAAkB,QAAQ;;EAE7D,KAAK,KAAK,iBAAiB,KAAK,gBAAgB,WAAW,kBAAkB;;EAE7E,SAAS,QAAQ;;EAEjB,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoCK,cAAc,SAAS,qBAAqB;;;UCvD3C;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIc;;;;;;;;UCuDC,qCAAqC,UAAU;GAC7D,UAAU,gBAAgB,UAAU,eAAe,YAAY,uBAC9D,MAAM,qBAAqB,GAAG,GAAG,GAAG,kBACnC,iBAAiB,GAAG,GAAG,GAAG;;;;;;;UAQd;GAEb,UAAU,gBACV,UAAU,eAAe,YAAY,uBACrC,UAAU,0BAA0B,uBAEpC,MAAM,qBAAqB,GAAG,GAAG,GAAG,kBACnC,iBAAiB,GAAG,GAAG,GAAG;;;;;;;;;;;;;;;;EAiB7B,WAAW,UAAU,4BAA4B,qCAAqC;;cAW3E,4BAA4B;;;;;;;;;;;;iBClFzB,kBAAkB,SAAS,eAAe;;;;;;;;;iBAkF1C,YAAY,sBAAsB,UAAS,qBAA0B;;;;;iBAQrE,kBAAkB,YAAY;;;;UC/H7B;;EAEf;;;;;;EAMA,WAAW;;;;;;;;;;;;;;;;;;;;;EAqBX,QAAQ;;EAER;;;cAIW,cAAY,kBAAA;;;;;iBAMT,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoCnB,gBAAgB,UAAU,gBAAgB,KAAK,IAAI,UAAU;;;;;;;;;;;iBCe7D,2BAAsB,KACnBC,WAAO,KAAOC,YAAQ,MAAQ;;;;;;;cCxFpC;UAKI;;;;;EAKf;;;;;UAMe;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;iBAqBc,iBAAiB,iBAAiB;;;;;;;;;;;;;;;;;;;;;;iBA0ClC,aAAa,UAAS,uBAAwB,KAC/CC,WAAO,KAAOC,YAAQ,MAAQ;;;UC5F5B;;;;;;;;;;;;;;;EAef;;EAGA;;EAGA;;EAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAgCW,aAAWC,eAAA;;;UCtDP;;;;;;;;;;;;;;EAcf;;;;;;;;;;;;;;;EAgBA;;;;;;;;;;EAWA;;;;;EAMA;;;;;EAMA;;;;;;;;;;;;;;EAeA;;;;;;;;;;;;;;;;;;;;;iBAoCc,iBACd,mBAKA,cACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAoJW,YAAUC,eAAA;;;;UCvON;EACf;EACA;;;;;;;;;UAUe;;EAEf;;EAEA,UAAU,QAAQ;;cAGP,cAAYC,eAAA;;UAGR;;;;;;;;;;;;;;;;;EAiBf,QAAQ;;cAIG;mBACkB;EAA7B,YAA6B,OAAO;;EAIpC,QAJoCC;;;OAIhCA;;;;;EAQE,SAAK;;;;;;;;;;;;;;;;cAwBA,cAAYC,cAAA;;;;;;;KC9Bb,kBAAkB;EAAmB;EAAc,SAAS;;;UAGvD;;;;;;;;EAQf;;;UAyBe;;EAEf,MAAM,IAAI,uBAAuB,IAAI;;EAErC;;UAGe;;;;;;;;;;;EAWf,SAAS;;;;;;;;;;;;;;;;;;EAmBT,OAAO,UAAU;;EAEjB,WAAW;;EAEX;;;;;;;;;;;;;;;;;;;;;;;;;EAyBA,SAAS;;EAET;;;;;;;;;EASA;;;;;;;;;;;;;;;;;;;;;;EAuBA,cAAc;;EAGd,UAAU;;;;;;;;;;;;;;;;;;;;;EAsBV,eAAe;;;;;;;;;;;;;;;;;;;EAoBf;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8BA,mBAAmB;EAEnB,UAAU;;;;;;;;;;;;EAaV;;EAGA,2CAA2C,YAAY;;EAEvD;;;;;;;;EAQA;;;;;EAKA;;;;;EAKA;;;;;;;;;;;;;;;;;;;EAoBA;IAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;EA0BtB,cAAc,UAAU,KAAK,iBAAiB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0CrD,WAAW,UAAU,UAAU,KAAK,iBAAiB,OAAO;;;;;;;;;;;;;;;;;;EAmB5D;;IAEE;;;cAwHS;mBAsDkB;UArDrB;;mBAES;UACT;UACA;;UAEA;;UAEA;;;;;;mBAMS;;UAET;mBACS;;;;;;mBAMA;mBAEA;;;;;;;;;;;;;UAcT;;UAMA;;UAEA;;UAEA;;UAEA;EAER,YAA6B,SAAS;;MAgDlC;;MAKA;;EAKJ,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+BhB,MAAM,SAAS,UAAU,QAAQ;EASjC,OAAO,KAAK,KAAK,iBAAiB,KAAK,KAAK,gBAAgB,QAAQ;;;;;;;;;;;;;;;;UAwB5D;;;;;;UAkBA;;;;;;;;;;UAkCM;EAeR,SAAS;;EA4aT,gBAAgB;;;;;;;;;EAYhB,sBAAsB;;;;;;;;;;EA6BtB,SAAS;;UA8FP;;EAkBF,WAAW;;;;;;;;;;;EAiDX,SAAS,UAAS,kBAAuB;;;;;;;EAgL/C,iBAAiB;;;;;;;;EAWjB;IAAsB;IAAc,cAAc;;;EAKlD,iBAAiB;;EAKjB,eAAe;;;;;;EASf,uBAAuB;;;;;;;;;;;;;;;;;;;EAsBvB,mBAAmB;IACjB;IACA;IACA;IACA;;EA6CF,iBAAiB;;;;;;;;;;;;;;UAmBT;UAWA;UAmBA;UAUA;;;;;;;;UAeA;;UAoBA"}
|