@forinda/kickjs 8.3.1 → 8.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +1 -1
  2. package/dist/{application-DT1nNdC7.mjs → application-B2SLsOAo.mjs} +3 -3
  3. package/dist/{application-DT1nNdC7.mjs.map → application-B2SLsOAo.mjs.map} +1 -1
  4. package/dist/{bootstrap-C6-CrVgx.mjs → bootstrap-CBoWnjli.mjs} +3 -3
  5. package/dist/{bootstrap-C6-CrVgx.mjs.map → bootstrap-CBoWnjli.mjs.map} +1 -1
  6. package/dist/{client-ip-r74dYQsg.mjs → client-ip-BAqx--eV.mjs} +2 -2
  7. package/dist/{client-ip-r74dYQsg.mjs.map → client-ip-BAqx--eV.mjs.map} +1 -1
  8. package/dist/config/index.mjs +2 -2
  9. package/dist/{config-Bbqd77CD.mjs → config-xYtlQp17.mjs} +3 -3
  10. package/dist/{config-Bbqd77CD.mjs.map → config-xYtlQp17.mjs.map} +1 -1
  11. package/dist/{container-CzBE79TS.mjs → container-DYDEiKNR.mjs} +4 -4
  12. package/dist/{container-CzBE79TS.mjs.map → container-DYDEiKNR.mjs.map} +1 -1
  13. package/dist/{context-CCo_tD5m.mjs → context-DVzzDJqS.mjs} +3 -3
  14. package/dist/{context-CCo_tD5m.mjs.map → context-DVzzDJqS.mjs.map} +1 -1
  15. package/dist/{context-errors-CH2YMGwk.mjs → context-errors-DBe_u92r.mjs} +2 -2
  16. package/dist/{context-errors-CH2YMGwk.mjs.map → context-errors-DBe_u92r.mjs.map} +1 -1
  17. package/dist/{contributor-runner-Cf6M_ILY.mjs → contributor-runner-DiDYluYn.mjs} +3 -3
  18. package/dist/{contributor-runner-Cf6M_ILY.mjs.map → contributor-runner-DiDYluYn.mjs.map} +1 -1
  19. package/dist/core/adapter.mjs +1 -1
  20. package/dist/core/app-module.mjs +1 -1
  21. package/dist/core/cache.mjs +1 -1
  22. package/dist/core/container.mjs +2 -2
  23. package/dist/core/cron.mjs +2 -2
  24. package/dist/core/decorators.mjs +2 -2
  25. package/dist/core/errors.mjs +1 -1
  26. package/dist/core/interfaces.mjs +1 -1
  27. package/dist/core/logger.mjs +1 -1
  28. package/dist/core/path.mjs +1 -1
  29. package/dist/core/plugin.mjs +1 -1
  30. package/dist/core/reactivity.mjs +2 -2
  31. package/dist/core/reactivity.mjs.map +1 -1
  32. package/dist/{core-vktCOX0c.mjs → core-C80EsmuT.mjs} +3 -3
  33. package/dist/{core-vktCOX0c.mjs.map → core-C80EsmuT.mjs.map} +1 -1
  34. package/dist/{csrf-DiOCjUPJ.mjs → csrf-Ci-NmNHy.mjs} +3 -3
  35. package/dist/{csrf-DiOCjUPJ.mjs.map → csrf-Ci-NmNHy.mjs.map} +1 -1
  36. package/dist/{decorate-Bn8r2PEB.mjs → decorate-Ba_J1qTc.mjs} +1 -1
  37. package/dist/{define-adapter-BBA4UbXZ.mjs → define-adapter-CBXnH8kA.mjs} +2 -2
  38. package/dist/{define-adapter-BBA4UbXZ.mjs.map → define-adapter-CBXnH8kA.mjs.map} +1 -1
  39. package/dist/{disposables-DsrxA4Xv.mjs → disposables-DnXJYY9g.mjs} +2 -2
  40. package/dist/{disposables-DsrxA4Xv.mjs.map → disposables-DnXJYY9g.mjs.map} +1 -1
  41. package/dist/{env-DmEVaWGi.mjs → env-Dzp4S93f.mjs} +3 -3
  42. package/dist/{env-DmEVaWGi.mjs.map → env-Dzp4S93f.mjs.map} +1 -1
  43. package/dist/{handler-HmFLwQ8M.mjs → handler-Cm5zNANP.mjs} +4 -4
  44. package/dist/{handler-HmFLwQ8M.mjs.map → handler-Cm5zNANP.mjs.map} +1 -1
  45. package/dist/http/application.mjs +2 -2
  46. package/dist/http/bootstrap.mjs +2 -2
  47. package/dist/http/context.mjs +2 -2
  48. package/dist/http/middleware/cors.mjs +1 -1
  49. package/dist/http/middleware/csrf.mjs +2 -2
  50. package/dist/http/middleware/error-handler.mjs +2 -2
  51. package/dist/http/middleware/helmet.mjs +1 -1
  52. package/dist/http/middleware/rate-limit.mjs +2 -2
  53. package/dist/http/middleware/request-id.mjs +1 -1
  54. package/dist/http/middleware/request-logger.mjs +2 -2
  55. package/dist/http/middleware/session.mjs +2 -2
  56. package/dist/http/middleware/session.mjs.map +1 -1
  57. package/dist/http/middleware/spa.mjs +2 -2
  58. package/dist/http/middleware/upload.mjs +3 -3
  59. package/dist/http/middleware/upload.mjs.map +1 -1
  60. package/dist/http/middleware/validate.mjs +1 -1
  61. package/dist/http/middleware/views.mjs +2 -2
  62. package/dist/http/query/index.mjs +2 -2
  63. package/dist/http/router-builder.mjs +2 -2
  64. package/dist/http/runtimes/fastify.d.mts +2 -3
  65. package/dist/http/runtimes/fastify.d.mts.map +1 -1
  66. package/dist/http/runtimes/fastify.mjs +3 -3
  67. package/dist/http/runtimes/fastify.mjs.map +1 -1
  68. package/dist/http/runtimes/h3-web.d.mts +3 -4
  69. package/dist/http/runtimes/h3-web.d.mts.map +1 -1
  70. package/dist/http/runtimes/h3-web.mjs +2 -2
  71. package/dist/http/runtimes/h3-web.mjs.map +1 -1
  72. package/dist/http/runtimes/h3.d.mts +2 -3
  73. package/dist/http/runtimes/h3.d.mts.map +1 -1
  74. package/dist/http/runtimes/h3.mjs +2 -2
  75. package/dist/http/runtimes/h3.mjs.map +1 -1
  76. package/dist/index.mjs +2 -2
  77. package/dist/{kv-stores-BQ5jox2K.mjs → kv-stores-CL1FlCuc.mjs} +3 -3
  78. package/dist/{kv-stores-BQ5jox2K.mjs.map → kv-stores-CL1FlCuc.mjs.map} +1 -1
  79. package/dist/{metadata-CugcKCXW.mjs → metadata-BfAea0Eh.mjs} +2 -2
  80. package/dist/{metadata-CugcKCXW.mjs.map → metadata-BfAea0Eh.mjs.map} +1 -1
  81. package/dist/{module-registry-D6lAL81K.mjs → module-registry-BXUTVQZX.mjs} +2 -2
  82. package/dist/{module-registry-D6lAL81K.mjs.map → module-registry-BXUTVQZX.mjs.map} +1 -1
  83. package/dist/query-6wpoIQxU.mjs +12 -0
  84. package/dist/{query-Dc58mMd1.mjs.map → query-6wpoIQxU.mjs.map} +1 -1
  85. package/dist/{respond-DM0pbGtn.mjs → respond-C1bmis_l.mjs} +2 -2
  86. package/dist/{respond-DM0pbGtn.mjs.map → respond-C1bmis_l.mjs.map} +1 -1
  87. package/dist/{route-flag-BKYcu3L0.mjs → route-flag-gN8YUbh7.mjs} +3 -3
  88. package/dist/{route-flag-BKYcu3L0.mjs.map → route-flag-gN8YUbh7.mjs.map} +1 -1
  89. package/dist/{route-policy-YrOBnVpp.mjs → route-policy-BmkX461L.mjs} +2 -2
  90. package/dist/{route-policy-YrOBnVpp.mjs.map → route-policy-BmkX461L.mjs.map} +1 -1
  91. package/dist/router-builder-mGBlnvon.mjs +12 -0
  92. package/dist/{router-builder-B5Vbzba3.mjs.map → router-builder-mGBlnvon.mjs.map} +1 -1
  93. package/dist/{runtime-DhY6i4TY.mjs → runtime-Dr3WVroF.mjs} +3 -3
  94. package/dist/{runtime-DhY6i4TY.mjs.map → runtime-Dr3WVroF.mjs.map} +1 -1
  95. package/dist/{upload-config-BYINSxAp.mjs → upload-config-oWkCq0nK.mjs} +2 -2
  96. package/dist/{upload-config-BYINSxAp.mjs.map → upload-config-oWkCq0nK.mjs.map} +1 -1
  97. package/dist/web.d.mts +8 -8
  98. package/dist/web.d.mts.map +1 -1
  99. package/dist/web.mjs +2 -2
  100. package/dist/web.mjs.map +1 -1
  101. package/package.json +4 -4
  102. package/dist/query-Dc58mMd1.mjs +0 -12
  103. package/dist/router-builder-B5Vbzba3.mjs +0 -12
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-DhY6i4TY.mjs","names":[],"sources":["../src/http/body-policy.ts","../src/http/middleware/request-scope.ts","../src/http/reply.ts","../src/http/runtime.ts"],"sourcesContent":["/**\n * How a request body is interpreted, decided in ONE place for every runtime.\n *\n * Before this, each engine brought its own library's opinion: Express parsed\n * only exact `application/json` and silently left everything else `undefined`,\n * Fastify answered 415 for anything it had no parser for, and h3 routed\n * unrecognised types through `destr` — so the same request produced a parsed\n * object, an empty body, or a 415 depending on which engine the app was\n * configured with. The runtime is meant to be swappable; that made it not\n * swappable for any app accepting a body outside `application/json` (#590).\n *\n * @module @forinda/kickjs/http/body-policy\n */\n\nimport { HttpException } from '../core/errors'\n\n/**\n * A header as the runtimes hand it over. Node's `IncomingHttpHeaders` and\n * Fastify's normalised map both allow `string[]`, so take the union here\n * rather than casting at every call site.\n */\nexport type HeaderValue = string | string[] | undefined\n\n/** What the framework does with a body of a given media type. */\nexport type BodyKind =\n /** Parse as JSON, strictly. */\n | 'json'\n /** Parse as `application/x-www-form-urlencoded` into an object. */\n | 'urlencoded'\n /** Hand the handler the raw string. Never JSON — see `classifyMediaType`. */\n | 'text'\n /** Left to the upload path, which consumes the stream itself. */\n | 'multipart'\n /** Not parsed. `ctx.body` stays undefined. */\n | 'unsupported'\n\n/**\n * Strip parameters and normalise: `Application/JSON; charset=utf-8` →\n * `application/json`. h3's own check is `===` against the bare type, which is\n * why `application/json; charset=utf-8` — what most clients actually send —\n * missed it and fell to a non-strict branch.\n */\nexport function normalizeMediaType(contentType: HeaderValue): string {\n const value = Array.isArray(contentType) ? contentType[0] : contentType\n return (value ?? '').split(';')[0].trim().toLowerCase()\n}\n\n/**\n * `application/*+json` is JSON, and this is not a judgement call: RFC 6838\n * §4.2.8 says a media type \"MUST NOT be given names incorporating suffixes for\n * structured syntaxes they do not actually employ\", and RFC 6839 §2 exists so\n * receivers can \"do generic processing of the underlying representation\".\n * Spring, ASP.NET Core and Hono's validator all match the suffix by default.\n *\n * So `application/merge-patch+json` (RFC 7396) and `application/problem+json`\n * (RFC 9457) parse as JSON — which the framework already emits for every\n * problem response, and previously could not read back.\n */\nfunction isJsonMediaType(mediaType: string): boolean {\n return mediaType === 'application/json' || /^application\\/[\\w.+-]+\\+json$/.test(mediaType)\n}\n\n/**\n * Decide how to treat a body, from its declared media type alone.\n *\n * `text/*` deliberately yields the raw string and is NEVER JSON-parsed.\n * `text/plain` is one of three CORS-safelisted content types, so it crosses\n * origins without a preflight; JSON-parsing it would re-open the simple-request\n * CSRF that requiring `application/json` closes. h3's own source carries this\n * warning, and h3 v2 pulled back from `destr` partly because of it.\n */\nexport function classifyMediaType(contentType: HeaderValue): BodyKind {\n const mediaType = normalizeMediaType(contentType)\n if (!mediaType) return 'unsupported'\n if (isJsonMediaType(mediaType)) return 'json'\n if (mediaType === 'application/x-www-form-urlencoded') return 'urlencoded'\n if (mediaType.startsWith('multipart/')) return 'multipart'\n if (mediaType.startsWith('text/')) return 'text'\n return 'unsupported'\n}\n\n/**\n * Media types Express should hand to its JSON body parser. `express.json()`\n * defaults to the exact string `application/json`, and `type-is` will not match\n * `application/json` against a bare wildcard-plus-json pattern (a length guard rejects\n * it), so BOTH entries are required — passing only the wildcard silently stops\n * parsing ordinary JSON.\n */\nexport const EXPRESS_JSON_TYPES = ['application/json', 'application/*+json'] as const\n\n/** Media types the framework claims to parse, for diagnostics and docs. */\nexport const SUPPORTED_BODY_TYPES = [\n 'application/json',\n 'application/*+json',\n 'application/x-www-form-urlencoded',\n 'text/*',\n 'multipart/form-data',\n] as const\n\n/**\n * The 415 for a body whose media type the framework does not parse.\n *\n * Only ever raised when a body was actually SENT. An absent or empty body is\n * treated as absent regardless of its declared type — the same call Spring's\n * `readWithMessageConverters` and ASP.NET's `BodyModelBinder` both make, and\n * the reason neither rejects a bodyless POST. A bare `POST` with no payload is\n * a legitimate request, not an unsupported one.\n *\n * Carries `Accept`, which RFC 9110 §15.5.16 says a 415 \"can\" use to indicate\n * which media types would have been accepted.\n */\nexport function unsupportedMediaTypeError(contentType: HeaderValue): HttpException {\n const mediaType = normalizeMediaType(contentType)\n return new HttpException(\n 415,\n mediaType\n ? `Unsupported Media Type: ${mediaType}`\n : 'Unsupported Media Type: request has a body but no Content-Type',\n undefined,\n { Accept: SUPPORTED_BODY_TYPES.join(', ') },\n )\n}\n\n/** Verbs whose body the framework reads. */\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\n/**\n * Did this request actually carry a body? Decided from the framing headers\n * rather than from whether a parser produced anything — the distinction that\n * separates \"nothing was sent\" from \"something was sent that we could not\n * read\", which is the whole basis for answering 415 on one and 200 on the other.\n */\nexport function hasRequestBody(headers: Record<string, unknown>): boolean {\n const length = headers['content-length'] as string | undefined\n const encoding = headers['transfer-encoding'] as string | undefined\n return encoding !== undefined || (length !== undefined && length !== '0')\n}\n\n/**\n * Connect middleware that answers 415 for a body Express's parsers left\n * untouched.\n *\n * Express has no concept of an unsupported type: a parser whose `type` does not\n * match simply calls `next()` and leaves `req.body` undefined, indistinguishable\n * from a request that sent nothing. Mounted after the parsers, this restores the\n * distinction so Express agrees with the other runtimes.\n */\nexport function rejectUnsupportedBody() {\n return (req: any, _res: unknown, next: (err?: unknown) => void): void => {\n if (!BODY_METHODS.has((req.method ?? 'GET').toUpperCase())) return next()\n if (classifyMediaType(req.headers?.['content-type']) !== 'unsupported') return next()\n\n const reject = (): void => next(unsupportedMediaTypeError(req.headers?.['content-type']))\n const length = req.headers?.['content-length'] as string | undefined\n if (length !== undefined) return length === '0' ? next() : reject()\n if (req.headers?.['transfer-encoding'] === undefined) return next()\n\n // Chunked: the framing headers say a body MAY follow, but an empty chunked\n // post sends none. Deciding on the headers alone would 415 a request that\n // sent nothing, so wait to see whether a byte actually arrives — the same\n // distinction the other runtimes make by inspecting the read body.\n let decided = false\n const settle = (fn: () => void) => (): void => {\n if (decided) return\n decided = true\n req.off?.('data', onData)\n req.off?.('end', onEnd)\n fn()\n }\n const onData = settle(reject)\n const onEnd = settle(() => next())\n req.on?.('data', onData)\n req.on?.('end', onEnd)\n }\n}\n","// Web Crypto via globalThis.crypto (randomUUID) — portable across\n// node/bun/deno/workers; node:crypto deliberately NOT imported here so this\n// module stays on the edge-safe request path.\nimport type { Request, Response, NextFunction } from 'express'\nimport { requestStore, type RequestStore } from '../request-store'\nimport { METADATA } from '../../core/interfaces'\nimport { getClassMetaOrUndefined } from '../../core/metadata'\nimport { createLogger } from '../../core/logger'\n\nconst log = createLogger('RequestScope')\n\n// Per-class @PreDestroy method-name cache: the metadata is immutable after\n// class definition, so pay the Reflect read once per class, not once per\n// instance per request. `null` = \"checked, none declared\".\nconst preDestroyCache = new Map<Function, string | symbol | null>()\n\nfunction preDestroyMethod(ctor: Function): string | symbol | null {\n let method = preDestroyCache.get(ctor)\n if (method === undefined) {\n method =\n getClassMetaOrUndefined<string | symbol>(METADATA.PRE_DESTROY, (ctor as any).prototype) ??\n null\n preDestroyCache.set(ctor, method)\n }\n return method\n}\n\n/**\n * Run `@PreDestroy` hooks on every REQUEST-scoped instance the request\n * created, then drop them. Called when the response closes (finished or\n * aborted) — the counterpart to `@PostConstruct`, closing the lifecycle gap\n * where per-request services holding transactions/handles/subscriptions were\n * silently dropped with no cleanup callback.\n *\n * Idempotent (guarded by `store.disposed`); hook errors are logged and\n * swallowed so one failing teardown can't break request completion. Async\n * hooks are fired without being awaited — the response is already gone.\n */\nexport function disposeRequestStore(store: RequestStore): void {\n if (store.disposed) return\n store.disposed = true\n if (store.instances.size === 0) return\n for (const instance of store.instances.values()) {\n if (instance === null || typeof instance !== 'object') continue\n const method = preDestroyMethod(instance.constructor)\n if (!method || typeof instance[method] !== 'function') continue\n try {\n const result = instance[method]()\n if (result && typeof result.then === 'function') {\n ;(result as Promise<void>).catch((err) =>\n log.error(err, `@PreDestroy on ${instance.constructor.name} rejected`),\n )\n }\n } catch (err) {\n log.error(err, `@PreDestroy on ${instance.constructor.name} threw`)\n }\n }\n store.instances.clear()\n}\n\n/**\n * Marker symbol stamped on the function returned by {@link requestScopeMiddleware}.\n *\n * `Application.setup()` uses {@link isRequestScopeMiddleware} to detect whether\n * the user-supplied middleware list already contains the request-scope wrapper,\n * and skips its automatic auto-mount when one is found. Lets adopters control\n * the exact position of the ALS frame in their pipeline (e.g., after a custom\n * tracing wrapper) without ending up with two nested frames.\n */\n// `Symbol.for(...)` returns a plain `symbol` (not `unique symbol`), but the\n// runtime detection marker doesn't need the unique nominal property — it only\n// needs the interned-slot reference equality that `Symbol.for` already provides.\nconst REQUEST_SCOPE_MIDDLEWARE_MARKER = Symbol.for('@kickjs/requestScopeMiddleware')\n\n/**\n * Build a fresh per-request store. Shared by {@link requestScopeMiddleware}\n * (Express) and by runtimes whose handler runs outside the connect-middleware\n * chain (e.g. Fastify), so both establish an identical ALS frame.\n */\nexport function createRequestStore(requestId?: string): RequestStore {\n return {\n requestId: requestId || crypto.randomUUID(),\n instances: new Map(),\n // Explicit generic locks the \"string keys only\" decision.\n values: new Map<string, unknown>(),\n }\n}\n\n/**\n * Wraps each request in an AsyncLocalStorage context.\n * Enables REQUEST-scoped DI and automatic requestId propagation to logs.\n *\n * Should be mounted early in the middleware pipeline (before route handlers).\n * `Application` mounts this automatically unless `contextStore: 'manual'` is set\n * or the user already includes one in their middleware list.\n */\nexport function requestScopeMiddleware() {\n const mw = (req: Request, res: Response, next: NextFunction) => {\n const requestIdHeader = req.headers['x-request-id']\n const requestId =\n (Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader) || crypto.randomUUID()\n const store = createRequestStore(requestId)\n // Surface the requestId on req so the standalone requestId() middleware\n // (when also mounted) reuses this value instead of generating a divergent\n // second one. Without this, log lines that read from the ALS store would\n // disagree with the X-Request-Id response header for any request that\n // didn't carry an inbound x-request-id header.\n ;(req as Request & { requestId?: string }).requestId = requestId\n // 'close' fires once the response finished OR the client aborted — run\n // @PreDestroy teardown for the request's scoped instances either way.\n res.once('close', () => disposeRequestStore(store))\n // Pass `next` directly — `run` invokes it with no args, so the arrow\n // wrapper previously allocated per request added nothing.\n requestStore.run(store, next)\n }\n ;(mw as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] = true\n return mw\n}\n\n/**\n * Returns `true` when `value` is the function produced by {@link requestScopeMiddleware}.\n *\n * Detection uses a `Symbol.for(...)` marker so the check survives different\n * copies of `@forinda/kickjs` loaded under different module identities (e.g.,\n * a published package alongside a workspace-linked one during local dev).\n */\nexport function isRequestScopeMiddleware(value: unknown): boolean {\n if (typeof value !== 'function') return false\n return (value as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] === true\n}\n","// Return-value handler support (response-inference-design.md §3.1).\n//\n// Handlers may RETURN their response payload instead of calling `ctx.json`:\n// the runtime auto-sends it when the pipeline finishes with nothing written.\n// `reply(status, body)` wraps a payload with a non-200 status while staying\n// transparent to type inference (`Reply<201, Task>` carries both statically).\n//\n// Edge-safe: no node imports — shared by all four runtimes including the\n// `@forinda/kickjs/web` entry.\n\nimport type { RequestContext } from './context'\nimport type { RuntimeResponse } from './runtime'\n\n// `Symbol.for` so the brand survives duplicate module copies (workspace-linked\n// + published package in one process — same rationale as the request-scope\n// middleware marker).\nconst REPLY_BRAND = Symbol.for('kickjs.reply')\n\n/**\n * A status-carrying response payload for return-value handlers. Create via\n * {@link reply} — the brand is what runtimes detect, and the generics are\n * what the `kick/routes` response typegen unwraps (`Reply<S, T>` → `T`).\n */\nexport interface Reply<S extends number = number, T = unknown> {\n readonly [REPLY_BRAND]: true\n readonly status: S\n readonly body: T\n}\n\n/**\n * Wrap a return-value handler's payload with an explicit status code:\n *\n * ```ts\n * @Post('/')\n * async create(ctx: RequestContext) {\n * return reply(201, await this.tasks.create(ctx.body))\n * }\n * ```\n */\nexport function reply<S extends number, T>(status: S, body: T): Reply<S, T> {\n return { [REPLY_BRAND]: true, status, body }\n}\n\n/** `reply(200, body)` — explicit form of a bare `return body`. */\nreply.ok = <T>(body: T): Reply<200, T> => reply(200, body)\n/** `reply(201, body)` */\nreply.created = <T>(body: T): Reply<201, T> => reply(201, body)\n/** `reply(202, body)` */\nreply.accepted = <T>(body: T): Reply<202, T> => reply(202, body)\n/** Empty 204 — maps to `ctx.noContent()`. */\nreply.noContent = (): Reply<204, undefined> => reply(204, undefined)\n\n/**\n * The statically-inferred response type of a controller handler — what\n * `kick typegen` emits into `KickRoutes[...].response`:\n *\n * - `Awaited<ReturnType>` of the method\n * - `Reply<S, T>` unwraps to `T`\n * - `void`/`undefined` members are dropped (imperative `ctx.json` branches);\n * a handler that never returns a value stays `unknown`, matching the\n * pre-inference emission\n *\n * ```ts\n * type R = InferHandlerResponse<UsersController['get']> // → User\n * ```\n */\nexport type InferHandlerResponse<H> = H extends (...args: never[]) => infer R\n ? [Exclude<Awaited<R>, void | undefined>] extends [never]\n ? unknown\n : UnwrapReply<Exclude<Awaited<R>, void | undefined>>\n : unknown\n\ntype UnwrapReply<A> =\n A extends Reply<number, infer B>\n ? B\n : A extends RuntimeResponse\n ? // The engine's own response object says nothing about the body, so a\n // handler ending `return ctx.json(...)` degrades to `unknown` — which is\n // what the typegen comment always claimed happened. It did not: the bare\n // `RuntimeResponse` leaked into `KickRoutes[...].response`, offering\n // `.status()` / `.setHeader()` to a client expecting a payload.\n // Confidently wrong is worse than unknown.\n //\n // To carry a payload, return it (`return user`) or wrap it\n // (`return reply(201, user)`) — that is what `reply` is for.\n unknown\n : A\n\nexport function isReply(value: unknown): value is Reply {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[REPLY_BRAND] === true\n )\n}\n\n/**\n * Send a handler's RETURN value when the pipeline finished without anything\n * having written a response. Callers guard on \"driver/response unsettled\" —\n * this only decides how to send:\n *\n * - `undefined` → not handled (returns false; runtime keeps today's behavior)\n * - `Reply` wrapper → status + body (204 → `ctx.noContent()`)\n * - anything else → `ctx.json(value)` (200)\n *\n * Returns true when a response was produced.\n */\nexport function applyHandlerResult(ctx: RequestContext, result: unknown): boolean {\n if (result === undefined) return false\n if (isReply(result)) {\n // Only 204 maps to noContent — an undefined body must NOT coerce the\n // declared status (reply(304, undefined) keeps its 304; the json body\n // is simply empty).\n if (result.status === 204) {\n ctx.noContent()\n return true\n }\n ctx.json(result.body, result.status)\n return true\n }\n ctx.json(result)\n return true\n}\n","// The HttpRuntime seam (spec: docs/http/spec-http-runtimes.md, Avenue B).\n//\n// Decorators no longer emit an `express.Router` directly. `buildRouteTable()`\n// turns controller metadata into a plain-data `RouteEntry[]`; an `HttpRuntime`\n// materializes that table onto whatever engine it owns. Express is the default\n// runtime and the only one shipped in M1 — its materializer reproduces the exact\n// handler chain the old `buildRoutes()` built, so behavior is unchanged.\n//\n// Fastify / h3 runtimes (later milestones) consume the SAME `RouteEntry` data\n// and the SAME `HttpRuntime` contract; the request/response driver abstraction\n// that lets `RequestContext` run engine-agnostically lands with them (M3), since\n// under Express the drivers ARE the Express request/response objects already.\n\nimport { ROUTE_SLOT, type RouteFlags } from '../core/route-flag'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type {\n RequestHandler,\n ErrorRequestHandler,\n Express,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from 'express'\n\nimport type { Constructor, FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core'\nimport type { RequestContext } from './context'\n\n/** HTTP verbs the router emits. Mirrors the decorator surface (@Get/@Post/...). */\nexport type RouteMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n\n/**\n * A context-based handler: the runtime-neutral unit of the request pipeline.\n * Contributors, route middleware (via {@link MiddlewareHandler}), and the\n * terminal controller call all reduce to this shape. The runtime owns how a\n * `CtxHandler` is wrapped onto its engine (Express wraps it in `(req, res,\n * next)`; Fastify in its own handler).\n */\nexport type CtxHandler = (ctx: RequestContext) => unknown | Promise<unknown>\n\n/**\n * Connect-style middleware — the portable middleware format. Built-in\n * middleware and adopter-supplied Express middleware are this shape. The\n * Express runtime hands them straight to `app.use`; later runtimes bridge\n * them (Fastify via `@fastify/middie`, h3 via its node adapter).\n *\n * Typed as the Express handler union for M1 (Express is the only runtime);\n * a future milestone narrows this to bare node `(req, res, next)` primitives\n * once the driver layer lands.\n */\nexport type ConnectMiddleware = RequestHandler | ErrorRequestHandler\n\n/**\n * One materialized route, as plain data. Produced by {@link buildRouteTable}\n * from decorator metadata; consumed by {@link HttpRuntime.mountRoutes}.\n *\n * The shape is deliberately richer than a flat `CtxHandler[]`: `middlewares`\n * keep the `(ctx, next)` signature (they may short-circuit or call `next(err)`),\n * `contributorRunner` is a closure over the pre-built pipeline + container (the\n * pipeline is built — and any cycle / missing-dep error thrown — at table-build\n * time, i.e. boot), and `validation` / `upload` stay as metadata the runtime\n * materializes its own way. Faithfully reproducing the legacy Express chain —\n * not collapsing it — is what keeps M1 behavior-neutral.\n */\nexport interface RouteEntry {\n method: RouteMethod\n /** Path with `:param` segments — the portable subset all engines accept. */\n path: string\n /** Class + method middleware, in execution order; each receives `(ctx, next)`. */\n middlewares: MiddlewareHandler[]\n /**\n * Runs the contributor pipeline against `ctx` (populates `ctx.set(...)`),\n * or `null` when no contributors apply. Closes over the built pipeline and\n * the DI container so it stays engine-neutral; the runtime advances the\n * chain (Express: call `next()`) once it resolves.\n */\n contributorRunner: CtxHandler | null\n /** Terminal handler: resolves the controller per-request and invokes it. */\n handler: CtxHandler\n /** Introspection + runtime-materialized concerns (swagger/typegen read this). */\n meta: RouteMeta\n}\n\n/** Per-route metadata: introspection refs plus runtime-materialized concerns. */\nexport interface RouteMeta {\n /**\n * Owning controller class — for DI resolution and adapter introspection.\n * Absent for ad-hoc routes registered through {@link AdapterHttp.route},\n * which have no controller class behind them.\n */\n controller?: Constructor\n /** Handler method name on the controller, when there is one. */\n handlerName?: string\n /** Zod/JSON-schema validation config from the route decorator, if any. */\n validation?: RouteDefinition['validation']\n /** `@FileUpload` config, if present — the runtime supplies the backend. */\n upload?: FileUploadConfig\n /**\n * Route flags in force here, resolved at build time (method over class).\n * Enabled flags only: a flag declared `false` at a higher-precedence site is\n * absent rather than present-and-false, so `has()` is always the right\n * question. See `core/route-flag.ts`.\n */\n flags?: RouteFlags\n}\n\n/**\n * The route a request matched, published on `ctx.route`.\n *\n * Everything that holds a {@link RequestContext} — handlers, `@Middleware()`,\n * guards, contributors — can read it, which is what lets one declaration on a\n * route serve every consumer.\n */\n/**\n * Publish the matched route on the request, for `ctx.route` to read.\n *\n * Called by each runtime once per dispatch, before it builds a RequestContext.\n * It lives on `req` rather than on the ctx because the Express runtime\n * constructs a fresh RequestContext per middleware step — anything written to\n * one ctx would be invisible to the next and to the handler.\n */\nexport function publishMatchedRoute(req: unknown, entry: RouteEntry): void {\n ;(req as Record<symbol, MatchedRoute>)[ROUTE_SLOT] = {\n method: entry.method,\n path: entry.path,\n controller: entry.meta.controller,\n handlerName: entry.meta.handlerName,\n flags: entry.meta.flags ?? EMPTY_FLAGS,\n }\n}\n\nconst EMPTY_FLAGS: RouteFlags = new Map() as RouteFlags\n\nexport interface MatchedRoute {\n method: RouteMethod\n /** Path as declared on the handler, e.g. `/:id`. */\n path: string\n controller?: Constructor\n handlerName?: string\n /** Enabled flags only — see {@link RouteMeta.flags}. */\n flags: RouteFlags\n}\n\n/** A controller's routes grouped under the module mount prefix. */\nexport type RouteTable = { mountPath: string; routes: RouteEntry[] }[]\n\n/**\n * The response surface {@link RequestContext}'s helpers (`json` / `html` /\n * `sse` / `download` / `render` / `problem`) drive, instead of calling Express\n * `res` methods directly (spec §4.3). Sized so `express.Response` satisfies it\n * structurally — under the Express runtime the driver IS the Express response,\n * so there is no wrapping and no behavior change. Other runtimes (Fastify / h3)\n * provide a thin object implementing this over their native reply.\n *\n * The terminal methods return `RuntimeResponse` for fluent chaining (Express's\n * `Response` is assignable, since it is a superset).\n */\nexport interface RuntimeResponse {\n status(code: number): RuntimeResponse\n json(data: unknown): RuntimeResponse\n send(data: unknown): RuntimeResponse\n type(contentType: string): RuntimeResponse\n setHeader(name: string, value: string | number | readonly string[]): unknown\n render(view: string, data?: Record<string, unknown>): unknown\n writeHead(statusCode: number, headers?: Record<string, string>): unknown\n flushHeaders(): unknown\n write(chunk: string | Buffer): boolean\n end(data?: unknown): unknown\n once(event: 'close', listener: () => void): unknown\n readonly headersSent: boolean\n}\n\n// ── Runtime-typed escape hatches (spec §4.3b) ─────────────────────────────\n\n/**\n * Per-runtime type map: the engine-native request / response / app types an\n * `HttpRuntime` exposes through `ctx.req.raw` / `ctx.res.raw` / `AdapterContext.app`.\n */\nexport interface RuntimeTypeMap {\n request: unknown\n response: unknown\n app: unknown\n}\n\n/** The Express runtime's native types — the default when nothing is augmented. */\nexport interface ExpressRuntimeTypes {\n request: ExpressRequest\n response: ExpressResponse\n app: Express\n}\n\n/**\n * Augmentable runtime registry. Empty by default; the `kick/runtime` typegen\n * plugin emits a `{ runtime: <RuntimeTypeMap> }` augmentation from the configured\n * runtime, which flips the engine-native escape-hatch types — `AdapterContext.app`,\n * `getRuntimeApp()`, and (once the driver layer lands) `ctx.req.raw` / `ctx.res.raw`\n * — to that runtime. Mirrors `KickDbRegister` / `KickEnv`, so the type story is\n * uniform with `kick/db` and env typing.\n *\n * @example\n * ```ts\n * // .kickjs/types/kick__runtime.d.ts — generated by typegen\n * declare module '@forinda/kickjs' {\n * interface KickRuntimeRegister {\n * runtime: import('@forinda/kickjs/fastify').FastifyRuntimeTypes\n * }\n * }\n * ```\n */\n\nexport interface KickRuntimeRegister {}\n\n/** The active runtime's type map — the augmented one, or Express by default. */\nexport type ActiveRuntime = KickRuntimeRegister extends { runtime: infer R extends RuntimeTypeMap }\n ? R\n : ExpressRuntimeTypes\n\n/** Options passed to {@link HttpRuntime.createApp}. */\nexport interface RuntimeAppOptions {\n /**\n * Express `trust proxy` setting (and the equivalent on other engines). The\n * function form is Express-specific; other runtimes interpret the simpler\n * shapes (or normalize X-Forwarded-For themselves — see spec §10 Q1).\n */\n trustProxy?: boolean | string | number | string[] | ((ip: string, hopIndex: number) => boolean)\n}\n\n/** Where a connect middleware sits and what it scopes to. */\nexport interface UseConnectOptions {\n path?: string | RegExp | ReadonlyArray<string | RegExp>\n}\n\n/**\n * Optional engine capabilities. Absence means the corresponding `ctx` feature\n * raises a clear error on that runtime rather than failing obscurely.\n */\nexport interface RuntimeCapabilities {\n /** View-engine rendering (`ctx.render`). Express: true; Fastify/h3: false initially. */\n render: boolean\n /** File uploads (`ctx.file` / `@FileUpload`). All engines, different backends. */\n uploads: boolean\n /** Connect-style middleware. Express/Fastify(middie): true; h3: best-effort. */\n connectMiddleware: boolean\n /**\n * The engine parses request bodies itself (Fastify, h3). When true, the\n * Application skips its default `express.json()` so the body stream isn't\n * consumed twice (which would deadlock the engine's own parser). Express:\n * false — it relies on the `express.json()` connect middleware.\n */\n nativeBodyParsing: boolean\n}\n\n/**\n * The engine driver. KickJS owns decorators → {@link RouteTable}, the\n * contributor pipeline, the `RequestContext` surface, and error mapping. The\n * runtime owns app/server creation, route materialization, connect-middleware\n * registration, static serving, and the terminal not-found / error handlers.\n *\n * `expressRuntime()` is the default and the M1 reference implementation.\n */\nexport interface HttpRuntime<TApp = unknown> {\n readonly name: 'express' | 'fastify' | 'h3' | (string & {})\n\n /** Create the engine app. Called once per Application (and per HMR rebuild). */\n createApp(options?: RuntimeAppOptions): TApp\n\n /**\n * Node-compatible request listener — the transport contract.\n * `http.createServer(handler)` in prod, Vite post-middleware in dev. MUST\n * invoke `next` (when given) instead of 404-ing if no route matched — the\n * Vite dev chain depends on fall-through.\n */\n nodeHandler(\n app: TApp,\n ): (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void\n\n /** Materialize the framework-built route table onto the engine. */\n mountRoutes(app: TApp, table: RouteTable): void\n\n /** Register a connect-style middleware (built-ins + adopter middleware). */\n useConnect(app: TApp, mw: ConnectMiddleware, opts?: UseConnectOptions): void\n\n /** Serve a static directory (swagger-ui assets, devtools SPA). */\n serveStatic(app: TApp, prefix: string, dir: string): void\n\n /** Terminal not-found handler. */\n setNotFound(app: TApp, mw: ConnectMiddleware): void\n\n /** Terminal error handler. */\n setErrorHandler(app: TApp, mw: ConnectMiddleware): void\n\n readonly capabilities: RuntimeCapabilities\n}\n\n/**\n * The supported surface adapters use to add HTTP routes, mounts, static dirs,\n * and middleware — engine-agnostic, in place of reaching for the raw Express\n * `app` (spec §4.4). Exposed as {@link AdapterContext.http}. Built by the\n * Application over the active {@link HttpRuntime}, so an adapter written against\n * this works under any runtime; the raw `app` escape hatch stays available for\n * the rare engine-specific need.\n */\nexport interface AdapterHttp {\n /** Register a single context-handler route (e.g. a docs or transport endpoint). */\n route(method: RouteMethod, path: string, handler: CtxHandler): void\n /** Mount a pre-built route table under a path prefix. */\n mount(prefix: string, routes: RouteEntry[]): void\n /** Serve a static directory under a path prefix. */\n serveStatic(prefix: string, dir: string): void\n /** Register a connect-style middleware (optionally path-scoped). */\n use(mw: ConnectMiddleware, opts?: UseConnectOptions): void\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runtime-Dr3WVroF.mjs","names":[],"sources":["../src/http/body-policy.ts","../src/http/middleware/request-scope.ts","../src/http/reply.ts","../src/http/runtime.ts"],"sourcesContent":["/**\n * How a request body is interpreted, decided in ONE place for every runtime.\n *\n * Before this, each engine brought its own library's opinion: Express parsed\n * only exact `application/json` and silently left everything else `undefined`,\n * Fastify answered 415 for anything it had no parser for, and h3 routed\n * unrecognised types through `destr` — so the same request produced a parsed\n * object, an empty body, or a 415 depending on which engine the app was\n * configured with. The runtime is meant to be swappable; that made it not\n * swappable for any app accepting a body outside `application/json` (#590).\n *\n * @module @forinda/kickjs/http/body-policy\n */\n\nimport { HttpException } from '../core/errors'\n\n/**\n * A header as the runtimes hand it over. Node's `IncomingHttpHeaders` and\n * Fastify's normalised map both allow `string[]`, so take the union here\n * rather than casting at every call site.\n */\nexport type HeaderValue = string | string[] | undefined\n\n/** What the framework does with a body of a given media type. */\nexport type BodyKind =\n /** Parse as JSON, strictly. */\n | 'json'\n /** Parse as `application/x-www-form-urlencoded` into an object. */\n | 'urlencoded'\n /** Hand the handler the raw string. Never JSON — see `classifyMediaType`. */\n | 'text'\n /** Left to the upload path, which consumes the stream itself. */\n | 'multipart'\n /** Not parsed. `ctx.body` stays undefined. */\n | 'unsupported'\n\n/**\n * Strip parameters and normalise: `Application/JSON; charset=utf-8` →\n * `application/json`. h3's own check is `===` against the bare type, which is\n * why `application/json; charset=utf-8` — what most clients actually send —\n * missed it and fell to a non-strict branch.\n */\nexport function normalizeMediaType(contentType: HeaderValue): string {\n const value = Array.isArray(contentType) ? contentType[0] : contentType\n return (value ?? '').split(';')[0].trim().toLowerCase()\n}\n\n/**\n * `application/*+json` is JSON, and this is not a judgement call: RFC 6838\n * §4.2.8 says a media type \"MUST NOT be given names incorporating suffixes for\n * structured syntaxes they do not actually employ\", and RFC 6839 §2 exists so\n * receivers can \"do generic processing of the underlying representation\".\n * Spring, ASP.NET Core and Hono's validator all match the suffix by default.\n *\n * So `application/merge-patch+json` (RFC 7396) and `application/problem+json`\n * (RFC 9457) parse as JSON — which the framework already emits for every\n * problem response, and previously could not read back.\n */\nfunction isJsonMediaType(mediaType: string): boolean {\n return mediaType === 'application/json' || /^application\\/[\\w.+-]+\\+json$/.test(mediaType)\n}\n\n/**\n * Decide how to treat a body, from its declared media type alone.\n *\n * `text/*` deliberately yields the raw string and is NEVER JSON-parsed.\n * `text/plain` is one of three CORS-safelisted content types, so it crosses\n * origins without a preflight; JSON-parsing it would re-open the simple-request\n * CSRF that requiring `application/json` closes. h3's own source carries this\n * warning, and h3 v2 pulled back from `destr` partly because of it.\n */\nexport function classifyMediaType(contentType: HeaderValue): BodyKind {\n const mediaType = normalizeMediaType(contentType)\n if (!mediaType) return 'unsupported'\n if (isJsonMediaType(mediaType)) return 'json'\n if (mediaType === 'application/x-www-form-urlencoded') return 'urlencoded'\n if (mediaType.startsWith('multipart/')) return 'multipart'\n if (mediaType.startsWith('text/')) return 'text'\n return 'unsupported'\n}\n\n/**\n * Media types Express should hand to its JSON body parser. `express.json()`\n * defaults to the exact string `application/json`, and `type-is` will not match\n * `application/json` against a bare wildcard-plus-json pattern (a length guard rejects\n * it), so BOTH entries are required — passing only the wildcard silently stops\n * parsing ordinary JSON.\n */\nexport const EXPRESS_JSON_TYPES = ['application/json', 'application/*+json'] as const\n\n/** Media types the framework claims to parse, for diagnostics and docs. */\nexport const SUPPORTED_BODY_TYPES = [\n 'application/json',\n 'application/*+json',\n 'application/x-www-form-urlencoded',\n 'text/*',\n 'multipart/form-data',\n] as const\n\n/**\n * The 415 for a body whose media type the framework does not parse.\n *\n * Only ever raised when a body was actually SENT. An absent or empty body is\n * treated as absent regardless of its declared type — the same call Spring's\n * `readWithMessageConverters` and ASP.NET's `BodyModelBinder` both make, and\n * the reason neither rejects a bodyless POST. A bare `POST` with no payload is\n * a legitimate request, not an unsupported one.\n *\n * Carries `Accept`, which RFC 9110 §15.5.16 says a 415 \"can\" use to indicate\n * which media types would have been accepted.\n */\nexport function unsupportedMediaTypeError(contentType: HeaderValue): HttpException {\n const mediaType = normalizeMediaType(contentType)\n return new HttpException(\n 415,\n mediaType\n ? `Unsupported Media Type: ${mediaType}`\n : 'Unsupported Media Type: request has a body but no Content-Type',\n undefined,\n { Accept: SUPPORTED_BODY_TYPES.join(', ') },\n )\n}\n\n/** Verbs whose body the framework reads. */\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\n/**\n * Did this request actually carry a body? Decided from the framing headers\n * rather than from whether a parser produced anything — the distinction that\n * separates \"nothing was sent\" from \"something was sent that we could not\n * read\", which is the whole basis for answering 415 on one and 200 on the other.\n */\nexport function hasRequestBody(headers: Record<string, unknown>): boolean {\n const length = headers['content-length'] as string | undefined\n const encoding = headers['transfer-encoding'] as string | undefined\n return encoding !== undefined || (length !== undefined && length !== '0')\n}\n\n/**\n * Connect middleware that answers 415 for a body Express's parsers left\n * untouched.\n *\n * Express has no concept of an unsupported type: a parser whose `type` does not\n * match simply calls `next()` and leaves `req.body` undefined, indistinguishable\n * from a request that sent nothing. Mounted after the parsers, this restores the\n * distinction so Express agrees with the other runtimes.\n */\nexport function rejectUnsupportedBody() {\n return (req: any, _res: unknown, next: (err?: unknown) => void): void => {\n if (!BODY_METHODS.has((req.method ?? 'GET').toUpperCase())) return next()\n if (classifyMediaType(req.headers?.['content-type']) !== 'unsupported') return next()\n\n const reject = (): void => next(unsupportedMediaTypeError(req.headers?.['content-type']))\n const length = req.headers?.['content-length'] as string | undefined\n if (length !== undefined) return length === '0' ? next() : reject()\n if (req.headers?.['transfer-encoding'] === undefined) return next()\n\n // Chunked: the framing headers say a body MAY follow, but an empty chunked\n // post sends none. Deciding on the headers alone would 415 a request that\n // sent nothing, so wait to see whether a byte actually arrives — the same\n // distinction the other runtimes make by inspecting the read body.\n let decided = false\n const settle = (fn: () => void) => (): void => {\n if (decided) return\n decided = true\n req.off?.('data', onData)\n req.off?.('end', onEnd)\n fn()\n }\n const onData = settle(reject)\n const onEnd = settle(() => next())\n req.on?.('data', onData)\n req.on?.('end', onEnd)\n }\n}\n","// Web Crypto via globalThis.crypto (randomUUID) — portable across\n// node/bun/deno/workers; node:crypto deliberately NOT imported here so this\n// module stays on the edge-safe request path.\nimport type { Request, Response, NextFunction } from 'express'\nimport { requestStore, type RequestStore } from '../request-store'\nimport { METADATA } from '../../core/interfaces'\nimport { getClassMetaOrUndefined } from '../../core/metadata'\nimport { createLogger } from '../../core/logger'\n\nconst log = createLogger('RequestScope')\n\n// Per-class @PreDestroy method-name cache: the metadata is immutable after\n// class definition, so pay the Reflect read once per class, not once per\n// instance per request. `null` = \"checked, none declared\".\nconst preDestroyCache = new Map<Function, string | symbol | null>()\n\nfunction preDestroyMethod(ctor: Function): string | symbol | null {\n let method = preDestroyCache.get(ctor)\n if (method === undefined) {\n method =\n getClassMetaOrUndefined<string | symbol>(METADATA.PRE_DESTROY, (ctor as any).prototype) ??\n null\n preDestroyCache.set(ctor, method)\n }\n return method\n}\n\n/**\n * Run `@PreDestroy` hooks on every REQUEST-scoped instance the request\n * created, then drop them. Called when the response closes (finished or\n * aborted) — the counterpart to `@PostConstruct`, closing the lifecycle gap\n * where per-request services holding transactions/handles/subscriptions were\n * silently dropped with no cleanup callback.\n *\n * Idempotent (guarded by `store.disposed`); hook errors are logged and\n * swallowed so one failing teardown can't break request completion. Async\n * hooks are fired without being awaited — the response is already gone.\n */\nexport function disposeRequestStore(store: RequestStore): void {\n if (store.disposed) return\n store.disposed = true\n if (store.instances.size === 0) return\n for (const instance of store.instances.values()) {\n if (instance === null || typeof instance !== 'object') continue\n const method = preDestroyMethod(instance.constructor)\n if (!method || typeof instance[method] !== 'function') continue\n try {\n const result = instance[method]()\n if (result && typeof result.then === 'function') {\n ;(result as Promise<void>).catch((err) =>\n log.error(err, `@PreDestroy on ${instance.constructor.name} rejected`),\n )\n }\n } catch (err) {\n log.error(err, `@PreDestroy on ${instance.constructor.name} threw`)\n }\n }\n store.instances.clear()\n}\n\n/**\n * Marker symbol stamped on the function returned by {@link requestScopeMiddleware}.\n *\n * `Application.setup()` uses {@link isRequestScopeMiddleware} to detect whether\n * the user-supplied middleware list already contains the request-scope wrapper,\n * and skips its automatic auto-mount when one is found. Lets adopters control\n * the exact position of the ALS frame in their pipeline (e.g., after a custom\n * tracing wrapper) without ending up with two nested frames.\n */\n// `Symbol.for(...)` returns a plain `symbol` (not `unique symbol`), but the\n// runtime detection marker doesn't need the unique nominal property — it only\n// needs the interned-slot reference equality that `Symbol.for` already provides.\nconst REQUEST_SCOPE_MIDDLEWARE_MARKER = Symbol.for('@kickjs/requestScopeMiddleware')\n\n/**\n * Build a fresh per-request store. Shared by {@link requestScopeMiddleware}\n * (Express) and by runtimes whose handler runs outside the connect-middleware\n * chain (e.g. Fastify), so both establish an identical ALS frame.\n */\nexport function createRequestStore(requestId?: string): RequestStore {\n return {\n requestId: requestId || crypto.randomUUID(),\n instances: new Map(),\n // Explicit generic locks the \"string keys only\" decision.\n values: new Map<string, unknown>(),\n }\n}\n\n/**\n * Wraps each request in an AsyncLocalStorage context.\n * Enables REQUEST-scoped DI and automatic requestId propagation to logs.\n *\n * Should be mounted early in the middleware pipeline (before route handlers).\n * `Application` mounts this automatically unless `contextStore: 'manual'` is set\n * or the user already includes one in their middleware list.\n */\nexport function requestScopeMiddleware() {\n const mw = (req: Request, res: Response, next: NextFunction) => {\n const requestIdHeader = req.headers['x-request-id']\n const requestId =\n (Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader) || crypto.randomUUID()\n const store = createRequestStore(requestId)\n // Surface the requestId on req so the standalone requestId() middleware\n // (when also mounted) reuses this value instead of generating a divergent\n // second one. Without this, log lines that read from the ALS store would\n // disagree with the X-Request-Id response header for any request that\n // didn't carry an inbound x-request-id header.\n ;(req as Request & { requestId?: string }).requestId = requestId\n // 'close' fires once the response finished OR the client aborted — run\n // @PreDestroy teardown for the request's scoped instances either way.\n res.once('close', () => disposeRequestStore(store))\n // Pass `next` directly — `run` invokes it with no args, so the arrow\n // wrapper previously allocated per request added nothing.\n requestStore.run(store, next)\n }\n ;(mw as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] = true\n return mw\n}\n\n/**\n * Returns `true` when `value` is the function produced by {@link requestScopeMiddleware}.\n *\n * Detection uses a `Symbol.for(...)` marker so the check survives different\n * copies of `@forinda/kickjs` loaded under different module identities (e.g.,\n * a published package alongside a workspace-linked one during local dev).\n */\nexport function isRequestScopeMiddleware(value: unknown): boolean {\n if (typeof value !== 'function') return false\n return (value as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] === true\n}\n","// Return-value handler support (response-inference-design.md §3.1).\n//\n// Handlers may RETURN their response payload instead of calling `ctx.json`:\n// the runtime auto-sends it when the pipeline finishes with nothing written.\n// `reply(status, body)` wraps a payload with a non-200 status while staying\n// transparent to type inference (`Reply<201, Task>` carries both statically).\n//\n// Edge-safe: no node imports — shared by all four runtimes including the\n// `@forinda/kickjs/web` entry.\n\nimport type { RequestContext } from './context'\nimport type { RuntimeResponse } from './runtime'\n\n// `Symbol.for` so the brand survives duplicate module copies (workspace-linked\n// + published package in one process — same rationale as the request-scope\n// middleware marker).\nconst REPLY_BRAND = Symbol.for('kickjs.reply')\n\n/**\n * A status-carrying response payload for return-value handlers. Create via\n * {@link reply} — the brand is what runtimes detect, and the generics are\n * what the `kick/routes` response typegen unwraps (`Reply<S, T>` → `T`).\n */\nexport interface Reply<S extends number = number, T = unknown> {\n readonly [REPLY_BRAND]: true\n readonly status: S\n readonly body: T\n}\n\n/**\n * Wrap a return-value handler's payload with an explicit status code:\n *\n * ```ts\n * @Post('/')\n * async create(ctx: RequestContext) {\n * return reply(201, await this.tasks.create(ctx.body))\n * }\n * ```\n */\nexport function reply<S extends number, T>(status: S, body: T): Reply<S, T> {\n return { [REPLY_BRAND]: true, status, body }\n}\n\n/** `reply(200, body)` — explicit form of a bare `return body`. */\nreply.ok = <T>(body: T): Reply<200, T> => reply(200, body)\n/** `reply(201, body)` */\nreply.created = <T>(body: T): Reply<201, T> => reply(201, body)\n/** `reply(202, body)` */\nreply.accepted = <T>(body: T): Reply<202, T> => reply(202, body)\n/** Empty 204 — maps to `ctx.noContent()`. */\nreply.noContent = (): Reply<204, undefined> => reply(204, undefined)\n\n/**\n * The statically-inferred response type of a controller handler — what\n * `kick typegen` emits into `KickRoutes[...].response`:\n *\n * - `Awaited<ReturnType>` of the method\n * - `Reply<S, T>` unwraps to `T`\n * - `void`/`undefined` members are dropped (imperative `ctx.json` branches);\n * a handler that never returns a value stays `unknown`, matching the\n * pre-inference emission\n *\n * ```ts\n * type R = InferHandlerResponse<UsersController['get']> // → User\n * ```\n */\nexport type InferHandlerResponse<H> = H extends (...args: never[]) => infer R\n ? [Exclude<Awaited<R>, void | undefined>] extends [never]\n ? unknown\n : UnwrapReply<Exclude<Awaited<R>, void | undefined>>\n : unknown\n\ntype UnwrapReply<A> =\n A extends Reply<number, infer B>\n ? B\n : A extends RuntimeResponse\n ? // The engine's own response object says nothing about the body, so a\n // handler ending `return ctx.json(...)` degrades to `unknown` — which is\n // what the typegen comment always claimed happened. It did not: the bare\n // `RuntimeResponse` leaked into `KickRoutes[...].response`, offering\n // `.status()` / `.setHeader()` to a client expecting a payload.\n // Confidently wrong is worse than unknown.\n //\n // To carry a payload, return it (`return user`) or wrap it\n // (`return reply(201, user)`) — that is what `reply` is for.\n unknown\n : A\n\nexport function isReply(value: unknown): value is Reply {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[REPLY_BRAND] === true\n )\n}\n\n/**\n * Send a handler's RETURN value when the pipeline finished without anything\n * having written a response. Callers guard on \"driver/response unsettled\" —\n * this only decides how to send:\n *\n * - `undefined` → not handled (returns false; runtime keeps today's behavior)\n * - `Reply` wrapper → status + body (204 → `ctx.noContent()`)\n * - anything else → `ctx.json(value)` (200)\n *\n * Returns true when a response was produced.\n */\nexport function applyHandlerResult(ctx: RequestContext, result: unknown): boolean {\n if (result === undefined) return false\n if (isReply(result)) {\n // Only 204 maps to noContent — an undefined body must NOT coerce the\n // declared status (reply(304, undefined) keeps its 304; the json body\n // is simply empty).\n if (result.status === 204) {\n ctx.noContent()\n return true\n }\n ctx.json(result.body, result.status)\n return true\n }\n ctx.json(result)\n return true\n}\n","// The HttpRuntime seam (spec: docs/http/spec-http-runtimes.md, Avenue B).\n//\n// Decorators no longer emit an `express.Router` directly. `buildRouteTable()`\n// turns controller metadata into a plain-data `RouteEntry[]`; an `HttpRuntime`\n// materializes that table onto whatever engine it owns. Express is the default\n// runtime and the only one shipped in M1 — its materializer reproduces the exact\n// handler chain the old `buildRoutes()` built, so behavior is unchanged.\n//\n// Fastify / h3 runtimes (later milestones) consume the SAME `RouteEntry` data\n// and the SAME `HttpRuntime` contract; the request/response driver abstraction\n// that lets `RequestContext` run engine-agnostically lands with them (M3), since\n// under Express the drivers ARE the Express request/response objects already.\n\nimport { ROUTE_SLOT, type RouteFlags } from '../core/route-flag'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type {\n RequestHandler,\n ErrorRequestHandler,\n Express,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from 'express'\n\nimport type { Constructor, FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core'\nimport type { RequestContext } from './context'\n\n/** HTTP verbs the router emits. Mirrors the decorator surface (@Get/@Post/...). */\nexport type RouteMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n\n/**\n * A context-based handler: the runtime-neutral unit of the request pipeline.\n * Contributors, route middleware (via {@link MiddlewareHandler}), and the\n * terminal controller call all reduce to this shape. The runtime owns how a\n * `CtxHandler` is wrapped onto its engine (Express wraps it in `(req, res,\n * next)`; Fastify in its own handler).\n */\nexport type CtxHandler = (ctx: RequestContext) => unknown | Promise<unknown>\n\n/**\n * Connect-style middleware — the portable middleware format. Built-in\n * middleware and adopter-supplied Express middleware are this shape. The\n * Express runtime hands them straight to `app.use`; later runtimes bridge\n * them (Fastify via `@fastify/middie`, h3 via its node adapter).\n *\n * Typed as the Express handler union for M1 (Express is the only runtime);\n * a future milestone narrows this to bare node `(req, res, next)` primitives\n * once the driver layer lands.\n */\nexport type ConnectMiddleware = RequestHandler | ErrorRequestHandler\n\n/**\n * One materialized route, as plain data. Produced by {@link buildRouteTable}\n * from decorator metadata; consumed by {@link HttpRuntime.mountRoutes}.\n *\n * The shape is deliberately richer than a flat `CtxHandler[]`: `middlewares`\n * keep the `(ctx, next)` signature (they may short-circuit or call `next(err)`),\n * `contributorRunner` is a closure over the pre-built pipeline + container (the\n * pipeline is built — and any cycle / missing-dep error thrown — at table-build\n * time, i.e. boot), and `validation` / `upload` stay as metadata the runtime\n * materializes its own way. Faithfully reproducing the legacy Express chain —\n * not collapsing it — is what keeps M1 behavior-neutral.\n */\nexport interface RouteEntry {\n method: RouteMethod\n /** Path with `:param` segments — the portable subset all engines accept. */\n path: string\n /** Class + method middleware, in execution order; each receives `(ctx, next)`. */\n middlewares: MiddlewareHandler[]\n /**\n * Runs the contributor pipeline against `ctx` (populates `ctx.set(...)`),\n * or `null` when no contributors apply. Closes over the built pipeline and\n * the DI container so it stays engine-neutral; the runtime advances the\n * chain (Express: call `next()`) once it resolves.\n */\n contributorRunner: CtxHandler | null\n /** Terminal handler: resolves the controller per-request and invokes it. */\n handler: CtxHandler\n /** Introspection + runtime-materialized concerns (swagger/typegen read this). */\n meta: RouteMeta\n}\n\n/** Per-route metadata: introspection refs plus runtime-materialized concerns. */\nexport interface RouteMeta {\n /**\n * Owning controller class — for DI resolution and adapter introspection.\n * Absent for ad-hoc routes registered through {@link AdapterHttp.route},\n * which have no controller class behind them.\n */\n controller?: Constructor\n /** Handler method name on the controller, when there is one. */\n handlerName?: string\n /** Zod/JSON-schema validation config from the route decorator, if any. */\n validation?: RouteDefinition['validation']\n /** `@FileUpload` config, if present — the runtime supplies the backend. */\n upload?: FileUploadConfig\n /**\n * Route flags in force here, resolved at build time (method over class).\n * Enabled flags only: a flag declared `false` at a higher-precedence site is\n * absent rather than present-and-false, so `has()` is always the right\n * question. See `core/route-flag.ts`.\n */\n flags?: RouteFlags\n}\n\n/**\n * The route a request matched, published on `ctx.route`.\n *\n * Everything that holds a {@link RequestContext} — handlers, `@Middleware()`,\n * guards, contributors — can read it, which is what lets one declaration on a\n * route serve every consumer.\n */\n/**\n * Publish the matched route on the request, for `ctx.route` to read.\n *\n * Called by each runtime once per dispatch, before it builds a RequestContext.\n * It lives on `req` rather than on the ctx because the Express runtime\n * constructs a fresh RequestContext per middleware step — anything written to\n * one ctx would be invisible to the next and to the handler.\n */\nexport function publishMatchedRoute(req: unknown, entry: RouteEntry): void {\n ;(req as Record<symbol, MatchedRoute>)[ROUTE_SLOT] = {\n method: entry.method,\n path: entry.path,\n controller: entry.meta.controller,\n handlerName: entry.meta.handlerName,\n flags: entry.meta.flags ?? EMPTY_FLAGS,\n }\n}\n\nconst EMPTY_FLAGS: RouteFlags = new Map() as RouteFlags\n\nexport interface MatchedRoute {\n method: RouteMethod\n /** Path as declared on the handler, e.g. `/:id`. */\n path: string\n controller?: Constructor\n handlerName?: string\n /** Enabled flags only — see {@link RouteMeta.flags}. */\n flags: RouteFlags\n}\n\n/** A controller's routes grouped under the module mount prefix. */\nexport type RouteTable = { mountPath: string; routes: RouteEntry[] }[]\n\n/**\n * The response surface {@link RequestContext}'s helpers (`json` / `html` /\n * `sse` / `download` / `render` / `problem`) drive, instead of calling Express\n * `res` methods directly (spec §4.3). Sized so `express.Response` satisfies it\n * structurally — under the Express runtime the driver IS the Express response,\n * so there is no wrapping and no behavior change. Other runtimes (Fastify / h3)\n * provide a thin object implementing this over their native reply.\n *\n * The terminal methods return `RuntimeResponse` for fluent chaining (Express's\n * `Response` is assignable, since it is a superset).\n */\nexport interface RuntimeResponse {\n status(code: number): RuntimeResponse\n json(data: unknown): RuntimeResponse\n send(data: unknown): RuntimeResponse\n type(contentType: string): RuntimeResponse\n setHeader(name: string, value: string | number | readonly string[]): unknown\n render(view: string, data?: Record<string, unknown>): unknown\n writeHead(statusCode: number, headers?: Record<string, string>): unknown\n flushHeaders(): unknown\n write(chunk: string | Buffer): boolean\n end(data?: unknown): unknown\n once(event: 'close', listener: () => void): unknown\n readonly headersSent: boolean\n}\n\n// ── Runtime-typed escape hatches (spec §4.3b) ─────────────────────────────\n\n/**\n * Per-runtime type map: the engine-native request / response / app types an\n * `HttpRuntime` exposes through `ctx.req.raw` / `ctx.res.raw` / `AdapterContext.app`.\n */\nexport interface RuntimeTypeMap {\n request: unknown\n response: unknown\n app: unknown\n}\n\n/** The Express runtime's native types — the default when nothing is augmented. */\nexport interface ExpressRuntimeTypes {\n request: ExpressRequest\n response: ExpressResponse\n app: Express\n}\n\n/**\n * Augmentable runtime registry. Empty by default; the `kick/runtime` typegen\n * plugin emits a `{ runtime: <RuntimeTypeMap> }` augmentation from the configured\n * runtime, which flips the engine-native escape-hatch types — `AdapterContext.app`,\n * `getRuntimeApp()`, and (once the driver layer lands) `ctx.req.raw` / `ctx.res.raw`\n * — to that runtime. Mirrors `KickDbRegister` / `KickEnv`, so the type story is\n * uniform with `kick/db` and env typing.\n *\n * @example\n * ```ts\n * // .kickjs/types/kick__runtime.d.ts — generated by typegen\n * declare module '@forinda/kickjs' {\n * interface KickRuntimeRegister {\n * runtime: import('@forinda/kickjs/fastify').FastifyRuntimeTypes\n * }\n * }\n * ```\n */\n\nexport interface KickRuntimeRegister {}\n\n/** The active runtime's type map — the augmented one, or Express by default. */\nexport type ActiveRuntime = KickRuntimeRegister extends { runtime: infer R extends RuntimeTypeMap }\n ? R\n : ExpressRuntimeTypes\n\n/** Options passed to {@link HttpRuntime.createApp}. */\nexport interface RuntimeAppOptions {\n /**\n * Express `trust proxy` setting (and the equivalent on other engines). The\n * function form is Express-specific; other runtimes interpret the simpler\n * shapes (or normalize X-Forwarded-For themselves — see spec §10 Q1).\n */\n trustProxy?: boolean | string | number | string[] | ((ip: string, hopIndex: number) => boolean)\n}\n\n/** Where a connect middleware sits and what it scopes to. */\nexport interface UseConnectOptions {\n path?: string | RegExp | ReadonlyArray<string | RegExp>\n}\n\n/**\n * Optional engine capabilities. Absence means the corresponding `ctx` feature\n * raises a clear error on that runtime rather than failing obscurely.\n */\nexport interface RuntimeCapabilities {\n /** View-engine rendering (`ctx.render`). Express: true; Fastify/h3: false initially. */\n render: boolean\n /** File uploads (`ctx.file` / `@FileUpload`). All engines, different backends. */\n uploads: boolean\n /** Connect-style middleware. Express/Fastify(middie): true; h3: best-effort. */\n connectMiddleware: boolean\n /**\n * The engine parses request bodies itself (Fastify, h3). When true, the\n * Application skips its default `express.json()` so the body stream isn't\n * consumed twice (which would deadlock the engine's own parser). Express:\n * false — it relies on the `express.json()` connect middleware.\n */\n nativeBodyParsing: boolean\n}\n\n/**\n * The engine driver. KickJS owns decorators → {@link RouteTable}, the\n * contributor pipeline, the `RequestContext` surface, and error mapping. The\n * runtime owns app/server creation, route materialization, connect-middleware\n * registration, static serving, and the terminal not-found / error handlers.\n *\n * `expressRuntime()` is the default and the M1 reference implementation.\n */\nexport interface HttpRuntime<TApp = unknown> {\n readonly name: 'express' | 'fastify' | 'h3' | (string & {})\n\n /** Create the engine app. Called once per Application (and per HMR rebuild). */\n createApp(options?: RuntimeAppOptions): TApp\n\n /**\n * Node-compatible request listener — the transport contract.\n * `http.createServer(handler)` in prod, Vite post-middleware in dev. MUST\n * invoke `next` (when given) instead of 404-ing if no route matched — the\n * Vite dev chain depends on fall-through.\n */\n nodeHandler(\n app: TApp,\n ): (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void\n\n /** Materialize the framework-built route table onto the engine. */\n mountRoutes(app: TApp, table: RouteTable): void\n\n /** Register a connect-style middleware (built-ins + adopter middleware). */\n useConnect(app: TApp, mw: ConnectMiddleware, opts?: UseConnectOptions): void\n\n /** Serve a static directory (swagger-ui assets, devtools SPA). */\n serveStatic(app: TApp, prefix: string, dir: string): void\n\n /** Terminal not-found handler. */\n setNotFound(app: TApp, mw: ConnectMiddleware): void\n\n /** Terminal error handler. */\n setErrorHandler(app: TApp, mw: ConnectMiddleware): void\n\n readonly capabilities: RuntimeCapabilities\n}\n\n/**\n * The supported surface adapters use to add HTTP routes, mounts, static dirs,\n * and middleware — engine-agnostic, in place of reaching for the raw Express\n * `app` (spec §4.4). Exposed as {@link AdapterContext.http}. Built by the\n * Application over the active {@link HttpRuntime}, so an adapter written against\n * this works under any runtime; the raw `app` escape hatch stays available for\n * the rare engine-specific need.\n */\nexport interface AdapterHttp {\n /** Register a single context-handler route (e.g. a docs or transport endpoint). */\n route(method: RouteMethod, path: string, handler: CtxHandler): void\n /** Mount a pre-built route table under a path prefix. */\n mount(prefix: string, routes: RouteEntry[]): void\n /** Serve a static directory under a path prefix. */\n serveStatic(prefix: string, dir: string): void\n /** Register a connect-style middleware (optionally path-scoped). */\n use(mw: ConnectMiddleware, opts?: UseConnectOptions): void\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v8.3.1
2
+ * @forinda/kickjs v8.3.2
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-BYINSxAp.mjs.map
12
+ //# sourceMappingURL=upload-config-oWkCq0nK.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload-config-BYINSxAp.mjs","names":[],"sources":["../src/http/middleware/upload-config.ts"],"sourcesContent":["// Pure (edge-safe) upload core — the engine-neutral half of the upload\n// middleware. Extracted from `upload.ts` so the `@forinda/kickjs/web` entry\n// graph can validate/shape buffered multipart parts WITHOUT dragging in\n// upload.ts's node-only surface (multer peer loading via `node:module`,\n// disk cleanup via `node:fs/promises`). `upload.ts` re-exports everything\n// here, so existing import sites are unchanged.\n\nimport { HttpException, HttpStatus } from '../../core/errors'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core/decorators'\n\nexport const MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * Build a runtime-agnostic `(mimetype, filename) => boolean` file-type filter\n * from `allowedTypes` (a predicate, or a list of extensions / MIME types /\n * wildcards). Returned by both the multer middleware (Express) and the\n * Fastify / h3 / web multipart paths so the accept/reject rule stays\n * identical across engines. No `allowedTypes` → accept everything.\n */\nexport function buildFileTypeFilter(\n allowedTypes: BaseUploadOptions['allowedTypes'],\n customMimeMap?: Record<string, string>,\n): (mimetype: string, filename: string) => boolean {\n if (!allowedTypes) return () => true\n if (typeof allowedTypes === 'function') return allowedTypes\n const mimeMap = customMimeMap ? { ...MIME_MAP, ...customMimeMap } : MIME_MAP\n const resolved = allowedTypes.map((t) => mimeMap[t.toLowerCase().replace(/^\\./, '')] ?? t)\n return (mimetype: string) =>\n resolved.some((type) =>\n type.endsWith('/*') ? mimetype.startsWith(type.replace('/*', '/')) : mimetype === type,\n )\n}\n\n/** A multer-shaped uploaded file — what `ctx.file` / `ctx.files` expose. */\nexport interface UploadedFileLike {\n fieldname: string\n originalname: string\n encoding: string\n mimetype: string\n size: number\n buffer: Buffer\n}\n\n/** A buffered multipart file part, as the runtime hands it to {@link applyUploadConfig}. */\nexport interface RawUploadPart {\n fieldname: string\n filename: string\n mimetype: string\n buffer: Buffer\n}\n\nconst DEFAULT_MAX_SIZE = 5 * 1024 * 1024\n\n/**\n * Apply a `@FileUpload` config to already-buffered multipart parts and return\n * the multer-shaped `file` / `files` for `ctx`. Enforces the field name, type\n * filter, per-file size limit, and (array mode) max count — throwing\n * `HttpException` on a violation, mirroring the Express/multer error path.\n * Used by the Fastify, h3 and web runtimes; Express keeps using multer directly.\n */\nexport function applyUploadConfig(\n parts: RawUploadPart[],\n config: FileUploadConfig,\n): { file?: UploadedFileLike; files?: UploadedFileLike[] } {\n if (config.mode === 'none') return {}\n\n const field = config.fieldName ?? 'file'\n const maxSize = config.maxSize ?? DEFAULT_MAX_SIZE\n const typeAllowed = buildFileTypeFilter(config.allowedTypes, config.customMimeMap)\n\n const matched = parts.filter((p) => p.fieldname === field)\n const files: UploadedFileLike[] = matched.map((p) => {\n if (!typeAllowed(p.mimetype, p.filename)) {\n throw new HttpException(\n HttpStatus.UNSUPPORTED_MEDIA_TYPE,\n `File type ${p.mimetype} is not allowed`,\n )\n }\n if (p.buffer.length > maxSize) {\n throw new HttpException(\n HttpStatus.PAYLOAD_TOO_LARGE,\n `File ${p.filename} exceeds the ${maxSize}-byte limit`,\n )\n }\n return {\n fieldname: p.fieldname,\n originalname: p.filename,\n encoding: '7bit',\n mimetype: p.mimetype,\n size: p.buffer.length,\n buffer: p.buffer,\n }\n })\n\n if (config.mode === 'single') return { file: files[0] }\n const maxCount = config.maxCount ?? 10\n return { files: files.slice(0, maxCount) }\n}\n"],"mappings":";;;;;;;;;;wDAUA,MAAa,SAAmC,CAE9C,IAAK,aACL,KAAM,aACN,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,gBACL,IAAK,YACL,IAAK,eACL,KAAM,aACN,IAAK,aACL,KAAM,aAEN,IAAK,kBACL,IAAK,qBACL,KAAM,0EACN,IAAK,2BACL,KAAM,oEACN,IAAK,gCACL,KAAM,4EACN,IAAK,0CACL,IAAK,iDACL,IAAK,kDACL,IAAK,kBACL,IAAK,aACL,IAAK,WAEL,IAAK,kBACL,GAAI,mBACJ,IAAK,oBACL,IAAK,sBACL,KAAM,8BAEN,IAAK,aACL,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,YAEL,IAAK,YACL,KAAM,aACN,IAAK,kBACL,IAAK,kBACL,IAAK,mBAEL,KAAM,mBACN,IAAK,kBACL,KAAM,WACR,EAYA,SAAgB,iBAAiB,MAA2B,CAC1D,OAAO,MAAM,IAAK,GAAM,CACtB,IAAM,MAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,EAC/C,OAAO,SAAS,QAAU,CAC5B,CAAC,CACH,CASA,SAAgB,oBACd,aACA,cACiD,CACjD,GAAI,CAAC,aAAc,UAAa,GAChC,GAAI,OAAO,cAAiB,WAAY,OAAO,aAC/C,IAAM,QAAU,cAAgB,CAAE,GAAG,SAAU,GAAG,aAAc,EAAI,SAC9D,SAAW,aAAa,IAAK,GAAM,QAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,IAAM,CAAC,EACzF,MAAQ,WACN,SAAS,KAAM,MACb,KAAK,SAAS,IAAI,EAAI,SAAS,WAAW,KAAK,QAAQ,KAAM,GAAG,CAAC,EAAI,WAAa,IACpF,CACJ,CA6BA,SAAgB,kBACd,MACA,OACyD,CACzD,GAAI,OAAO,OAAS,OAAQ,MAAO,CAAC,EAEpC,IAAM,MAAQ,OAAO,WAAa,OAC5B,QAAU,OAAO,SAAW,QAC5B,YAAc,oBAAoB,OAAO,aAAc,OAAO,aAAa,EAG3E,MADU,MAAM,OAAQ,GAAM,EAAE,YAAc,KACZ,CAAC,CAAC,IAAK,GAAM,CACnD,GAAI,CAAC,YAAY,EAAE,SAAU,EAAE,QAAQ,EACrC,MAAM,IAAI,cACR,WAAW,uBACX,aAAa,EAAE,SAAS,gBAC1B,EAEF,GAAI,EAAE,OAAO,OAAS,QACpB,MAAM,IAAI,cACR,WAAW,kBACX,QAAQ,EAAE,SAAS,eAAe,QAAQ,YAC5C,EAEF,MAAO,CACL,UAAW,EAAE,UACb,aAAc,EAAE,SAChB,SAAU,OACV,SAAU,EAAE,SACZ,KAAM,EAAE,OAAO,OACf,OAAQ,EAAE,MACZ,CACF,CAAC,EAED,GAAI,OAAO,OAAS,SAAU,MAAO,CAAE,KAAM,MAAM,EAAG,EACtD,IAAM,SAAW,OAAO,UAAY,GACpC,MAAO,CAAE,MAAO,MAAM,MAAM,EAAG,QAAQ,CAAE,CAC3C"}
1
+ {"version":3,"file":"upload-config-oWkCq0nK.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
@@ -11,7 +11,7 @@ import { a as KvSessionStoreOptions, c as InferHandlerResponse, d as isReply, f
11
11
  * the request's `AbortSignal` — the only EventEmitter surface the context
12
12
  * layer uses (`ctx.signal`, SSE close).
13
13
  */
14
- declare class WebRequestShim {
14
+ export declare class WebRequestShim {
15
15
  readonly method: string;
16
16
  readonly url: string;
17
17
  /** Lower-cased plain header object (RequestContext indexes it directly). */
@@ -44,7 +44,7 @@ declare class WebRequestShim {
44
44
  * A class so the method table lives on a shared prototype (one allocation
45
45
  * per request — same rationale as the Fastify/h3 driver classes).
46
46
  */
47
- declare class WebResponseDriver implements RuntimeResponse {
47
+ export declare class WebResponseDriver implements RuntimeResponse {
48
48
  private readonly signal?;
49
49
  /** Resolves with the final (or streamed) Response. */
50
50
  readonly ready: Promise<Response>;
@@ -95,10 +95,10 @@ interface WebRouteHooks {
95
95
  * Everything derivable at route-build time (validator, upload config) is
96
96
  * hoisted here — per-request work is shim + store + pipeline only.
97
97
  */
98
- declare function compileWebRoute(entry: RouteEntry, hooks?: WebRouteHooks): (invocation: WebRouteInvocation) => Promise<Response>;
98
+ export declare function compileWebRoute(entry: RouteEntry, hooks?: WebRouteHooks): (invocation: WebRouteInvocation) => Promise<Response>;
99
99
  //#endregion
100
100
  //#region src/web.d.ts
101
- interface CreateWebAppOptions {
101
+ export interface CreateWebAppOptions {
102
102
  /**
103
103
  * The h3 v2 module, statically imported by the caller:
104
104
  * `import * as h3 from 'h3'`. Passed in (rather than loaded here) because
@@ -130,7 +130,7 @@ interface CreateWebAppOptions {
130
130
  */
131
131
  middlewares?: MiddlewareHandler[];
132
132
  }
133
- interface WebApp {
133
+ export interface WebApp {
134
134
  /** Web-standard entry: `Request` in, `Response` out. */
135
135
  fetch: (request: Request) => Promise<Response>;
136
136
  /** The underlying h3 v2 app, for advanced composition. */
@@ -142,7 +142,7 @@ interface WebApp {
142
142
  * pipeline behave exactly like `bootstrap()`; routes are compiled once here
143
143
  * and served through h3 v2's router.
144
144
  */
145
- declare function createWebApp(options: CreateWebAppOptions): WebApp;
145
+ export declare function createWebApp(options: CreateWebAppOptions): WebApp;
146
146
  /**
147
147
  * Cloudflare Workers convenience: lazily build the app on the first request
148
148
  * so the Workers `env` binding can seed config before any module resolves.
@@ -151,9 +151,9 @@ declare function createWebApp(options: CreateWebAppOptions): WebApp;
151
151
  * export default createFetchHandler((env) => ({ h3, modules, env }))
152
152
  * ```
153
153
  */
154
- declare function createFetchHandler(build: (env: Record<string, string | undefined>) => CreateWebAppOptions): {
154
+ export declare function createFetchHandler(build: (env: Record<string, string | undefined>) => CreateWebAppOptions): {
155
155
  fetch: (request: Request, env?: Record<string, string | undefined>) => Promise<Response>;
156
156
  };
157
157
  //#endregion
158
- export { CreateWebAppOptions, type InferHandlerResponse, type KvLike, KvRateLimitStore, type KvRateLimitStoreOptions, KvSessionStore, type KvSessionStoreOptions, type RateLimitGuardOptions, type Reply, type SseHandler, WebApp, WebRequestShim, WebResponseDriver, compileWebRoute, createFetchHandler, createWebApp, isReply, rateLimitGuard, reply };
158
+ export { type InferHandlerResponse, type KvLike, KvRateLimitStore, type KvRateLimitStoreOptions, KvSessionStore, type KvSessionStoreOptions, type RateLimitGuardOptions, type Reply, type SseHandler, isReply, rateLimitGuard, reply };
159
159
  //# sourceMappingURL=web.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"web.d.mts","names":[],"sources":["../src/http/web/driver.ts","../src/http/web/handler.ts","../src/web.ts"],"mappings":";;;;;;;;;;;;;cAiBa;WACF;WACA;;WAEA,SAAS;EAClB;EACA,QAAQ;EACR,OAAO;EACP;EACA;EACA;GAGC;mBAEgB;EAEjB,YAAY,SAAS,SAAS,KAAK;;EAmBnC,KAAK,eAAe;;EAMpB,GAAG,eAAe;;;;;;;;;;;;;;;;cA0BP,6BAA6B;mBAYX;;WAVpB,OAAO,QAAQ;;EAExB;UAEQ;mBACS;UACT;UACA;UACA;EAER,YAA6B,SAAS;EAMtC,OAAO;EAKP,KAAK;EAML,KAAK;EAYL,KAAK;EAKL,UAAU,cAAc;EAUxB;EAIA,UAAU,oBAAoB,UAAU;EASxC;EAKA,MAAM;EAQN,IAAI;EAUJ,KAAK,eAAe,cAAc;MAM9B;;UAKI;;UAgBA;;;;UCtLO;EACf,SAAS;EACT,KAAK;EACL,QAAQ;;UAGO;;;;;;;;EAQf,WAAW,cAAc,KAAK,gBAAgB,KAAK,6BAA6B;;;;;;;iBAkDlE,gBACd,OAAO,YACP,QAAQ,iBACN,YAAY,uBAAuB,QAAQ;;;UCxB9B;;;;;;;EAOf;;EAEA,SAAS;;EAET;;;;;EAKA;;EAEA,eAAe;;;;;;EAMf,MAAM;;;;;;EAMN,cAAc;;UAGC;;EAEf,QAAQ,SAAS,YAAY,QAAQ;;EAErC;;;;;;;;iBASc,aAAa,SAAS,sBAAsB;;;;;;;;;iBAiH5C,mBACd,QAAQ,KAAK,uCAAuC;EACjD,QAAQ,SAAS,SAAS,MAAM,uCAAuC,QAAQ"}
1
+ {"version":3,"file":"web.d.mts","names":[],"sources":["../src/http/web/driver.ts","../src/http/web/handler.ts","../src/web.ts"],"mappings":";;;;;;;;;;;;;qBAiBa;WACF;WACA;;WAEA,SAAS;EAClB;EACA,QAAQ;EACR,OAAO;EACP;EACA;EACA;GAGC;mBAEgB;EAEjB,YAAY,SAAS,SAAS,KAAK;;EAmBnC,KAAK,eAAe;;EAMpB,GAAG,eAAe;;;;;;;;;;;;;;;;qBA0BP,6BAA6B;mBAYX;;WAVpB,OAAO,QAAQ;;EAExB;UAEQ;mBACS;UACT;UACA;UACA;EAER,YAA6B,SAAS;EAMtC,OAAO;EAKP,KAAK;EAML,KAAK;EAYL,KAAK;EAKL,UAAU,cAAc;EAUxB;EAIA,UAAU,oBAAoB,UAAU;EASxC;EAKA,MAAM;EAQN,IAAI;EAUJ,KAAK,eAAe,cAAc;MAM9B;;UAKI;;UAgBA;;;;UCtLO;EACf,SAAS;EACT,KAAK;EACL,QAAQ;;UAGO;;;;;;;;EAQf,WAAW,cAAc,KAAK,gBAAgB,KAAK,6BAA6B;;;;;;;wBAkDlE,gBACd,OAAO,YACP,QAAQ,iBACN,YAAY,uBAAuB,QAAQ;;;iBCxB9B;;;;;;;EAOf;;EAEA,SAAS;;EAET;;;;;EAKA;;EAEA,eAAe;;;;;;EAMf,MAAM;;;;;;EAMN,cAAc;;iBAGC;;EAEf,QAAQ,SAAS,YAAY,QAAQ;;EAErC;;;;;;;;wBASc,aAAa,SAAS,sBAAsB;;;;;;;;;wBAiH5C,mBACd,QAAQ,KAAK,uCAAuC;EACjD,QAAQ,SAAS,SAAS,MAAM,uCAAuC,QAAQ"}
package/dist/web.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v8.3.1
2
+ * @forinda/kickjs v8.3.2
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-CzBE79TS.mjs";import{t as MutableModuleRegistry}from"./module-registry-D6lAL81K.mjs";import{buildMountPath}from"./core/path.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-B5Vbzba3.mjs";import{i as reply,r as isReply}from"./runtime-DhY6i4TY.mjs";import{i as requestStore}from"./context-CCo_tD5m.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-BQ5jox2K.mjs";import{n as WebRequestShim,r as WebResponseDriver,t as compileWebRoute}from"./handler-HmFLwQ8M.mjs";function createWebApp(options){let h3mod=options.h3;if(typeof h3mod?.H3!=`function`)throw Error(`@forinda/kickjs/web: pass the h3 v2 module — \`createWebApp({ h3: await import("h3"), ... })\`. The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.`);let container=Container.getInstance();if(options.env){let env=options.env;Container._envResolver=key=>env[key]}Container._requestStoreProvider=()=>requestStore.getStore()??null;let registry=new MutableModuleRegistry;for(let m of options.modules)registry.mount(m);let modules=registry.entries.map(entry=>{let mod=typeof entry==`function`?new entry:entry;return mod.register?.(container),mod});container.bootstrap();let app=new h3mod.H3,apiPrefix=options.apiPrefix??`/api`,defaultVersion=options.defaultVersion??1,globalMiddleware=options.middlewares??[],globalSources=(options.contributors??[]).map(registration=>({source:`global`,registration,label:`createWebApp`})),seenRoutes=new Map;for(let mod of modules){let declaredName=mod.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:mod.constructor?.name??`module`;_setExternalContributorSources([...(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel})),...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 mountPath=buildMountPath(apiPrefix,route.version??defaultVersion,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:request=>app.fetch(request),h3:app}}function createFetchHandler(build){let app;return{fetch:(request,env={})=>(app??=createWebApp(build(env)),app.fetch(request))}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{KvRateLimitStore,KvSessionStore,WebRequestShim,WebResponseDriver,compileWebRoute,createFetchHandler,createWebApp,isReply,rateLimitGuard,reply};
11
+ import{t as Container}from"./container-DYDEiKNR.mjs";import{t as MutableModuleRegistry}from"./module-registry-BXUTVQZX.mjs";import{buildMountPath}from"./core/path.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-mGBlnvon.mjs";import{i as reply,r as isReply}from"./runtime-Dr3WVroF.mjs";import{i as requestStore}from"./context-DVzzDJqS.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-CL1FlCuc.mjs";import{n as WebRequestShim,r as WebResponseDriver,t as compileWebRoute}from"./handler-Cm5zNANP.mjs";function createWebApp(options){let h3mod=options.h3;if(typeof h3mod?.H3!=`function`)throw Error(`@forinda/kickjs/web: pass the h3 v2 module — \`createWebApp({ h3: await import("h3"), ... })\`. The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.`);let container=Container.getInstance();if(options.env){let env=options.env;Container._envResolver=key=>env[key]}Container._requestStoreProvider=()=>requestStore.getStore()??null;let registry=new MutableModuleRegistry;for(let m of options.modules)registry.mount(m);let modules=registry.entries.map(entry=>{let mod=typeof entry==`function`?new entry:entry;return mod.register?.(container),mod});container.bootstrap();let app=new h3mod.H3,apiPrefix=options.apiPrefix??`/api`,defaultVersion=options.defaultVersion??1,globalMiddleware=options.middlewares??[],globalSources=(options.contributors??[]).map(registration=>({source:`global`,registration,label:`createWebApp`})),seenRoutes=new Map;for(let mod of modules){let declaredName=mod.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:mod.constructor?.name??`module`,moduleSources=(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel}));_setExternalContributorSources([...moduleSources,...globalSources]);try{let result=mod.routes();if(!result)continue;let routeSets=Array.isArray(result)?result:[result];for(let route of routeSets){if(!route.controller)throw Error(`@forinda/kickjs/web: module route at '${route.path}' has no controller. Express-style \`router\` mounts are not supported on the web entry.`);let version=route.version??defaultVersion,mountPath=buildMountPath(apiPrefix,version,route.path);for(let entry of buildRouteTable(route.controller,{mountFlags:route.flags})){let url=joinPath(mountPath,entry.path),owner=`${route.controller.name??`controller`}.${String(entry.meta.handlerName??`?`)}`;assertRouteUnique(seenRoutes,entry.method,url,owner),globalMiddleware.length>0&&(entry.middlewares=[...globalMiddleware,...entry.middlewares]);let run=compileWebRoute(entry);app.on(entry.method,url,event=>run({request:event.req,url:event.url,params:event.context.params??{}}))}}}finally{_setExternalContributorSources([])}}return{fetch:request=>app.fetch(request),h3:app}}function createFetchHandler(build){let app;return{fetch:(request,env={})=>(app??=createWebApp(build(env)),app.fetch(request))}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{KvRateLimitStore,KvSessionStore,WebRequestShim,WebResponseDriver,compileWebRoute,createFetchHandler,createWebApp,isReply,rateLimitGuard,reply};
12
12
  //# sourceMappingURL=web.mjs.map
package/dist/web.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"web.mjs","names":[],"sources":["../src/web.ts"],"sourcesContent":["// `@forinda/kickjs/web` — the web-standard fetch entry for edge runtimes\n// (Cloudflare Workers), Bun, and Deno. See web-standards-edge-design.md §3.\n//\n// PURITY CONTRACT: this entry's import graph must stay free of node-only\n// runtime imports — no `node:http`, `node:cluster`, `node:fs`, `node:module`,\n// no `express`, no `Application`/`bootstrap`. The single sanctioned node API\n// is `node:async_hooks` (AsyncLocalStorage — available on Workers with\n// `nodejs_compat`, Deno, and Bun). Enforced by the bundle-graph purity test.\n//\n// ```ts\n// // Cloudflare Workers (wrangler.toml: compatibility_flags = [\"nodejs_compat\"])\n// import { createWebApp } from '@forinda/kickjs/web'\n// import * as h3 from 'h3'\n// import { modules } from './modules'\n//\n// const app = createWebApp({ h3, modules })\n// export default { fetch: (req: Request) => app.fetch(req) }\n//\n// // Bun\n// Bun.serve({ fetch: (req) => app.fetch(req) })\n// ```\n\nimport { Container } from './core/container'\nimport { MutableModuleRegistry } from './core/module-registry'\nimport type { AppModule, AppModuleClass, AppModuleEntry } from './core/app-module'\nimport type { ContributorRegistrations } from './core/context-decorator'\nimport type { MiddlewareHandler } from './core/decorators'\nimport type { SourcedRegistration } from './core/contributor-pipeline'\nimport {\n _setExternalContributorSources,\n assertRouteUnique,\n buildRouteTable,\n} from './http/router-builder'\nimport { requestStore } from './http/request-store'\nimport { compileWebRoute } from './http/web/handler'\nimport { buildMountPath } from './core/path'\n\nexport { WebRequestShim, WebResponseDriver } from './http/web/driver'\nexport { compileWebRoute } from './http/web/handler'\n// Return-value handler helpers — edge consumers can't import the main barrel.\nexport { reply, isReply, type Reply, type InferHandlerResponse } from './http/reply'\nexport type { SseHandler } from './http/context'\n// Edge-safe stores + ctx-style rate limiter (zero runtime imports).\nexport {\n KvRateLimitStore,\n KvSessionStore,\n type KvLike,\n type KvRateLimitStoreOptions,\n type KvSessionStoreOptions,\n} from './http/middleware/kv-stores'\nexport { rateLimitGuard, type RateLimitGuardOptions } from './http/middleware/rate-limit-guard'\n\n// Minimal structural surface of h3 v2 used here (kept local so this entry\n// never imports the node-coupled h3-web runtime module).\ninterface H3AppLike {\n on(method: string, path: string, handler: (event: H3EventLike) => unknown): unknown\n all(path: string, handler: (event: H3EventLike) => unknown): unknown\n fetch(request: Request): Promise<Response>\n}\ninterface H3EventLike {\n req: Request\n url: URL\n context: { params?: Record<string, string> }\n}\n\nexport interface CreateWebAppOptions {\n /**\n * The h3 v2 module, statically imported by the caller:\n * `import * as h3 from 'h3'`. Passed in (rather than loaded here) because\n * edge bundlers have no `createRequire`, and a static `import 'h3'` inside\n * this entry would force the peer on every consumer of the subpath.\n */\n h3: unknown\n /** App modules — same shapes `bootstrap({ modules })` accepts. */\n modules: AppModuleEntry[]\n /** Route prefix (default '/api') — parity with bootstrap. */\n apiPrefix?: string\n /**\n * Default route version (default 1) — parity with bootstrap. `false` opts\n * out of URL versioning; a module's own `version` still overrides it.\n */\n defaultVersion?: number | false\n /** Global Context Contributors — parity with `bootstrap({ contributors })`. */\n contributors?: ContributorRegistrations\n /**\n * Env values for ConfigService/@Value resolution. On Workers, pass the\n * `env` binding from the fetch handler (see `createFetchHandler`); on\n * Bun/Deno, ambient `process.env` already works and this can be omitted.\n */\n env?: Record<string, string | undefined>\n /**\n * Global `(ctx, next)` middlewares, run before route middlewares on every\n * route — the web-entry counterpart of `bootstrap({ middlewares })`. Only\n * ctx-style handlers are accepted (no connect/express middleware here).\n */\n middlewares?: MiddlewareHandler[]\n}\n\nexport interface WebApp {\n /** Web-standard entry: `Request` in, `Response` out. */\n fetch: (request: Request) => Promise<Response>\n /** The underlying h3 v2 app, for advanced composition. */\n h3: unknown\n}\n\n/**\n * Build a KickJS app as a web-standard fetch handler — no node http server,\n * no Application/bootstrap. Module registration, DI, and the contributor\n * pipeline behave exactly like `bootstrap()`; routes are compiled once here\n * and served through h3 v2's router.\n */\nexport function createWebApp(options: CreateWebAppOptions): WebApp {\n const h3mod = options.h3 as { H3?: new () => H3AppLike }\n if (typeof h3mod?.H3 !== 'function') {\n throw new Error(\n '@forinda/kickjs/web: pass the h3 v2 module — `createWebApp({ h3: await import(\"h3\"), ... })`. ' +\n \"The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.\",\n )\n }\n\n const container = Container.getInstance()\n\n // Wire ConfigService/@Value env resolution when the platform has no\n // ambient process.env (Workers). Latest-wins like loadEnv().\n if (options.env) {\n const env = options.env\n Container._envResolver = (key: string) => env[key]\n }\n\n // Request-scope provider — same wiring Application.setup() performs.\n Container._requestStoreProvider = () => requestStore.getStore() ?? null\n\n // Module mounting — mirrors Application.setup() steps 1:1 for the module\n // path (plugins/adapters are out of scope for the edge entry at launch).\n const registry = new MutableModuleRegistry()\n for (const m of options.modules) registry.mount(m)\n const modules = registry.entries.map((entry) => {\n const mod: AppModule = typeof entry === 'function' ? new (entry as AppModuleClass)() : entry\n mod.register?.(container)\n return mod\n })\n container.bootstrap()\n\n const app = new h3mod.H3()\n const apiPrefix = options.apiPrefix ?? '/api'\n const defaultVersion = options.defaultVersion ?? 1\n const globalMiddleware = options.middlewares ?? []\n\n const globalSources: SourcedRegistration[] = (options.contributors ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'global',\n registration: registration as SourcedRegistration['registration'],\n label: 'createWebApp',\n }),\n )\n\n // Duplicate-route guard (KICK006) — same boot-time check as the node path.\n const seenRoutes = new Map<string, string>()\n\n for (const mod of modules) {\n const declaredName = (mod as { name?: unknown }).name\n const moduleLabel =\n typeof declaredName === 'string' && declaredName.length > 0\n ? declaredName\n : (mod.constructor?.name ?? 'module')\n const moduleSources: SourcedRegistration[] = (mod.contributors?.() ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'module',\n registration,\n label: moduleLabel,\n }),\n )\n\n _setExternalContributorSources([...moduleSources, ...globalSources])\n try {\n const result = mod.routes()\n if (!result) continue\n const routeSets = Array.isArray(result) ? result : [result]\n for (const route of routeSets) {\n if (!route.controller) {\n throw new Error(\n `@forinda/kickjs/web: module route at '${route.path}' has no controller. ` +\n 'Express-style `router` mounts are not supported on the web entry.',\n )\n }\n const version = route.version ?? defaultVersion\n const mountPath = buildMountPath(apiPrefix, version, route.path)\n for (const entry of buildRouteTable(route.controller, { mountFlags: route.flags })) {\n const url = joinPath(mountPath, entry.path)\n // Per-handler owner so intra-controller duplicates name both methods.\n const owner = `${route.controller.name ?? 'controller'}.${String(entry.meta.handlerName ?? '?')}`\n assertRouteUnique(seenRoutes, entry.method, url, owner)\n if (globalMiddleware.length > 0) {\n entry.middlewares = [...globalMiddleware, ...entry.middlewares]\n }\n const run = compileWebRoute(entry)\n app.on(entry.method, url, (event: H3EventLike) =>\n run({\n request: event.req,\n url: event.url,\n params: event.context.params ?? {},\n }),\n )\n }\n }\n } finally {\n _setExternalContributorSources([])\n }\n }\n\n return {\n fetch: (request: Request) => app.fetch(request),\n h3: app,\n }\n}\n\n/**\n * Cloudflare Workers convenience: lazily build the app on the first request\n * so the Workers `env` binding can seed config before any module resolves.\n *\n * ```ts\n * export default createFetchHandler((env) => ({ h3, modules, env }))\n * ```\n */\nexport function createFetchHandler(\n build: (env: Record<string, string | undefined>) => CreateWebAppOptions,\n): { fetch: (request: Request, env?: Record<string, string | undefined>) => Promise<Response> } {\n let app: WebApp | undefined\n return {\n fetch: (request, env = {}) => {\n app ??= createWebApp(build(env))\n return app.fetch(request)\n },\n }\n}\n\n/** Join a mount prefix and a route path into one URL, collapsing slashes. */\nfunction joinPath(mountPath: string, path: string): string {\n const a = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n if (path === '/' || path === '') return a === '' ? '/' : a\n const b = path.startsWith('/') ? path : `/${path}`\n return `${a}${b}`\n}\n"],"mappings":";;;;;;;;;;6lBA+GA,SAAgB,aAAa,QAAsC,CACjE,IAAM,MAAQ,QAAQ,GACtB,GAAI,OAAO,OAAO,IAAO,WACvB,MAAU,MACR,0KAEF,EAGF,IAAM,UAAY,UAAU,YAAY,EAIxC,GAAI,QAAQ,IAAK,CACf,IAAM,IAAM,QAAQ,IACpB,UAAU,aAAgB,KAAgB,IAAI,IAChD,CAGA,UAAU,0BAA8B,aAAa,SAAS,GAAK,KAInE,IAAM,SAAW,IAAI,sBACrB,IAAK,IAAM,KAAK,QAAQ,QAAS,SAAS,MAAM,CAAC,EACjD,IAAM,QAAU,SAAS,QAAQ,IAAK,OAAU,CAC9C,IAAM,IAAiB,OAAO,OAAU,WAAa,IAAK,MAA6B,MAEvF,OADA,IAAI,WAAW,SAAS,EACjB,GACT,CAAC,EACD,UAAU,UAAU,EAEpB,IAAM,IAAM,IAAI,MAAM,GAChB,UAAY,QAAQ,WAAa,OACjC,eAAiB,QAAQ,gBAAkB,EAC3C,iBAAmB,QAAQ,aAAe,CAAC,EAE3C,eAAwC,QAAQ,cAAgB,CAAC,EAAA,CAAG,IACvE,eAAuC,CACtC,OAAQ,SACM,aACd,MAAO,cACT,EACF,EAGM,WAAa,IAAI,IAEvB,IAAK,IAAM,OAAO,QAAS,CACzB,IAAM,aAAgB,IAA2B,KAC3C,YACJ,OAAO,cAAiB,UAAY,aAAa,OAAS,EACtD,aACC,IAAI,aAAa,MAAQ,SAShC,+BAA+B,CAAC,IARc,IAAI,eAAe,GAAK,CAAC,EAAA,CAAG,IACvE,eAAuC,CACtC,OAAQ,SACR,aACA,MAAO,WACT,EAG6C,EAAG,GAAG,aAAa,CAAC,EACnE,GAAI,CACF,IAAM,OAAS,IAAI,OAAO,EAC1B,GAAI,CAAC,OAAQ,SACb,IAAM,UAAY,MAAM,QAAQ,MAAM,EAAI,OAAS,CAAC,MAAM,EAC1D,IAAK,IAAM,SAAS,UAAW,CAC7B,GAAI,CAAC,MAAM,WACT,MAAU,MACR,yCAAyC,MAAM,KAAK,yFAEtD,EAGF,IAAM,UAAY,eAAe,UADjB,MAAM,SAAW,eACoB,MAAM,IAAI,EAC/D,IAAK,IAAM,SAAS,gBAAgB,MAAM,WAAY,CAAE,WAAY,MAAM,KAAM,CAAC,EAAG,CAClF,IAAM,IAAM,SAAS,UAAW,MAAM,IAAI,EAEpC,MAAQ,GAAG,MAAM,WAAW,MAAQ,aAAa,GAAG,OAAO,MAAM,KAAK,aAAe,GAAG,IAC9F,kBAAkB,WAAY,MAAM,OAAQ,IAAK,KAAK,EAClD,iBAAiB,OAAS,IAC5B,MAAM,YAAc,CAAC,GAAG,iBAAkB,GAAG,MAAM,WAAW,GAEhE,IAAM,IAAM,gBAAgB,KAAK,EACjC,IAAI,GAAG,MAAM,OAAQ,IAAM,OACzB,IAAI,CACF,QAAS,MAAM,IACf,IAAK,MAAM,IACX,OAAQ,MAAM,QAAQ,QAAU,CAAC,CACnC,CAAC,CACH,CACF,CACF,CACF,QAAU,CACR,+BAA+B,CAAC,CAAC,CACnC,CACF,CAEA,MAAO,CACL,MAAQ,SAAqB,IAAI,MAAM,OAAO,EAC9C,GAAI,GACN,CACF,CAUA,SAAgB,mBACd,MAC8F,CAC9F,IAAI,IACJ,MAAO,CACL,OAAQ,QAAS,IAAM,CAAC,KACtB,MAAQ,aAAa,MAAM,GAAG,CAAC,EACxB,IAAI,MAAM,OAAO,EAE5B,CACF,CAGA,SAAS,SAAS,UAAmB,KAAsB,CACzD,IAAM,EAAI,UAAU,SAAS,GAAG,EAAI,UAAU,MAAM,EAAG,EAAE,EAAI,UAG7D,OAFI,OAAS,KAAO,OAAS,GAAW,IAAM,GAAK,IAAM,EAElD,GAAG,IADA,KAAK,WAAW,GAAG,EAAI,KAAO,IAAI,QAE9C"}
1
+ {"version":3,"file":"web.mjs","names":[],"sources":["../src/web.ts"],"sourcesContent":["// `@forinda/kickjs/web` — the web-standard fetch entry for edge runtimes\n// (Cloudflare Workers), Bun, and Deno. See web-standards-edge-design.md §3.\n//\n// PURITY CONTRACT: this entry's import graph must stay free of node-only\n// runtime imports — no `node:http`, `node:cluster`, `node:fs`, `node:module`,\n// no `express`, no `Application`/`bootstrap`. The single sanctioned node API\n// is `node:async_hooks` (AsyncLocalStorage — available on Workers with\n// `nodejs_compat`, Deno, and Bun). Enforced by the bundle-graph purity test.\n//\n// ```ts\n// // Cloudflare Workers (wrangler.toml: compatibility_flags = [\"nodejs_compat\"])\n// import { createWebApp } from '@forinda/kickjs/web'\n// import * as h3 from 'h3'\n// import { modules } from './modules'\n//\n// const app = createWebApp({ h3, modules })\n// export default { fetch: (req: Request) => app.fetch(req) }\n//\n// // Bun\n// Bun.serve({ fetch: (req) => app.fetch(req) })\n// ```\n\nimport { Container } from './core/container'\nimport { MutableModuleRegistry } from './core/module-registry'\nimport type { AppModule, AppModuleClass, AppModuleEntry } from './core/app-module'\nimport type { ContributorRegistrations } from './core/context-decorator'\nimport type { MiddlewareHandler } from './core/decorators'\nimport type { SourcedRegistration } from './core/contributor-pipeline'\nimport {\n _setExternalContributorSources,\n assertRouteUnique,\n buildRouteTable,\n} from './http/router-builder'\nimport { requestStore } from './http/request-store'\nimport { compileWebRoute } from './http/web/handler'\nimport { buildMountPath } from './core/path'\n\nexport { WebRequestShim, WebResponseDriver } from './http/web/driver'\nexport { compileWebRoute } from './http/web/handler'\n// Return-value handler helpers — edge consumers can't import the main barrel.\nexport { reply, isReply, type Reply, type InferHandlerResponse } from './http/reply'\nexport type { SseHandler } from './http/context'\n// Edge-safe stores + ctx-style rate limiter (zero runtime imports).\nexport {\n KvRateLimitStore,\n KvSessionStore,\n type KvLike,\n type KvRateLimitStoreOptions,\n type KvSessionStoreOptions,\n} from './http/middleware/kv-stores'\nexport { rateLimitGuard, type RateLimitGuardOptions } from './http/middleware/rate-limit-guard'\n\n// Minimal structural surface of h3 v2 used here (kept local so this entry\n// never imports the node-coupled h3-web runtime module).\ninterface H3AppLike {\n on(method: string, path: string, handler: (event: H3EventLike) => unknown): unknown\n all(path: string, handler: (event: H3EventLike) => unknown): unknown\n fetch(request: Request): Promise<Response>\n}\ninterface H3EventLike {\n req: Request\n url: URL\n context: { params?: Record<string, string> }\n}\n\nexport interface CreateWebAppOptions {\n /**\n * The h3 v2 module, statically imported by the caller:\n * `import * as h3 from 'h3'`. Passed in (rather than loaded here) because\n * edge bundlers have no `createRequire`, and a static `import 'h3'` inside\n * this entry would force the peer on every consumer of the subpath.\n */\n h3: unknown\n /** App modules — same shapes `bootstrap({ modules })` accepts. */\n modules: AppModuleEntry[]\n /** Route prefix (default '/api') — parity with bootstrap. */\n apiPrefix?: string\n /**\n * Default route version (default 1) — parity with bootstrap. `false` opts\n * out of URL versioning; a module's own `version` still overrides it.\n */\n defaultVersion?: number | false\n /** Global Context Contributors — parity with `bootstrap({ contributors })`. */\n contributors?: ContributorRegistrations\n /**\n * Env values for ConfigService/@Value resolution. On Workers, pass the\n * `env` binding from the fetch handler (see `createFetchHandler`); on\n * Bun/Deno, ambient `process.env` already works and this can be omitted.\n */\n env?: Record<string, string | undefined>\n /**\n * Global `(ctx, next)` middlewares, run before route middlewares on every\n * route — the web-entry counterpart of `bootstrap({ middlewares })`. Only\n * ctx-style handlers are accepted (no connect/express middleware here).\n */\n middlewares?: MiddlewareHandler[]\n}\n\nexport interface WebApp {\n /** Web-standard entry: `Request` in, `Response` out. */\n fetch: (request: Request) => Promise<Response>\n /** The underlying h3 v2 app, for advanced composition. */\n h3: unknown\n}\n\n/**\n * Build a KickJS app as a web-standard fetch handler — no node http server,\n * no Application/bootstrap. Module registration, DI, and the contributor\n * pipeline behave exactly like `bootstrap()`; routes are compiled once here\n * and served through h3 v2's router.\n */\nexport function createWebApp(options: CreateWebAppOptions): WebApp {\n const h3mod = options.h3 as { H3?: new () => H3AppLike }\n if (typeof h3mod?.H3 !== 'function') {\n throw new Error(\n '@forinda/kickjs/web: pass the h3 v2 module — `createWebApp({ h3: await import(\"h3\"), ... })`. ' +\n \"The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.\",\n )\n }\n\n const container = Container.getInstance()\n\n // Wire ConfigService/@Value env resolution when the platform has no\n // ambient process.env (Workers). Latest-wins like loadEnv().\n if (options.env) {\n const env = options.env\n Container._envResolver = (key: string) => env[key]\n }\n\n // Request-scope provider — same wiring Application.setup() performs.\n Container._requestStoreProvider = () => requestStore.getStore() ?? null\n\n // Module mounting — mirrors Application.setup() steps 1:1 for the module\n // path (plugins/adapters are out of scope for the edge entry at launch).\n const registry = new MutableModuleRegistry()\n for (const m of options.modules) registry.mount(m)\n const modules = registry.entries.map((entry) => {\n const mod: AppModule = typeof entry === 'function' ? new (entry as AppModuleClass)() : entry\n mod.register?.(container)\n return mod\n })\n container.bootstrap()\n\n const app = new h3mod.H3()\n const apiPrefix = options.apiPrefix ?? '/api'\n const defaultVersion = options.defaultVersion ?? 1\n const globalMiddleware = options.middlewares ?? []\n\n const globalSources: SourcedRegistration[] = (options.contributors ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'global',\n registration: registration as SourcedRegistration['registration'],\n label: 'createWebApp',\n }),\n )\n\n // Duplicate-route guard (KICK006) — same boot-time check as the node path.\n const seenRoutes = new Map<string, string>()\n\n for (const mod of modules) {\n const declaredName = (mod as { name?: unknown }).name\n const moduleLabel =\n typeof declaredName === 'string' && declaredName.length > 0\n ? declaredName\n : (mod.constructor?.name ?? 'module')\n const moduleSources: SourcedRegistration[] = (mod.contributors?.() ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'module',\n registration,\n label: moduleLabel,\n }),\n )\n\n _setExternalContributorSources([...moduleSources, ...globalSources])\n try {\n const result = mod.routes()\n if (!result) continue\n const routeSets = Array.isArray(result) ? result : [result]\n for (const route of routeSets) {\n if (!route.controller) {\n throw new Error(\n `@forinda/kickjs/web: module route at '${route.path}' has no controller. ` +\n 'Express-style `router` mounts are not supported on the web entry.',\n )\n }\n const version = route.version ?? defaultVersion\n const mountPath = buildMountPath(apiPrefix, version, route.path)\n for (const entry of buildRouteTable(route.controller, { mountFlags: route.flags })) {\n const url = joinPath(mountPath, entry.path)\n // Per-handler owner so intra-controller duplicates name both methods.\n const owner = `${route.controller.name ?? 'controller'}.${String(entry.meta.handlerName ?? '?')}`\n assertRouteUnique(seenRoutes, entry.method, url, owner)\n if (globalMiddleware.length > 0) {\n entry.middlewares = [...globalMiddleware, ...entry.middlewares]\n }\n const run = compileWebRoute(entry)\n app.on(entry.method, url, (event: H3EventLike) =>\n run({\n request: event.req,\n url: event.url,\n params: event.context.params ?? {},\n }),\n )\n }\n }\n } finally {\n _setExternalContributorSources([])\n }\n }\n\n return {\n fetch: (request: Request) => app.fetch(request),\n h3: app,\n }\n}\n\n/**\n * Cloudflare Workers convenience: lazily build the app on the first request\n * so the Workers `env` binding can seed config before any module resolves.\n *\n * ```ts\n * export default createFetchHandler((env) => ({ h3, modules, env }))\n * ```\n */\nexport function createFetchHandler(\n build: (env: Record<string, string | undefined>) => CreateWebAppOptions,\n): { fetch: (request: Request, env?: Record<string, string | undefined>) => Promise<Response> } {\n let app: WebApp | undefined\n return {\n fetch: (request, env = {}) => {\n app ??= createWebApp(build(env))\n return app.fetch(request)\n },\n }\n}\n\n/** Join a mount prefix and a route path into one URL, collapsing slashes. */\nfunction joinPath(mountPath: string, path: string): string {\n const a = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n if (path === '/' || path === '') return a === '' ? '/' : a\n const b = path.startsWith('/') ? path : `/${path}`\n return `${a}${b}`\n}\n"],"mappings":";;;;;;;;;;6lBA+GA,SAAgB,aAAa,QAAsC,CACjE,IAAM,MAAQ,QAAQ,GACtB,GAAI,OAAO,OAAO,IAAO,WACvB,MAAU,MACR,0KAEF,EAGF,IAAM,UAAY,UAAU,YAAY,EAIxC,GAAI,QAAQ,IAAK,CACf,IAAM,IAAM,QAAQ,IACpB,UAAU,aAAgB,KAAgB,IAAI,IAChD,CAGA,UAAU,0BAA8B,aAAa,SAAS,GAAK,KAInE,IAAM,SAAW,IAAI,sBACrB,IAAK,IAAM,KAAK,QAAQ,QAAS,SAAS,MAAM,CAAC,EACjD,IAAM,QAAU,SAAS,QAAQ,IAAK,OAAU,CAC9C,IAAM,IAAiB,OAAO,OAAU,WAAa,IAAK,MAA6B,MAEvF,OADA,IAAI,WAAW,SAAS,EACjB,GACT,CAAC,EACD,UAAU,UAAU,EAEpB,IAAM,IAAM,IAAI,MAAM,GAChB,UAAY,QAAQ,WAAa,OACjC,eAAiB,QAAQ,gBAAkB,EAC3C,iBAAmB,QAAQ,aAAe,CAAC,EAE3C,eAAwC,QAAQ,cAAgB,CAAC,EAAA,CAAG,IACvE,eAAuC,CACtC,OAAQ,SACM,aACd,MAAO,cACT,EACF,EAGM,WAAa,IAAI,IAEvB,IAAK,IAAM,OAAO,QAAS,CACzB,IAAM,aAAgB,IAA2B,KAC3C,YACJ,OAAO,cAAiB,UAAY,aAAa,OAAS,EACtD,aACC,IAAI,aAAa,MAAQ,SAC1B,eAAwC,IAAI,eAAe,GAAK,CAAC,EAAA,CAAG,IACvE,eAAuC,CACtC,OAAQ,SACR,aACA,MAAO,WACT,EACF,EAEA,+BAA+B,CAAC,GAAG,cAAe,GAAG,aAAa,CAAC,EACnE,GAAI,CACF,IAAM,OAAS,IAAI,OAAO,EAC1B,GAAI,CAAC,OAAQ,SACb,IAAM,UAAY,MAAM,QAAQ,MAAM,EAAI,OAAS,CAAC,MAAM,EAC1D,IAAK,IAAM,SAAS,UAAW,CAC7B,GAAI,CAAC,MAAM,WACT,MAAU,MACR,yCAAyC,MAAM,KAAK,yFAEtD,EAEF,IAAM,QAAU,MAAM,SAAW,eAC3B,UAAY,eAAe,UAAW,QAAS,MAAM,IAAI,EAC/D,IAAK,IAAM,SAAS,gBAAgB,MAAM,WAAY,CAAE,WAAY,MAAM,KAAM,CAAC,EAAG,CAClF,IAAM,IAAM,SAAS,UAAW,MAAM,IAAI,EAEpC,MAAQ,GAAG,MAAM,WAAW,MAAQ,aAAa,GAAG,OAAO,MAAM,KAAK,aAAe,GAAG,IAC9F,kBAAkB,WAAY,MAAM,OAAQ,IAAK,KAAK,EAClD,iBAAiB,OAAS,IAC5B,MAAM,YAAc,CAAC,GAAG,iBAAkB,GAAG,MAAM,WAAW,GAEhE,IAAM,IAAM,gBAAgB,KAAK,EACjC,IAAI,GAAG,MAAM,OAAQ,IAAM,OACzB,IAAI,CACF,QAAS,MAAM,IACf,IAAK,MAAM,IACX,OAAQ,MAAM,QAAQ,QAAU,CAAC,CACnC,CAAC,CACH,CACF,CACF,CACF,QAAU,CACR,+BAA+B,CAAC,CAAC,CACnC,CACF,CAEA,MAAO,CACL,MAAQ,SAAqB,IAAI,MAAM,OAAO,EAC9C,GAAI,GACN,CACF,CAUA,SAAgB,mBACd,MAC8F,CAC9F,IAAI,IACJ,MAAO,CACL,OAAQ,QAAS,IAAM,CAAC,KACtB,MAAQ,aAAa,MAAM,GAAG,CAAC,EACxB,IAAI,MAAM,OAAO,EAE5B,CACF,CAGA,SAAS,SAAS,UAAmB,KAAsB,CACzD,IAAM,EAAI,UAAU,SAAS,GAAG,EAAI,UAAU,MAAM,EAAG,EAAE,EAAI,UAG7D,OAFI,OAAS,KAAO,OAAS,GAAW,IAAM,GAAK,IAAM,EAElD,GAAG,IADA,KAAK,WAAW,GAAG,EAAI,KAAO,IAAI,QAE9C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs",
3
- "version": "8.3.1",
3
+ "version": "8.3.2",
4
4
  "description": "A production-grade, decorator-driven Node.js framework for TypeScript — runs on Express, Fastify, or h3 (swap the engine in one line).",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -157,7 +157,7 @@
157
157
  "@swc/core": "^1.16.1",
158
158
  "@types/express": "^5.0.6",
159
159
  "@types/multer": "^2.2.0",
160
- "@types/node": "^26.4.0",
160
+ "@types/node": "^26.4.1",
161
161
  "@types/serve-static": "^2.2.0",
162
162
  "fastify": "^5.12.1",
163
163
  "h3": "1.15.11",
@@ -166,8 +166,8 @@
166
166
  "serve-static": "^2.2.0",
167
167
  "typescript": "^7.0.2",
168
168
  "vite": "^8.2.2",
169
- "vitest": "^4.1.11",
170
- "zod": "^4.5.1"
169
+ "vitest": "^5.0.0",
170
+ "zod": "^4.5.4"
171
171
  },
172
172
  "publishConfig": {
173
173
  "access": "public"
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v8.3.1
3
- *
4
- * Copyright (c) Felix Orinda
5
- *
6
- * This source code is licensed under the MIT license found in the
7
- * LICENSE file in the root directory of this source tree.
8
- *
9
- * @license MIT
10
- */
11
- const FILTER_OPERATORS=new Set([`eq`,`neq`,`gt`,`gte`,`lt`,`lte`,`between`,`in`,`contains`,`starts`,`ends`]),BUILTIN_DEFAULTS={maxLimit:100,defaultLimit:20,maxSearchLength:200,defaultPage:1},defaults={...BUILTIN_DEFAULTS};function setQueryParsingDefaults(partial){partial.maxLimit!==void 0&&(defaults.maxLimit=partial.maxLimit),partial.defaultLimit!==void 0&&(defaults.defaultLimit=partial.defaultLimit),partial.maxSearchLength!==void 0&&(defaults.maxSearchLength=partial.maxSearchLength)}function getQueryParsingDefaults(){return{...defaults}}function resetQueryParsingDefaults(){Object.assign(defaults,BUILTIN_DEFAULTS)}function parseFilters(filterParam,allowedFields,options){if(!filterParam)return[];let items=Array.isArray(filterParam)?filterParam:[filterParam],results=[],allowed=allowedFields?new Set(allowedFields):null,onReject=options?.onReject;for(let item of items){let firstColon=item.indexOf(`:`);if(firstColon===-1)continue;let secondColon=item.indexOf(`:`,firstColon+1);if(secondColon===-1)continue;let field=item.slice(0,firstColon),operator=item.slice(firstColon+1,secondColon),value=item.slice(secondColon+1);if(!(!field||!value)){if(!FILTER_OPERATORS.has(operator)){onReject?.({kind:`filter`,field,reason:`unknown-operator`});continue}if(allowed&&!allowed.has(field)){onReject?.({kind:`filter`,field,reason:`field-not-allowed`});continue}results.push({field,operator,value})}}return results}function parseSort(sortParam,allowedFields,options){if(!sortParam)return[];let items=Array.isArray(sortParam)?sortParam:[sortParam],results=[],allowed=allowedFields?new Set(allowedFields):null,onReject=options?.onReject;for(let item of items){let lastColon=item.lastIndexOf(`:`);if(lastColon===-1)continue;let field=item.slice(0,lastColon),dir=item.slice(lastColon+1).toLowerCase();if(field&&(dir===`asc`||dir===`desc`)){if(allowed&&!allowed.has(field)){onReject?.({kind:`sort`,field,reason:`field-not-allowed`});continue}results.push({field,direction:dir})}}return results}function parsePagination(params,options){let defaultLimit=options?.defaultLimit??defaults.defaultLimit,maxLimit=options?.maxLimit??defaults.maxLimit,page=typeof params.page==`string`?Number.parseInt(params.page,10):params.page??defaults.defaultPage,limit=typeof params.limit==`string`?Number.parseInt(params.limit,10):params.limit??defaultLimit;return(Number.isNaN(page)||page<1)&&(page=defaults.defaultPage),(Number.isNaN(limit)||limit<1)&&(limit=defaultLimit),limit>maxLimit&&(limit=maxLimit),{page,limit,offset:(page-1)*limit}}function parseSearchQuery(q,options){if(!q)return``;let maxLen=options?.maxSearchLength??defaults.maxSearchLength,trimmed=q.trim();return trimmed.length>maxLen?(options?.onReject?.({kind:`search`,field:`q`,reason:`truncated`}),trimmed.slice(0,maxLen)):trimmed}function normalizeFieldConfig(config){if(config)return`columns`in config&&config.columns&&typeof config.columns==`object`?{filterable:Object.keys(config.columns),sortable:config.sortable?Object.keys(config.sortable):void 0,searchable:void 0}:config}function parseQuery(query,fieldConfig,options){let normalized=normalizeFieldConfig(fieldConfig),rejectOpts=options?.onReject?{onReject:options.onReject}:void 0;return{filters:parseFilters(query.filter,normalized?.filterable,rejectOpts),sort:parseSort(query.sort,normalized?.sortable,rejectOpts),pagination:parsePagination({page:query.page,limit:query.limit},options),search:parseSearchQuery(query.q,options)}}function buildQueryParams(parsed){let params={};return parsed.filters?.length&&(params.filter=parsed.filters.map(f=>`${f.field}:${f.operator}:${f.value}`)),parsed.sort?.length&&(params.sort=parsed.sort.map(s=>`${s.field}:${s.direction}`)),parsed.pagination&&(params.page=parsed.pagination.page,params.limit=parsed.pagination.limit),parsed.search&&(params.q=parsed.search),params}export{parseQuery as a,resetQueryParsingDefaults as c,parsePagination as i,setQueryParsingDefaults as l,getQueryParsingDefaults as n,parseSearchQuery as o,parseFilters as r,parseSort as s,buildQueryParams as t,FILTER_OPERATORS as u};
12
- //# sourceMappingURL=query-Dc58mMd1.mjs.map
@@ -1,12 +0,0 @@
1
- /**
2
- * @forinda/kickjs v8.3.1
3
- *
4
- * Copyright (c) Felix Orinda
5
- *
6
- * This source code is licensed under the MIT license found in the
7
- * LICENSE file in the root directory of this source tree.
8
- *
9
- * @license MIT
10
- */
11
- import{METADATA}from"./core/interfaces.mjs";import{o as duplicateRouteError,t as Container}from"./container-CzBE79TS.mjs";import{a as getMethodMeta,o as getMethodMetaOrUndefined,t as getClassMeta}from"./metadata-CugcKCXW.mjs";import{a as getMethodFlagDeclarations,c as registerMountFlags,i as getClassFlagDeclarations,l as resolveRouteFlags,u as toFlagDeclarations}from"./route-flag-BKYcu3L0.mjs";import{i as MissingContributorError,n as DuplicateContributorError,t as ContributorCycleError}from"./context-errors-CH2YMGwk.mjs";import{t as runContributors}from"./contributor-runner-Cf6M_ILY.mjs";const PRECEDENCE_ORDER=[`method`,`class`,`module`,`adapter`,`global`];function buildPipeline(sources,options={}){let route=options.route,bySource=new Map;for(let src of PRECEDENCE_ORDER)bySource.set(src,[]);for(let entry of sources)bySource.get(entry.source).push(entry);let winners=new Map;for(let src of PRECEDENCE_ORDER){let rows=bySource.get(src),keysAtThisLevel=new Map;for(let row of rows){let key=row.registration.key,existing=keysAtThisLevel.get(key)??[];existing.push(row),keysAtThisLevel.set(key,existing)}for(let[key,rowsForKey]of keysAtThisLevel){if(rowsForKey.length>1)throw new DuplicateContributorError(key,rowsForKey.map((r,i)=>r.label??`${src}#${i}`));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 topoSort(registrations,route){let indegree=new Map,dependents=new Map,byKey=new Map;for(let reg of registrations)indegree.set(reg.key,reg.dependsOn.length),byKey.set(reg.key,reg);for(let reg of registrations)for(let dep of reg.dependsOn){let list=dependents.get(dep)??[];list.push(reg),dependents.set(dep,list)}let queue=[];for(let reg of registrations)indegree.get(reg.key)===0&&queue.push(reg);let sorted=[];for(;queue.length>0;){let node=queue.shift();sorted.push(node);for(let dependent of dependents.get(node.key)??[]){let next=(indegree.get(dependent.key)??0)-1;indegree.set(dependent.key,next),next===0&&queue.push(dependent)}}if(sorted.length!==registrations.length)throw new ContributorCycleError(reconstructCycle(registrations.filter(r=>(indegree.get(r.key)??0)>0),byKey),route);return sorted}function reconstructCycle(residual,byKey){let residualKeys=new Set(residual.map(r=>r.key)),start=residual[0].key,path=[start],seen=new Set([start]),cursor=start;for(;;){let nextDep=byKey.get(cursor).dependsOn.find(d=>residualKeys.has(d));if(nextDep===void 0)break;if(seen.has(nextDep)){let cycleStart=path.indexOf(nextDep);return[...path.slice(cycleStart),nextDep]}path.push(nextDep),seen.add(nextDep),cursor=nextDep}return path}let _externalContributorSources=[];function _setExternalContributorSources(sources){_externalContributorSources=sources}function assertRouteUnique(registry,method,fullPath,owner){let key=`${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g,`:*`)}`,prior=registry.get(key);if(prior!==void 0)throw duplicateRouteError(method.toUpperCase(),fullPath,owner,prior);registry.set(key,owner)}function buildRouteTable(controllerClass,options={}){let container=Container.getInstance(),externalSources=options.externalSources??_externalContributorSources,routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classMiddlewares=getClassMeta(METADATA.CLASS_MIDDLEWARES,controllerClass,[]),classContributors=getClassMeta(METADATA.CLASS_CONTRIBUTORS,controllerClass,[]),classFlagDeclarations=getClassFlagDeclarations(controllerClass),mountFlagDeclarations=toFlagDeclarations(options.mountFlags,`${controllerClass?.name??`controller`} mount flags`);registerMountFlags(controllerClass,mountFlagDeclarations);let entries=[];for(let route of routes){let method=route.method.toUpperCase(),fullPath=route.path||`/`,methodMiddlewares=getMethodMeta(METADATA.METHOD_MIDDLEWARES,controllerClass,route.handlerName,[]),fileUploadConfig=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName),methodContributors=getMethodMeta(METADATA.METHOD_CONTRIBUTORS,controllerClass,route.handlerName,[]),flags=resolveRouteFlags(classFlagDeclarations,getMethodFlagDeclarations(controllerClass,route.handlerName),mountFlagDeclarations),contributorRunner=null;if(classContributors.length>0||methodContributors.length>0||externalSources.length>0){let pipeline=buildPipeline([...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],{route:`${method} ${fullPath}`});contributorRunner=ctx=>runContributors({pipeline,ctx,container,flags})}let handler=ctx=>container.resolve(controllerClass)[route.handlerName](ctx);entries.push({method,path:fullPath,middlewares:[...classMiddlewares,...methodMiddlewares],contributorRunner,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-B5Vbzba3.mjs.map