@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.
- package/README.md +1 -1
- package/dist/{application-DT1nNdC7.mjs → application-B2SLsOAo.mjs} +3 -3
- package/dist/{application-DT1nNdC7.mjs.map → application-B2SLsOAo.mjs.map} +1 -1
- package/dist/{bootstrap-C6-CrVgx.mjs → bootstrap-CBoWnjli.mjs} +3 -3
- package/dist/{bootstrap-C6-CrVgx.mjs.map → bootstrap-CBoWnjli.mjs.map} +1 -1
- package/dist/{client-ip-r74dYQsg.mjs → client-ip-BAqx--eV.mjs} +2 -2
- package/dist/{client-ip-r74dYQsg.mjs.map → client-ip-BAqx--eV.mjs.map} +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-Bbqd77CD.mjs → config-xYtlQp17.mjs} +3 -3
- package/dist/{config-Bbqd77CD.mjs.map → config-xYtlQp17.mjs.map} +1 -1
- package/dist/{container-CzBE79TS.mjs → container-DYDEiKNR.mjs} +4 -4
- package/dist/{container-CzBE79TS.mjs.map → container-DYDEiKNR.mjs.map} +1 -1
- package/dist/{context-CCo_tD5m.mjs → context-DVzzDJqS.mjs} +3 -3
- package/dist/{context-CCo_tD5m.mjs.map → context-DVzzDJqS.mjs.map} +1 -1
- package/dist/{context-errors-CH2YMGwk.mjs → context-errors-DBe_u92r.mjs} +2 -2
- package/dist/{context-errors-CH2YMGwk.mjs.map → context-errors-DBe_u92r.mjs.map} +1 -1
- package/dist/{contributor-runner-Cf6M_ILY.mjs → contributor-runner-DiDYluYn.mjs} +3 -3
- package/dist/{contributor-runner-Cf6M_ILY.mjs.map → contributor-runner-DiDYluYn.mjs.map} +1 -1
- package/dist/core/adapter.mjs +1 -1
- package/dist/core/app-module.mjs +1 -1
- package/dist/core/cache.mjs +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/errors.mjs +1 -1
- package/dist/core/interfaces.mjs +1 -1
- package/dist/core/logger.mjs +1 -1
- package/dist/core/path.mjs +1 -1
- package/dist/core/plugin.mjs +1 -1
- package/dist/core/reactivity.mjs +2 -2
- package/dist/core/reactivity.mjs.map +1 -1
- package/dist/{core-vktCOX0c.mjs → core-C80EsmuT.mjs} +3 -3
- package/dist/{core-vktCOX0c.mjs.map → core-C80EsmuT.mjs.map} +1 -1
- package/dist/{csrf-DiOCjUPJ.mjs → csrf-Ci-NmNHy.mjs} +3 -3
- package/dist/{csrf-DiOCjUPJ.mjs.map → csrf-Ci-NmNHy.mjs.map} +1 -1
- package/dist/{decorate-Bn8r2PEB.mjs → decorate-Ba_J1qTc.mjs} +1 -1
- package/dist/{define-adapter-BBA4UbXZ.mjs → define-adapter-CBXnH8kA.mjs} +2 -2
- package/dist/{define-adapter-BBA4UbXZ.mjs.map → define-adapter-CBXnH8kA.mjs.map} +1 -1
- package/dist/{disposables-DsrxA4Xv.mjs → disposables-DnXJYY9g.mjs} +2 -2
- package/dist/{disposables-DsrxA4Xv.mjs.map → disposables-DnXJYY9g.mjs.map} +1 -1
- package/dist/{env-DmEVaWGi.mjs → env-Dzp4S93f.mjs} +3 -3
- package/dist/{env-DmEVaWGi.mjs.map → env-Dzp4S93f.mjs.map} +1 -1
- package/dist/{handler-HmFLwQ8M.mjs → handler-Cm5zNANP.mjs} +4 -4
- package/dist/{handler-HmFLwQ8M.mjs.map → handler-Cm5zNANP.mjs.map} +1 -1
- package/dist/http/application.mjs +2 -2
- package/dist/http/bootstrap.mjs +2 -2
- package/dist/http/context.mjs +2 -2
- package/dist/http/middleware/cors.mjs +1 -1
- package/dist/http/middleware/csrf.mjs +2 -2
- package/dist/http/middleware/error-handler.mjs +2 -2
- package/dist/http/middleware/helmet.mjs +1 -1
- package/dist/http/middleware/rate-limit.mjs +2 -2
- package/dist/http/middleware/request-id.mjs +1 -1
- package/dist/http/middleware/request-logger.mjs +2 -2
- package/dist/http/middleware/session.mjs +2 -2
- package/dist/http/middleware/session.mjs.map +1 -1
- package/dist/http/middleware/spa.mjs +2 -2
- package/dist/http/middleware/upload.mjs +3 -3
- package/dist/http/middleware/upload.mjs.map +1 -1
- package/dist/http/middleware/validate.mjs +1 -1
- package/dist/http/middleware/views.mjs +2 -2
- package/dist/http/query/index.mjs +2 -2
- package/dist/http/router-builder.mjs +2 -2
- package/dist/http/runtimes/fastify.d.mts +2 -3
- package/dist/http/runtimes/fastify.d.mts.map +1 -1
- package/dist/http/runtimes/fastify.mjs +3 -3
- package/dist/http/runtimes/fastify.mjs.map +1 -1
- package/dist/http/runtimes/h3-web.d.mts +3 -4
- package/dist/http/runtimes/h3-web.d.mts.map +1 -1
- package/dist/http/runtimes/h3-web.mjs +2 -2
- package/dist/http/runtimes/h3-web.mjs.map +1 -1
- package/dist/http/runtimes/h3.d.mts +2 -3
- package/dist/http/runtimes/h3.d.mts.map +1 -1
- package/dist/http/runtimes/h3.mjs +2 -2
- package/dist/http/runtimes/h3.mjs.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{kv-stores-BQ5jox2K.mjs → kv-stores-CL1FlCuc.mjs} +3 -3
- package/dist/{kv-stores-BQ5jox2K.mjs.map → kv-stores-CL1FlCuc.mjs.map} +1 -1
- package/dist/{metadata-CugcKCXW.mjs → metadata-BfAea0Eh.mjs} +2 -2
- package/dist/{metadata-CugcKCXW.mjs.map → metadata-BfAea0Eh.mjs.map} +1 -1
- package/dist/{module-registry-D6lAL81K.mjs → module-registry-BXUTVQZX.mjs} +2 -2
- package/dist/{module-registry-D6lAL81K.mjs.map → module-registry-BXUTVQZX.mjs.map} +1 -1
- package/dist/query-6wpoIQxU.mjs +12 -0
- package/dist/{query-Dc58mMd1.mjs.map → query-6wpoIQxU.mjs.map} +1 -1
- package/dist/{respond-DM0pbGtn.mjs → respond-C1bmis_l.mjs} +2 -2
- package/dist/{respond-DM0pbGtn.mjs.map → respond-C1bmis_l.mjs.map} +1 -1
- package/dist/{route-flag-BKYcu3L0.mjs → route-flag-gN8YUbh7.mjs} +3 -3
- package/dist/{route-flag-BKYcu3L0.mjs.map → route-flag-gN8YUbh7.mjs.map} +1 -1
- package/dist/{route-policy-YrOBnVpp.mjs → route-policy-BmkX461L.mjs} +2 -2
- package/dist/{route-policy-YrOBnVpp.mjs.map → route-policy-BmkX461L.mjs.map} +1 -1
- package/dist/router-builder-mGBlnvon.mjs +12 -0
- package/dist/{router-builder-B5Vbzba3.mjs.map → router-builder-mGBlnvon.mjs.map} +1 -1
- package/dist/{runtime-DhY6i4TY.mjs → runtime-Dr3WVroF.mjs} +3 -3
- package/dist/{runtime-DhY6i4TY.mjs.map → runtime-Dr3WVroF.mjs.map} +1 -1
- package/dist/{upload-config-BYINSxAp.mjs → upload-config-oWkCq0nK.mjs} +2 -2
- package/dist/{upload-config-BYINSxAp.mjs.map → upload-config-oWkCq0nK.mjs.map} +1 -1
- package/dist/web.d.mts +8 -8
- package/dist/web.d.mts.map +1 -1
- package/dist/web.mjs +2 -2
- package/dist/web.mjs.map +1 -1
- package/package.json +4 -4
- package/dist/query-Dc58mMd1.mjs +0 -12
- package/dist/router-builder-B5Vbzba3.mjs +0 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"h3-web.mjs","names":[],"sources":["../../../src/http/runtimes/h3-web.ts"],"sourcesContent":["// The h3 v2 (web-standards) runtime — `@forinda/kickjs/h3-web`.\n//\n// ADDITIVE runtime: the h3 v1 runtime (`./h3.ts`, `@forinda/kickjs/h3`)\n// stays untouched so existing adopters keep working (locked decision,\n// web-standards-edge-design.md §3.1). This runtime targets h3 >= 2 — the\n// web-standard rebase where `event.req` is a WHATWG Request, handlers\n// return values/Responses, and `app.fetch(request)` is the universal entry.\n//\n// The request pipeline runs through the shared web driver pair\n// (`../web/driver`, `../web/handler`) — the same code path the\n// `@forinda/kickjs/web` edge entry uses, so node bootstrap and edge deploys\n// exercise identical request semantics.\n//\n// Known limitation vs the v1 runtime: no Vite dev-server fall-through —\n// unmatched requests are answered by the kick notFound handler (h3 v2 owns\n// the full request; there is no node `next()` bail-out mid-engine). Use the\n// v1 h3 runtime or Express for Vite-integrated dev setups.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { createRequire } from 'node:module'\n\nimport type {\n ConnectMiddleware,\n HttpRuntime,\n RouteTable,\n RuntimeAppOptions,\n UseConnectOptions,\n} from '../runtime'\nimport { compileWebRoute, type WebRouteHooks } from '../web/handler'\nimport { WebRequestShim, WebResponseDriver } from '../web/driver'\nimport { createLogger, describeError } from '../../core/logger'\n\nconst log = createLogger('H3WebRuntime')\n\nconst peerRequire = createRequire(import.meta.url)\n\nconst NOTFOUND_MW = Symbol('kickjs.h3web.notFoundMw')\nconst ERROR_MW = Symbol('kickjs.h3web.errorMw')\nconst ASSEMBLED = Symbol('kickjs.h3web.assembled')\nconst NOOP_NEXT = (): void => {}\n\n// Minimal structural surface of h3 v2 we depend on.\ninterface H3AppLike {\n on(method: string, path: string, handler: (event: H3EventLike) => unknown): unknown\n all(path: string, handler: (event: H3EventLike) => unknown): unknown\n use(...args: unknown[]): unknown\n fetch(request: Request): Promise<Response>\n}\ninterface H3EventLike {\n req: Request & { runtime?: { node?: { req: IncomingMessage; res: ServerResponse } } }\n url: URL\n context: { params?: Record<string, string> }\n}\n\ninterface H3v2Module {\n H3: new (config?: unknown) => H3AppLike\n toNodeHandler: (app: H3AppLike) => (req: IncomingMessage, res: ServerResponse) => void\n fromNodeHandler: (mw: unknown) => unknown\n}\n\nexport interface H3WebRuntimeOptions {\n /**\n * Pre-imported h3 v2 module. Two audiences:\n * - Edge/worker bundlers with no `createRequire` at runtime:\n * `h3WebRuntime({ h3: await import('h3') })`\n * - Tests running both h3 majors side by side via an npm alias.\n * Omitted → the peer is loaded via `createRequire` (node bootstrap path).\n */\n h3?: unknown\n}\n\n/** Resolve h3 (injected or peer-required) and fail fast unless it is v2. */\nfunction loadH3v2(injected?: unknown): H3v2Module {\n const mod = injected ?? peerRequire('h3')\n const m = mod as { __esModule?: boolean; default?: Record<string, unknown> } & Record<\n string,\n unknown\n >\n const resolved = (m && m.__esModule && m.default\n ? { ...m.default, ...m }\n : m) as unknown as H3v2Module\n if (typeof resolved.H3 !== 'function') {\n throw new Error(\n \"@forinda/kickjs: h3WebRuntime() requires h3 v2 (the web-standards line, npm dist-tag 'latest'), \" +\n 'but the installed h3 has no H3 class — that is the v1 line. ' +\n \"Either `pnpm add h3@latest` for this runtime, or keep h3 v1 and use h3Runtime() from '@forinda/kickjs/h3'.\",\n )\n }\n return resolved\n}\n\n/**\n * Bridge a connect-style terminal middleware (kick's notFound / error\n * handlers) onto the web driver pair, returning the produced Response.\n */\nasync function runConnectTerminal(\n mw: ConnectMiddleware | undefined,\n event: H3EventLike,\n err?: unknown,\n): Promise<Response> {\n const req = new WebRequestShim(event.req, event.url)\n const driver = new WebResponseDriver(event.req.signal)\n if (mw) {\n if (err !== undefined) {\n ;(mw as (e: unknown, rq: unknown, rs: unknown, n: () => void) => void)(\n err,\n req,\n driver,\n NOOP_NEXT,\n )\n } else {\n ;(mw as (rq: unknown, rs: unknown, n: () => void) => void)(req, driver, NOOP_NEXT)\n }\n }\n if (!driver.settled) {\n if (err !== undefined) {\n // The configured error handler ran but didn't settle the response\n // (or there wasn't one). Emitting a bare 500 with no log entry\n // makes this failure invisible on both sides — log it here, since\n // by definition nothing downstream will.\n log.error(err, `h3-web: error handler did not settle the response — ${describeError(err)}`)\n driver.status(500).json({\n error: 'Internal Server Error',\n ...(typeof process !== 'undefined' && process.env?.NODE_ENV === 'production'\n ? {}\n : { message: describeError(err) }),\n })\n } else {\n driver.status(404).json({ error: 'Not Found' })\n }\n }\n return driver.ready\n}\n\n/**\n * The h3 v2 HTTP runtime. Pass to `bootstrap({ runtime: h3WebRuntime() })`.\n * Requires the h3 v2 peer; fails fast with guidance when v1 is installed.\n */\nexport function h3WebRuntime(options: H3WebRuntimeOptions = {}): HttpRuntime<H3AppLike> {\n const { H3, toNodeHandler, fromNodeHandler } = loadH3v2(options.h3)\n\n return {\n name: 'h3-web',\n\n createApp(_options: RuntimeAppOptions = {}): H3AppLike {\n const holder: { app?: H3AppLike } = {}\n const app = new H3({\n // Engine-level errors (middleware throws outside the kick pipeline)\n // dispatch to the kick error handler once setErrorHandler stashes it.\n onError: (error: unknown, event: H3EventLike) => {\n const mw = (holder.app as unknown as Record<symbol, ConnectMiddleware | undefined>)?.[\n ERROR_MW\n ]\n return runConnectTerminal(mw, event, error)\n },\n })\n holder.app = app\n return app\n },\n\n nodeHandler(app) {\n const state = app as unknown as Record<symbol, unknown>\n if (!state[ASSEMBLED]) {\n state[ASSEMBLED] = true\n // Terminal catch-all AFTER all mounted routes: kick's own notFound\n // handler answers unmatched paths (h3 v2's default 404 never fires).\n app.all('/**', (event: H3EventLike) => {\n const mw = state[NOTFOUND_MW] as ConnectMiddleware | undefined\n return runConnectTerminal(mw, event)\n })\n }\n const listener = toNodeHandler(app)\n return (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => {\n try {\n const result = listener(req, res) as unknown\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n void (result as Promise<unknown>).catch((err) => next?.(err))\n }\n } catch (err) {\n next?.(err)\n }\n }\n },\n\n mountRoutes(app, table: RouteTable) {\n for (const { mountPath, routes } of table) {\n for (const entry of routes) {\n const url = joinPath(mountPath, entry.path)\n const hooks: WebRouteHooks = {\n onError: async (err, req, driver) => {\n const mw = (app as unknown as Record<symbol, ConnectMiddleware | undefined>)[ERROR_MW]\n if (!mw) return\n ;(mw as (e: unknown, rq: unknown, rs: unknown, n: () => void) => void)(\n err,\n req,\n driver,\n NOOP_NEXT,\n )\n },\n }\n const run = compileWebRoute(entry, hooks)\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 },\n\n useConnect(app, mw: ConnectMiddleware, opts?: UseConnectOptions) {\n // Node bootstrap path only — h3 v2 bridges node middleware via\n // fromNodeHandler (the edge entry never calls useConnect).\n const handler = fromNodeHandler(mw)\n if (opts?.path !== undefined) {\n // v2 `use` is exact-match; `/**` covers the subtree like v1's prefix.\n app.use(joinPath(String(opts.path), '/**'), handler)\n } else {\n app.use(handler)\n }\n },\n\n serveStatic(app, prefix, dir) {\n // serve-static bridged like any other connect middleware (node only).\n const serveStatic = peerRequire('serve-static')\n this.useConnect(app, serveStatic(dir) as ConnectMiddleware, { path: prefix })\n },\n\n setNotFound(app, mw: ConnectMiddleware) {\n ;(app as unknown as Record<symbol, ConnectMiddleware>)[NOTFOUND_MW] = mw\n },\n\n setErrorHandler(app, mw: ConnectMiddleware) {\n ;(app as unknown as Record<symbol, ConnectMiddleware>)[ERROR_MW] = mw\n },\n\n capabilities: {\n render: false,\n uploads: true,\n connectMiddleware: true,\n nativeBodyParsing: true,\n },\n }\n}\n\n/**\n * h3-web runtime engine types for the `kick/runtime` typegen augmentation —\n * mirrors {@link import('./h3').H3RuntimeTypes} for the v1 runtime.\n */\nexport interface H3WebRuntimeTypes {\n request: Request\n response: Response\n app: unknown\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":"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"}
|
|
1
|
+
{"version":3,"file":"h3-web.mjs","names":[],"sources":["../../../src/http/runtimes/h3-web.ts"],"sourcesContent":["// The h3 v2 (web-standards) runtime — `@forinda/kickjs/h3-web`.\n//\n// ADDITIVE runtime: the h3 v1 runtime (`./h3.ts`, `@forinda/kickjs/h3`)\n// stays untouched so existing adopters keep working (locked decision,\n// web-standards-edge-design.md §3.1). This runtime targets h3 >= 2 — the\n// web-standard rebase where `event.req` is a WHATWG Request, handlers\n// return values/Responses, and `app.fetch(request)` is the universal entry.\n//\n// The request pipeline runs through the shared web driver pair\n// (`../web/driver`, `../web/handler`) — the same code path the\n// `@forinda/kickjs/web` edge entry uses, so node bootstrap and edge deploys\n// exercise identical request semantics.\n//\n// Known limitation vs the v1 runtime: no Vite dev-server fall-through —\n// unmatched requests are answered by the kick notFound handler (h3 v2 owns\n// the full request; there is no node `next()` bail-out mid-engine). Use the\n// v1 h3 runtime or Express for Vite-integrated dev setups.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { createRequire } from 'node:module'\n\nimport type {\n ConnectMiddleware,\n HttpRuntime,\n RouteTable,\n RuntimeAppOptions,\n UseConnectOptions,\n} from '../runtime'\nimport { compileWebRoute, type WebRouteHooks } from '../web/handler'\nimport { WebRequestShim, WebResponseDriver } from '../web/driver'\nimport { createLogger, describeError } from '../../core/logger'\n\nconst log = createLogger('H3WebRuntime')\n\nconst peerRequire = createRequire(import.meta.url)\n\nconst NOTFOUND_MW = Symbol('kickjs.h3web.notFoundMw')\nconst ERROR_MW = Symbol('kickjs.h3web.errorMw')\nconst ASSEMBLED = Symbol('kickjs.h3web.assembled')\nconst NOOP_NEXT = (): void => {}\n\n// Minimal structural surface of h3 v2 we depend on.\ninterface H3AppLike {\n on(method: string, path: string, handler: (event: H3EventLike) => unknown): unknown\n all(path: string, handler: (event: H3EventLike) => unknown): unknown\n use(...args: unknown[]): unknown\n fetch(request: Request): Promise<Response>\n}\ninterface H3EventLike {\n req: Request & { runtime?: { node?: { req: IncomingMessage; res: ServerResponse } } }\n url: URL\n context: { params?: Record<string, string> }\n}\n\ninterface H3v2Module {\n H3: new (config?: unknown) => H3AppLike\n toNodeHandler: (app: H3AppLike) => (req: IncomingMessage, res: ServerResponse) => void\n fromNodeHandler: (mw: unknown) => unknown\n}\n\nexport interface H3WebRuntimeOptions {\n /**\n * Pre-imported h3 v2 module. Two audiences:\n * - Edge/worker bundlers with no `createRequire` at runtime:\n * `h3WebRuntime({ h3: await import('h3') })`\n * - Tests running both h3 majors side by side via an npm alias.\n * Omitted → the peer is loaded via `createRequire` (node bootstrap path).\n */\n h3?: unknown\n}\n\n/** Resolve h3 (injected or peer-required) and fail fast unless it is v2. */\nfunction loadH3v2(injected?: unknown): H3v2Module {\n const mod = injected ?? peerRequire('h3')\n const m = mod as { __esModule?: boolean; default?: Record<string, unknown> } & Record<\n string,\n unknown\n >\n const resolved = (m && m.__esModule && m.default\n ? { ...m.default, ...m }\n : m) as unknown as H3v2Module\n if (typeof resolved.H3 !== 'function') {\n throw new Error(\n \"@forinda/kickjs: h3WebRuntime() requires h3 v2 (the web-standards line, npm dist-tag 'latest'), \" +\n 'but the installed h3 has no H3 class — that is the v1 line. ' +\n \"Either `pnpm add h3@latest` for this runtime, or keep h3 v1 and use h3Runtime() from '@forinda/kickjs/h3'.\",\n )\n }\n return resolved\n}\n\n/**\n * Bridge a connect-style terminal middleware (kick's notFound / error\n * handlers) onto the web driver pair, returning the produced Response.\n */\nasync function runConnectTerminal(\n mw: ConnectMiddleware | undefined,\n event: H3EventLike,\n err?: unknown,\n): Promise<Response> {\n const req = new WebRequestShim(event.req, event.url)\n const driver = new WebResponseDriver(event.req.signal)\n if (mw) {\n if (err !== undefined) {\n ;(mw as (e: unknown, rq: unknown, rs: unknown, n: () => void) => void)(\n err,\n req,\n driver,\n NOOP_NEXT,\n )\n } else {\n ;(mw as (rq: unknown, rs: unknown, n: () => void) => void)(req, driver, NOOP_NEXT)\n }\n }\n if (!driver.settled) {\n if (err !== undefined) {\n // The configured error handler ran but didn't settle the response\n // (or there wasn't one). Emitting a bare 500 with no log entry\n // makes this failure invisible on both sides — log it here, since\n // by definition nothing downstream will.\n log.error(err, `h3-web: error handler did not settle the response — ${describeError(err)}`)\n driver.status(500).json({\n error: 'Internal Server Error',\n ...(typeof process !== 'undefined' && process.env?.NODE_ENV === 'production'\n ? {}\n : { message: describeError(err) }),\n })\n } else {\n driver.status(404).json({ error: 'Not Found' })\n }\n }\n return driver.ready\n}\n\n/**\n * The h3 v2 HTTP runtime. Pass to `bootstrap({ runtime: h3WebRuntime() })`.\n * Requires the h3 v2 peer; fails fast with guidance when v1 is installed.\n */\nexport function h3WebRuntime(options: H3WebRuntimeOptions = {}): HttpRuntime<H3AppLike> {\n const { H3, toNodeHandler, fromNodeHandler } = loadH3v2(options.h3)\n\n return {\n name: 'h3-web',\n\n createApp(_options: RuntimeAppOptions = {}): H3AppLike {\n const holder: { app?: H3AppLike } = {}\n const app = new H3({\n // Engine-level errors (middleware throws outside the kick pipeline)\n // dispatch to the kick error handler once setErrorHandler stashes it.\n onError: (error: unknown, event: H3EventLike) => {\n const mw = (holder.app as unknown as Record<symbol, ConnectMiddleware | undefined>)?.[\n ERROR_MW\n ]\n return runConnectTerminal(mw, event, error)\n },\n })\n holder.app = app\n return app\n },\n\n nodeHandler(app) {\n const state = app as unknown as Record<symbol, unknown>\n if (!state[ASSEMBLED]) {\n state[ASSEMBLED] = true\n // Terminal catch-all AFTER all mounted routes: kick's own notFound\n // handler answers unmatched paths (h3 v2's default 404 never fires).\n app.all('/**', (event: H3EventLike) => {\n const mw = state[NOTFOUND_MW] as ConnectMiddleware | undefined\n return runConnectTerminal(mw, event)\n })\n }\n const listener = toNodeHandler(app)\n return (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => {\n try {\n const result = listener(req, res) as unknown\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n void (result as Promise<unknown>).catch((err) => next?.(err))\n }\n } catch (err) {\n next?.(err)\n }\n }\n },\n\n mountRoutes(app, table: RouteTable) {\n for (const { mountPath, routes } of table) {\n for (const entry of routes) {\n const url = joinPath(mountPath, entry.path)\n const hooks: WebRouteHooks = {\n onError: async (err, req, driver) => {\n const mw = (app as unknown as Record<symbol, ConnectMiddleware | undefined>)[ERROR_MW]\n if (!mw) return\n ;(mw as (e: unknown, rq: unknown, rs: unknown, n: () => void) => void)(\n err,\n req,\n driver,\n NOOP_NEXT,\n )\n },\n }\n const run = compileWebRoute(entry, hooks)\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 },\n\n useConnect(app, mw: ConnectMiddleware, opts?: UseConnectOptions) {\n // Node bootstrap path only — h3 v2 bridges node middleware via\n // fromNodeHandler (the edge entry never calls useConnect).\n const handler = fromNodeHandler(mw)\n if (opts?.path !== undefined) {\n // v2 `use` is exact-match; `/**` covers the subtree like v1's prefix.\n app.use(joinPath(String(opts.path), '/**'), handler)\n } else {\n app.use(handler)\n }\n },\n\n serveStatic(app, prefix, dir) {\n // serve-static bridged like any other connect middleware (node only).\n const serveStatic = peerRequire('serve-static')\n this.useConnect(app, serveStatic(dir) as ConnectMiddleware, { path: prefix })\n },\n\n setNotFound(app, mw: ConnectMiddleware) {\n ;(app as unknown as Record<symbol, ConnectMiddleware>)[NOTFOUND_MW] = mw\n },\n\n setErrorHandler(app, mw: ConnectMiddleware) {\n ;(app as unknown as Record<symbol, ConnectMiddleware>)[ERROR_MW] = mw\n },\n\n capabilities: {\n render: false,\n uploads: true,\n connectMiddleware: true,\n nativeBodyParsing: true,\n },\n }\n}\n\n/**\n * h3-web runtime engine types for the `kick/runtime` typegen augmentation —\n * mirrors {@link import('./h3').H3RuntimeTypes} for the v1 runtime.\n */\nexport interface H3WebRuntimeTypes {\n request: Request\n response: Response\n app: unknown\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":";;;;;;;;;;6MAgCA,MAAM,IAAM,aAAa,cAAc,EAEjC,YAAc,cAAc,YAAY,GAAG,EAE3C,YAAc,OAAO,yBAAyB,EAC9C,SAAW,OAAO,sBAAsB,EACxC,UAAY,OAAO,wBAAwB,EAC3C,cAAwB,CAAC,EAiC/B,SAAS,SAAS,SAAgC,CAEhD,IAAM,EADM,UAAY,YAAY,IAAI,EAKlC,SAAY,GAAK,EAAE,YAAc,EAAE,QACrC,CAAE,GAAG,EAAE,QAAS,GAAG,CAAE,EACrB,EACJ,GAAI,OAAO,SAAS,IAAO,WACzB,MAAU,MACR,wQAGF,EAEF,OAAO,QACT,CAMA,eAAe,mBACb,GACA,MACA,IACmB,CACnB,IAAM,IAAM,IAAI,eAAe,MAAM,IAAK,MAAM,GAAG,EAC7C,OAAS,IAAI,kBAAkB,MAAM,IAAI,MAAM,EA8BrD,OA7BI,KACE,MAAQ,IAAA,GAQT,GAA0D,IAAK,OAAQ,SAAS,EAPhF,GACC,IACA,IACA,OACA,SACF,GAKC,OAAO,UACN,MAAQ,IAAA,GAaV,OAAO,OAAO,GAAG,CAAC,CAAC,KAAK,CAAE,MAAO,WAAY,CAAC,GAR9C,IAAI,MAAM,IAAK,uDAAuD,cAAc,GAAG,GAAG,EAC1F,OAAO,OAAO,GAAG,CAAC,CAAC,KAAK,CACtB,MAAO,wBACP,GAAI,OAAO,QAAY,KAAe,QAAQ,KAAK,WAAa,aAC5D,CAAC,EACD,CAAE,QAAS,cAAc,GAAG,CAAE,CACpC,CAAC,IAKE,OAAO,KAChB,CAMA,SAAgB,aAAa,QAA+B,CAAC,EAA2B,CACtF,GAAM,CAAE,GAAI,cAAe,iBAAoB,SAAS,QAAQ,EAAE,EAElE,MAAO,CACL,KAAM,SAEN,UAAU,SAA8B,CAAC,EAAc,CACrD,IAAM,OAA8B,CAAC,EAC/B,IAAM,IAAI,GAAG,CAGjB,SAAU,MAAgB,QAAuB,CAC/C,IAAM,GAAM,OAAO,MACjB,UAEF,OAAO,mBAAmB,GAAI,MAAO,KAAK,CAC5C,CACF,CAAC,EAED,MADA,QAAO,IAAM,IACN,GACT,EAEA,YAAY,IAAK,CACf,IAAM,MAAQ,IACT,MAAM,aACT,MAAM,WAAa,GAGnB,IAAI,IAAI,MAAQ,OAAuB,CACrC,IAAM,GAAK,MAAM,aACjB,OAAO,mBAAmB,GAAI,KAAK,CACrC,CAAC,GAEH,IAAM,SAAW,cAAc,GAAG,EAClC,OAAQ,IAAsB,IAAqB,OAAmC,CACpF,GAAI,CACF,IAAM,OAAS,SAAS,IAAK,GAAG,EAC5B,QAAU,OAAQ,OAA4B,MAAS,YACzD,OAAkC,MAAO,KAAQ,OAAO,GAAG,CAAC,CAEhE,OAAS,IAAK,CACZ,OAAO,GAAG,CACZ,CACF,CACF,EAEA,YAAY,IAAK,MAAmB,CAClC,IAAK,GAAM,CAAE,UAAW,UAAY,MAClC,IAAK,IAAM,SAAS,OAAQ,CAC1B,IAAM,IAAM,SAAS,UAAW,MAAM,IAAI,EAapC,IAAM,gBAAgB,MAAO,CAXjC,QAAS,MAAO,IAAK,IAAK,SAAW,CACnC,IAAM,GAAM,IAAiE,UACxE,IACJ,GACC,IACA,IACA,OACA,SACF,CACF,CAEqC,CAAC,EACxC,IAAI,GAAG,MAAM,OAAQ,IAAM,OACzB,IAAI,CACF,QAAS,MAAM,IACf,IAAK,MAAM,IACX,OAAQ,MAAM,QAAQ,QAAU,CAAC,CACnC,CAAC,CACH,CACF,CAEJ,EAEA,WAAW,IAAK,GAAuB,KAA0B,CAG/D,IAAM,QAAU,gBAAgB,EAAE,EAC9B,MAAM,OAAS,IAAA,GAIjB,IAAI,IAAI,OAAO,EAFf,IAAI,IAAI,SAAS,OAAO,KAAK,IAAI,EAAG,KAAK,EAAG,OAAO,CAIvD,EAEA,YAAY,IAAK,OAAQ,IAAK,CAE5B,IAAM,YAAc,YAAY,cAAc,EAC9C,KAAK,WAAW,IAAK,YAAY,GAAG,EAAwB,CAAE,KAAM,MAAO,CAAC,CAC9E,EAEA,YAAY,IAAK,GAAuB,CACrC,IAAsD,aAAe,EACxE,EAEA,gBAAgB,IAAK,GAAuB,CACzC,IAAsD,UAAY,EACrE,EAEA,aAAc,CACZ,OAAQ,GACR,QAAS,GACT,kBAAmB,GACnB,kBAAmB,EACrB,CACF,CACF,CAaA,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"}
|
|
@@ -10,7 +10,7 @@ interface H3AppLike {
|
|
|
10
10
|
* h3 parses bodies itself (`readBody`), so the Application skips its default
|
|
11
11
|
* `express.json()` (see `nativeBodyParsing`).
|
|
12
12
|
*/
|
|
13
|
-
declare function h3Runtime(): HttpRuntime<H3AppLike>;
|
|
13
|
+
export declare function h3Runtime(): HttpRuntime<H3AppLike>;
|
|
14
14
|
/**
|
|
15
15
|
* The h3 runtime's engine-native types — what the runtime-typed escape hatches
|
|
16
16
|
* resolve to once the `kick/runtime` typegen emits a `KickRuntimeRegister`
|
|
@@ -24,11 +24,10 @@ type H3AppType = typeof import('h3') extends {
|
|
|
24
24
|
type H3EventType = typeof import('h3') extends {
|
|
25
25
|
H3Event: new (...args: never[]) => infer E;
|
|
26
26
|
} ? E : unknown;
|
|
27
|
-
interface H3RuntimeTypes {
|
|
27
|
+
export interface H3RuntimeTypes {
|
|
28
28
|
request: H3EventType;
|
|
29
29
|
response: H3EventType;
|
|
30
30
|
app: H3AppType;
|
|
31
31
|
}
|
|
32
32
|
//#endregion
|
|
33
|
-
export { H3RuntimeTypes, h3Runtime };
|
|
34
33
|
//# sourceMappingURL=h3.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"h3.d.mts","names":[],"sources":["../../../src/http/runtimes/h3.ts"],"mappings":";;;UA+CU;EACR,IAAI,eAAe;EACnB;;;;;;;
|
|
1
|
+
{"version":3,"file":"h3.d.mts","names":[],"sources":["../../../src/http/runtimes/h3.ts"],"mappings":";;;UA+CU;EACR,IAAI,eAAe;EACnB;;;;;;;wBAoQc,aAAa,YAAY;;;;;;;;KAyIpC;EAA0C,WAAW;IAAM;KAC3D;EAA4C,iBAAiB,wBAAwB;IACtF;iBAGa;EACf,SAAS;EACT,UAAU;EACV,KAAK"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v8.3.
|
|
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{HttpException}from"../../core/errors.mjs";import{a as createRequestStore,f as unsupportedMediaTypeError,n as applyHandlerResult,o as disposeRequestStore,t as publishMatchedRoute,u as classifyMediaType}from"../../runtime-
|
|
11
|
+
import{HttpException}from"../../core/errors.mjs";import{a as createRequestStore,f as unsupportedMediaTypeError,n as applyHandlerResult,o as disposeRequestStore,t as publishMatchedRoute,u as classifyMediaType}from"../../runtime-Dr3WVroF.mjs";import{i as requestStore,t as RequestContext}from"../../context-DVzzDJqS.mjs";import{validate}from"../middleware/validate.mjs";import{t as applyUploadConfig}from"../../upload-config-oWkCq0nK.mjs";import"../middleware/upload.mjs";import{createRequire}from"node:module";const peerRequire=createRequire(import.meta.url),h3=()=>peerRequire(`h3`),NEXT=Symbol(`kickjs.h3.next`),ERROR_MW=Symbol(`kickjs.h3.errorMw`),ROUTER=Symbol(`kickjs.h3.router`),NOTFOUND_MW=Symbol(`kickjs.h3.notFoundMw`),ASSEMBLED=Symbol(`kickjs.h3.assembled`),NOOP_NEXT=()=>{},BODY_METHODS=new Set([`POST`,`PUT`,`PATCH`,`DELETE`]);var NodeResDriver=class{res;constructor(res){this.res=res}status(code){return this.res.statusCode=code,this}json(data){return!this.res.headersSent&&!this.res.getHeader(`content-type`)&&this.res.setHeader(`content-type`,`application/json; charset=utf-8`),this.res.end(JSON.stringify(data)),this}send(data){return this.res.end(data),this}type(contentType){return this.res.setHeader(`content-type`,contentType),this}setHeader(name,value){return this.res.setHeader(name,value),this}render(){throw Error(`ctx.render() is not supported on the h3 runtime (no view engine).`)}writeHead(statusCode,headers){return this.res.writeHead(statusCode,headers),this}flushHeaders(){return this.res.flushHeaders(),this}write(chunk){return this.res.write(chunk)}end(data){return this.res.end(data),this}once(event,listener){return this.res.once(event,listener),this}get headersSent(){return this.res.headersSent}};function resDriver(res){return new NodeResDriver(res)}const TRUST_PROXY=Symbol(`kickjs.h3.trustProxy`);function makeEventHandler(entry,trustProxy){let{getRouterParams,getQuery,readRawBody,readMultipartFormData}=h3(),upload=entry.meta.upload,validator=entry.meta.validation?validate(entry.meta.validation):void 0;return async event=>{let req=event.node.req,res=event.node.res;if(trustProxy&&!req.ip){let forwarded=req.headers[`x-forwarded-for`],first=(Array.isArray(forwarded)?forwarded[0]:forwarded)?.split(`,`)[0]?.trim();first&&(req.ip=first)}if(req.params=getRouterParams(event)??{},req.query=getQuery(event)??{},upload&&upload.mode!==`none`){let form=await readMultipartFormData(event).catch(()=>void 0),rawParts=[],fields={};for(let part of form??[])part.filename?rawParts.push({fieldname:part.name??``,filename:part.filename,mimetype:part.type??`application/octet-stream`,buffer:part.data}):part.name&&(fields[part.name]=part.data.toString(`utf-8`));let{file,files}=applyUploadConfig(rawParts,upload);req.file=file,req.files=files,req.body=fields}else if(BODY_METHODS.has(req.method??`GET`)){let{"content-length":length,"transfer-encoding":encoding}=req.headers;if(encoding!==void 0||length!==void 0&&length!==`0`){let kind=classifyMediaType(req.headers[`content-type`]);if(kind!==`multipart`){let raw=await readRawBody(event,`utf8`);if(raw){if(kind===`unsupported`)throw unsupportedMediaTypeError(req.headers[`content-type`]);if(kind===`json`)try{req.body=JSON.parse(raw)}catch(err){throw HttpException.badRequest(err instanceof Error?err.message:`Request body could not be parsed`)}else req.body=kind===`urlencoded`?Object.fromEntries(new URLSearchParams(raw)):raw}}}}let headerId=req.headers[`x-request-id`],requestId=(Array.isArray(headerId)?headerId[0]:headerId)||req.requestId,store=createRequestStore(requestId);req.requestId=store.requestId,res.once(`close`,()=>disposeRequestStore(store)),await requestStore.run(store,async()=>{publishMatchedRoute(req,entry);let ctx=new RequestContext(req,res,NOOP_NEXT,resDriver(res));validator&&await new Promise((resolve,reject)=>{validator(req,void 0,err=>err?reject(err):resolve())});for(let mw of entry.middlewares){let advanced=!1;if(await new Promise((resolve,reject)=>{Promise.resolve(mw(ctx,err=>{advanced=!0,err?reject(err):resolve()})).catch(reject)}),!advanced||res.writableEnded)return}if(entry.contributorRunner&&await entry.contributorRunner(ctx),res.writableEnded)return;let result=await entry.handler(ctx);!res.writableEnded&&!res.headersSent&&applyHandlerResult(ctx,result)})}}function unwrapH3Error(error){if(!(error instanceof Error))return error;let{cause}=error;return cause instanceof Error?cause:error}function h3Runtime(){return{name:`h3`,createApp(options={}){let{createApp}=h3(),app=createApp({onError:(error,event)=>{let res=event.node.res;if(res.writableEnded)return;let state=app;if(error?.statusCode===404){let next=event.node.req[NEXT];if(next){next();return}let nf=state[NOTFOUND_MW];if(nf){nf(event.node.req,resDriver(res),NOOP_NEXT);return}}let mw=state[ERROR_MW];mw&&mw(unwrapH3Error(error),event.node.req,resDriver(res),NOOP_NEXT)}});return app[TRUST_PROXY]=options.trustProxy,app},nodeHandler(app){let{toNodeListener}=h3(),state=app;state[ASSEMBLED]||(state[ASSEMBLED]=!0,state[ROUTER]&&app.use(state[ROUTER]));let listener=toNodeListener(app);return(req,res,next)=>{next&&(req[NEXT]=next),Promise.resolve(listener(req,res)).catch(err=>{next&&next(err)})}},mountRoutes(app,table){let{createRouter,eventHandler}=h3(),trustProxy=!!app[TRUST_PROXY],state=app;state[ROUTER]||(state[ROUTER]=createRouter());let router=state[ROUTER];for(let{mountPath,routes}of table)for(let entry of routes){let url=joinPath(mountPath,entry.path);router[entry.method.toLowerCase()](url,eventHandler(makeEventHandler(entry,trustProxy)))}},useConnect(app,mw,opts){let{fromNodeMiddleware}=h3(),handler=fromNodeMiddleware(mw);opts?.path===void 0?app.use(handler):app.use(opts.path,handler)},serveStatic(app,prefix,dir){let serveStatic=peerRequire(`serve-static`);this.useConnect(app,serveStatic(dir),{path:prefix})},setNotFound(app,mw){app[NOTFOUND_MW]=mw},setErrorHandler(app,mw){app[ERROR_MW]=mw},capabilities:{render:!1,uploads:!0,connectMiddleware:!0,nativeBodyParsing:!0}}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{h3Runtime};
|
|
12
12
|
//# sourceMappingURL=h3.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"h3.mjs","names":[],"sources":["../../../src/http/runtimes/h3.ts"],"sourcesContent":["// The h3 runtime — `@forinda/kickjs/h3`.\n//\n// h3 (unjs) is the HTTP layer behind Nitro / Nuxt. This binds it to the same\n// HttpRuntime contract as Express and Fastify: routes become native h3 router\n// routes, and the node `ServerResponse` (h3 exposes it as `event.node.res`) is\n// wrapped in a RuntimeResponse so `ctx.json` / `ctx.html` / `ctx.sse` work\n// unchanged.\n//\n// Targets h3 **v1** (stable, node-based: `createApp` / `createRouter` /\n// `toNodeListener`, with `event.node.req` / `event.node.res`). `h3` is an\n// optional peer pinned to `^1`.\n//\n// h3 v2 (https://h3.dev/migration) moves to a web-standard `Request`/`Response`\n// core. Adopting it is the spec's §8 \"web-standard driver\" — `RuntimeResponse`\n// was shaped so a future `webRuntime()` can implement it over WHATWG streams,\n// at which point h3 v2 (and edge/Bun/Deno) become a driver swap rather than a\n// rewrite. Until that lands, this binding uses the node-compatible v1 surface.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { createRequire } from 'node:module'\n\nimport { classifyMediaType, unsupportedMediaTypeError } from '../body-policy'\nimport { RequestContext } from '../context'\nimport { publishMatchedRoute } from '../runtime'\nimport { applyHandlerResult } from '../reply'\nimport { requestStore } from '../request-store'\nimport { createRequestStore, disposeRequestStore } from '../middleware/request-scope'\nimport { validate } from '../middleware/validate'\nimport { HttpException } from '../../core/errors'\nimport { applyUploadConfig, type RawUploadPart } from '../middleware/upload'\nimport type {\n ConnectMiddleware,\n HttpRuntime,\n RouteEntry,\n RouteTable,\n RuntimeAppOptions,\n RuntimeResponse,\n UseConnectOptions,\n} from '../runtime'\n\nconst peerRequire = createRequire(import.meta.url)\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst h3 = (): any => peerRequire('h3')\n\ninterface H3EventLike {\n node: { req: IncomingMessage; res: ServerResponse }\n}\ninterface H3AppLike {\n use(arg1: unknown, arg2?: unknown): unknown\n handler: unknown\n}\n\nconst NEXT = Symbol('kickjs.h3.next')\nconst ERROR_MW = Symbol('kickjs.h3.errorMw')\n// h3's `createRouter` is terminal — on no match it THROWS a 404, it does not\n// fall through to the next `app.use` layer like an Express Router. So every\n// route source (controllers, /health, devtools /_debug, ad-hoc adapter routes)\n// must live on ONE shared router, registered AFTER all connect middleware.\nconst ROUTER = Symbol('kickjs.h3.router')\nconst NOTFOUND_MW = Symbol('kickjs.h3.notFoundMw')\nconst ASSEMBLED = Symbol('kickjs.h3.assembled')\nconst NOOP_NEXT = (): void => {}\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\n/**\n * Wrap a node ServerResponse (from `event.node.res`) as a RuntimeResponse.\n * A class so the ~12 driver methods live on a shared prototype — the previous\n * object-literal version re-allocated every method closure on each request.\n */\nclass NodeResDriver implements RuntimeResponse {\n constructor(private readonly res: ServerResponse) {}\n status(code: number): this {\n this.res.statusCode = code\n return this\n }\n json(data: unknown): this {\n // Do not clobber a content-type the caller already chose. This used to set\n // `application/json` unconditionally, so every `application/problem+json`\n // response — RFC 9457 problem details, which the error handler sets the\n // header for before serialising — went out as plain JSON on this runtime\n // alone. Express and Fastify both leave an existing value alone.\n if (!this.res.headersSent && !this.res.getHeader('content-type')) {\n this.res.setHeader('content-type', 'application/json; charset=utf-8')\n }\n this.res.end(JSON.stringify(data))\n return this\n }\n send(data: unknown): this {\n this.res.end(data as never)\n return this\n }\n type(contentType: string): this {\n this.res.setHeader('content-type', contentType)\n return this\n }\n setHeader(name: string, value: unknown): this {\n this.res.setHeader(name, value as never)\n return this\n }\n render(): never {\n throw new Error('ctx.render() is not supported on the h3 runtime (no view engine).')\n }\n writeHead(statusCode: number, headers?: Record<string, string | number | string[]>): this {\n this.res.writeHead(statusCode, headers)\n return this\n }\n flushHeaders(): this {\n this.res.flushHeaders()\n return this\n }\n write(chunk: unknown): boolean {\n return this.res.write(chunk as never)\n }\n end(data?: unknown): this {\n this.res.end(data as never)\n return this\n }\n once(event: string, listener: (...args: unknown[]) => void): this {\n this.res.once(event, listener)\n return this\n }\n get headersSent(): boolean {\n return this.res.headersSent\n }\n}\n\nfunction resDriver(res: ServerResponse): RuntimeResponse {\n return new NodeResDriver(res) as unknown as RuntimeResponse\n}\n\n/** Carries `trustProxy` from createApp() to the per-route handlers. */\nconst TRUST_PROXY = Symbol('kickjs.h3.trustProxy')\n\n/** Build the h3 event handler that runs the kickjs pipeline for one route. */\nfunction makeEventHandler(\n entry: RouteEntry,\n trustProxy: boolean,\n): (event: H3EventLike) => Promise<void> {\n const { getRouterParams, getQuery, readRawBody, readMultipartFormData } = h3()\n const upload = entry.meta.upload\n // Validation middleware built ONCE per route (parity with Express) — calling\n // validate() per request re-allocated the closure every time.\n const validator = entry.meta.validation ? validate(entry.meta.validation) : undefined\n return async (event: H3EventLike) => {\n const req = event.node.req as IncomingMessage & {\n requestId?: string\n body?: unknown\n params?: unknown\n query?: unknown\n file?: unknown\n files?: unknown\n }\n const res = event.node.res\n\n // h3 computes no client address of its own, so `resolveClientIp` fell\n // straight through to the socket — which behind a proxy is the proxy, and\n // put every caller in one rate-limit bucket with no way to configure out\n // of it (#614). Express and Fastify derive `req.ip` from their own\n // trust-proxy settings; this is h3's equivalent, and like theirs it is\n // OFF unless the app opts in, because an unverified `x-forwarded-for` is\n // client-controllable.\n if (trustProxy && !(req as { ip?: string }).ip) {\n const forwarded = req.headers['x-forwarded-for']\n const first = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(',')[0]?.trim()\n if (first) (req as { ip?: string }).ip = first\n }\n\n // Populate the express-shaped request fields h3 keeps elsewhere.\n req.params = getRouterParams(event) ?? {}\n req.query = getQuery(event) ?? {}\n if (upload && upload.mode !== 'none') {\n // @FileUpload: read the multipart body once (readBody would consume the\n // same stream, so the two are mutually exclusive). File parts → the\n // multer-shaped ctx.file/ctx.files; non-file fields → req.body.\n const form = (await readMultipartFormData(event).catch(() => undefined)) as\n | Array<{ name?: string; filename?: string; type?: string; data: Buffer }>\n | undefined\n const rawParts: RawUploadPart[] = []\n const fields: Record<string, unknown> = {}\n for (const part of form ?? []) {\n if (part.filename) {\n rawParts.push({\n fieldname: part.name ?? '',\n filename: part.filename,\n mimetype: part.type ?? 'application/octet-stream',\n buffer: part.data,\n })\n } else if (part.name) {\n fields[part.name] = part.data.toString('utf-8')\n }\n }\n const { file, files } = applyUploadConfig(rawParts, upload)\n req.file = file\n req.files = files\n req.body = fields\n } else if (BODY_METHODS.has(req.method ?? 'GET')) {\n // Swallow ONLY the absent-body case. `.catch(() => undefined)` also\n // swallowed a malformed one, so h3 answered 200 with the handler running\n // on `undefined` where Express and Fastify both answer 400 — a client\n // sending broken JSON got a success response, and the handler executed\n // against data that was never valid.\n const { 'content-length': length, 'transfer-encoding': encoding } = req.headers\n const sentBody = encoding !== undefined || (length !== undefined && length !== '0')\n if (sentBody) {\n // Read raw and apply the shared policy rather than calling `readBody`,\n // whose own content-type dispatch is the source of the divergence:\n // it matches `application/json` with `===` (so `; charset=utf-8` misses)\n // and routes everything unrecognised through `destr`, which returns the\n // RAW STRING for malformed input instead of throwing (#590).\n const kind = classifyMediaType(req.headers['content-type'])\n if (kind !== 'multipart') {\n const raw = await readRawBody(event, 'utf8')\n // Reject only once the body is known to be non-empty. `sentBody` is\n // true for an empty CHUNKED post — `transfer-encoding` is present\n // with no payload — and rejecting on that alone would 415 a request\n // that sent nothing, contradicting the rule the other runtimes apply.\n if (raw) {\n if (kind === 'unsupported') {\n // A body was sent in a format the framework does not parse. Say\n // so rather than handing the handler `undefined` and letting it\n // fail somewhere less obvious.\n throw unsupportedMediaTypeError(req.headers['content-type'])\n }\n if (kind === 'json') {\n try {\n req.body = JSON.parse(raw)\n } catch (err) {\n throw HttpException.badRequest(\n err instanceof Error ? err.message : 'Request body could not be parsed',\n )\n }\n } else if (kind === 'urlencoded') {\n req.body = Object.fromEntries(new URLSearchParams(raw))\n } else {\n req.body = raw\n }\n }\n }\n }\n }\n\n const headerId = req.headers['x-request-id']\n const requestId = (Array.isArray(headerId) ? headerId[0] : headerId) || req.requestId\n const store = createRequestStore(requestId)\n req.requestId = store.requestId\n\n // @PreDestroy teardown for REQUEST-scoped instances once the response\n // closes (finished or aborted) — parity with requestScopeMiddleware.\n res.once('close', () => disposeRequestStore(store))\n\n await requestStore.run(store, async () => {\n publishMatchedRoute(req, entry)\n const ctx = new RequestContext(req as never, res as never, NOOP_NEXT, resDriver(res))\n\n if (validator) {\n await new Promise<void>((resolve, reject) => {\n validator(req as never, undefined as never, (err?: unknown) =>\n err ? reject(err) : resolve(),\n )\n })\n }\n\n for (const mw of entry.middlewares) {\n let advanced = false\n await new Promise<void>((resolve, reject) => {\n const next = (err?: unknown): void => {\n advanced = true\n if (err) reject(err)\n else resolve()\n }\n Promise.resolve(mw(ctx, next)).catch(reject)\n })\n if (!advanced || res.writableEnded) return\n }\n\n if (entry.contributorRunner) await entry.contributorRunner(ctx)\n if (res.writableEnded) return\n const result = await entry.handler(ctx)\n // Return-value handlers (reply.ts): auto-send when nothing was written.\n if (!res.writableEnded && !res.headersSent) applyHandlerResult(ctx, result)\n })\n }\n}\n\n/**\n * The original error h3 wrapped.\n *\n * h3 routes every thrown value through `createError()`, which returns its own\n * error with the original on `cause`. Handing that wrapper to the shared error\n * middleware means it sees something Express and Fastify never give it, and the\n * responses diverge: `instanceof HttpException` fails, so a thrown\n * `HttpException(418)` falls through to the generic branch and picks up a\n * `requestId` the other two omit, and `describeError` walks the cause chain to\n * report `kaboom ← caused by kaboom`.\n *\n * Only an `Error` cause is unwrapped. h3's own 404 carries no cause, so the\n * not-found path above is unaffected.\n */\nfunction unwrapH3Error(error: unknown): unknown {\n if (!(error instanceof Error)) return error\n const { cause } = error as { cause?: unknown }\n return cause instanceof Error ? cause : error\n}\n\n/**\n * The h3 HTTP runtime. Pass to `bootstrap({ runtime: h3Runtime() })`.\n * h3 parses bodies itself (`readBody`), so the Application skips its default\n * `express.json()` (see `nativeBodyParsing`).\n */\nexport function h3Runtime(): HttpRuntime<H3AppLike> {\n return {\n name: 'h3',\n\n createApp(options: RuntimeAppOptions = {}): H3AppLike {\n const { createApp } = h3()\n const app = createApp({\n onError: (error: unknown, event: H3EventLike) => {\n const res = event.node.res\n if (res.writableEnded) return\n const state = app as {\n [ERROR_MW]?: ConnectMiddleware\n [NOTFOUND_MW]?: ConnectMiddleware\n }\n // The shared router throws a 404 on no match (it's terminal). Treat\n // that as \"not found\", not a server error: hand back to the outer\n // chain (Vite dev fall-through) when present, else run the framework's\n // notFound handler so it returns a proper 404 — only genuine handler\n // errors reach the error middleware (and the ErrorHandler log).\n const status = (error as { statusCode?: number } | undefined)?.statusCode\n if (status === 404) {\n const next = (event.node.req as IncomingMessage & { [NEXT]?: () => void })[NEXT]\n if (next) {\n next()\n return\n }\n const nf = state[NOTFOUND_MW]\n if (nf) {\n ;(nf as (req: unknown, res: unknown, next: () => void) => void)(\n event.node.req,\n resDriver(res),\n NOOP_NEXT,\n )\n return\n }\n }\n const mw = state[ERROR_MW]\n if (mw) {\n ;(mw as (e: unknown, req: unknown, res: unknown, next: () => void) => void)(\n unwrapH3Error(error),\n event.node.req,\n resDriver(res),\n NOOP_NEXT,\n )\n }\n },\n })\n // Stash the setting for mountRoutes — h3 has no app-level config object\n // of its own to hang it on.\n ;(app as { [TRUST_PROXY]?: unknown })[TRUST_PROXY] = options.trustProxy\n return app as H3AppLike\n },\n\n nodeHandler(app) {\n const { toNodeListener } = h3()\n // Register the shared router exactly once, AFTER all connect middleware\n // (serveStatic / useConnect) the setup phase added via `app.use` — the\n // router is terminal, so anything that must run for non-route paths has to\n // sit before it in the stack.\n const state = app as { [ROUTER]?: unknown; [ASSEMBLED]?: boolean }\n if (!state[ASSEMBLED]) {\n state[ASSEMBLED] = true\n if (state[ROUTER]) app.use(state[ROUTER])\n }\n const listener = toNodeListener(app)\n return (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => {\n if (next) (req as IncomingMessage & { [NEXT]?: (err?: unknown) => void })[NEXT] = next\n Promise.resolve(listener(req, res)).catch((err) => {\n if (next) next(err)\n })\n }\n },\n\n mountRoutes(app, table: RouteTable) {\n const { createRouter, eventHandler } = h3()\n const trustProxy = Boolean((app as { [TRUST_PROXY]?: unknown })[TRUST_PROXY])\n // One shared router per app — every mountRoutes call (controllers, health,\n // devtools, ad-hoc adapter routes) adds to it. `app.use(router)` is\n // deferred to nodeHandler so the router lands after connect middleware.\n const state = app as { [ROUTER]?: ReturnType<typeof createRouter> }\n if (!state[ROUTER]) state[ROUTER] = createRouter()\n const router = state[ROUTER]\n for (const { mountPath, routes } of table) {\n for (const entry of routes) {\n const url = joinPath(mountPath, entry.path)\n const method = entry.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch'\n router[method](url, eventHandler(makeEventHandler(entry, trustProxy)))\n }\n }\n },\n\n useConnect(app, mw: ConnectMiddleware, opts?: UseConnectOptions) {\n const { fromNodeMiddleware } = h3()\n const handler = fromNodeMiddleware(mw)\n if (opts?.path !== undefined) app.use(opts.path, handler)\n else app.use(handler)\n },\n\n serveStatic(app, prefix, dir) {\n // `serve-static` (the standalone connect middleware behind express.static)\n // so the h3 runtime carries no `express` dependency — bridged via\n // fromNodeMiddleware like any other connect middleware.\n const serveStatic = peerRequire('serve-static')\n this.useConnect(app, serveStatic(dir) as ConnectMiddleware, { path: prefix })\n },\n\n setNotFound(app, mw: ConnectMiddleware) {\n // Stash it — the shared router throws a 404 on no match, which `onError`\n // (createApp) dispatches to this handler (or to the Vite fall-through when\n // a `next` is present). A post-router `app.use` could never run, since the\n // terminal router throws before the stack advances.\n ;(app as { [NOTFOUND_MW]?: ConnectMiddleware })[NOTFOUND_MW] = mw\n },\n\n setErrorHandler(app, mw: ConnectMiddleware) {\n ;(app as { [ERROR_MW]?: ConnectMiddleware })[ERROR_MW] = mw\n },\n\n capabilities: {\n render: false,\n uploads: true,\n connectMiddleware: true,\n nativeBodyParsing: true,\n },\n }\n}\n\n/**\n * The h3 runtime's engine-native types — what the runtime-typed escape hatches\n * resolve to once the `kick/runtime` typegen emits a `KickRuntimeRegister`\n * augmentation pointing here (spec §4.3b). `AdapterContext.app` / `getRuntimeApp()`\n * become the h3 `App`; `ctx.req` / `ctx.res` the per-request `H3Event` (h3 v1\n * routes everything through the event). Mirrors `ExpressRuntimeTypes`.\n */\n// Resolved conditionally so this file typechecks against EITHER installed h3\n// major: v1 exports `App`; v2 (used by the sibling h3-web runtime) does not.\n// Runtime behavior is untouched — this block is type-only.\ntype H3AppType = typeof import('h3') extends { App: infer A } ? A : unknown\ntype H3EventType = typeof import('h3') extends { H3Event: new (...args: never[]) => infer E }\n ? E\n : unknown\n\nexport interface H3RuntimeTypes {\n request: H3EventType\n response: H3EventType\n app: H3AppType\n}\n\n/** Join a mount prefix and a route path; a root path maps to the prefix itself. */\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":"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"}
|
|
1
|
+
{"version":3,"file":"h3.mjs","names":[],"sources":["../../../src/http/runtimes/h3.ts"],"sourcesContent":["// The h3 runtime — `@forinda/kickjs/h3`.\n//\n// h3 (unjs) is the HTTP layer behind Nitro / Nuxt. This binds it to the same\n// HttpRuntime contract as Express and Fastify: routes become native h3 router\n// routes, and the node `ServerResponse` (h3 exposes it as `event.node.res`) is\n// wrapped in a RuntimeResponse so `ctx.json` / `ctx.html` / `ctx.sse` work\n// unchanged.\n//\n// Targets h3 **v1** (stable, node-based: `createApp` / `createRouter` /\n// `toNodeListener`, with `event.node.req` / `event.node.res`). `h3` is an\n// optional peer pinned to `^1`.\n//\n// h3 v2 (https://h3.dev/migration) moves to a web-standard `Request`/`Response`\n// core. Adopting it is the spec's §8 \"web-standard driver\" — `RuntimeResponse`\n// was shaped so a future `webRuntime()` can implement it over WHATWG streams,\n// at which point h3 v2 (and edge/Bun/Deno) become a driver swap rather than a\n// rewrite. Until that lands, this binding uses the node-compatible v1 surface.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { createRequire } from 'node:module'\n\nimport { classifyMediaType, unsupportedMediaTypeError } from '../body-policy'\nimport { RequestContext } from '../context'\nimport { publishMatchedRoute } from '../runtime'\nimport { applyHandlerResult } from '../reply'\nimport { requestStore } from '../request-store'\nimport { createRequestStore, disposeRequestStore } from '../middleware/request-scope'\nimport { validate } from '../middleware/validate'\nimport { HttpException } from '../../core/errors'\nimport { applyUploadConfig, type RawUploadPart } from '../middleware/upload'\nimport type {\n ConnectMiddleware,\n HttpRuntime,\n RouteEntry,\n RouteTable,\n RuntimeAppOptions,\n RuntimeResponse,\n UseConnectOptions,\n} from '../runtime'\n\nconst peerRequire = createRequire(import.meta.url)\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst h3 = (): any => peerRequire('h3')\n\ninterface H3EventLike {\n node: { req: IncomingMessage; res: ServerResponse }\n}\ninterface H3AppLike {\n use(arg1: unknown, arg2?: unknown): unknown\n handler: unknown\n}\n\nconst NEXT = Symbol('kickjs.h3.next')\nconst ERROR_MW = Symbol('kickjs.h3.errorMw')\n// h3's `createRouter` is terminal — on no match it THROWS a 404, it does not\n// fall through to the next `app.use` layer like an Express Router. So every\n// route source (controllers, /health, devtools /_debug, ad-hoc adapter routes)\n// must live on ONE shared router, registered AFTER all connect middleware.\nconst ROUTER = Symbol('kickjs.h3.router')\nconst NOTFOUND_MW = Symbol('kickjs.h3.notFoundMw')\nconst ASSEMBLED = Symbol('kickjs.h3.assembled')\nconst NOOP_NEXT = (): void => {}\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\n/**\n * Wrap a node ServerResponse (from `event.node.res`) as a RuntimeResponse.\n * A class so the ~12 driver methods live on a shared prototype — the previous\n * object-literal version re-allocated every method closure on each request.\n */\nclass NodeResDriver implements RuntimeResponse {\n constructor(private readonly res: ServerResponse) {}\n status(code: number): this {\n this.res.statusCode = code\n return this\n }\n json(data: unknown): this {\n // Do not clobber a content-type the caller already chose. This used to set\n // `application/json` unconditionally, so every `application/problem+json`\n // response — RFC 9457 problem details, which the error handler sets the\n // header for before serialising — went out as plain JSON on this runtime\n // alone. Express and Fastify both leave an existing value alone.\n if (!this.res.headersSent && !this.res.getHeader('content-type')) {\n this.res.setHeader('content-type', 'application/json; charset=utf-8')\n }\n this.res.end(JSON.stringify(data))\n return this\n }\n send(data: unknown): this {\n this.res.end(data as never)\n return this\n }\n type(contentType: string): this {\n this.res.setHeader('content-type', contentType)\n return this\n }\n setHeader(name: string, value: unknown): this {\n this.res.setHeader(name, value as never)\n return this\n }\n render(): never {\n throw new Error('ctx.render() is not supported on the h3 runtime (no view engine).')\n }\n writeHead(statusCode: number, headers?: Record<string, string | number | string[]>): this {\n this.res.writeHead(statusCode, headers)\n return this\n }\n flushHeaders(): this {\n this.res.flushHeaders()\n return this\n }\n write(chunk: unknown): boolean {\n return this.res.write(chunk as never)\n }\n end(data?: unknown): this {\n this.res.end(data as never)\n return this\n }\n once(event: string, listener: (...args: unknown[]) => void): this {\n this.res.once(event, listener)\n return this\n }\n get headersSent(): boolean {\n return this.res.headersSent\n }\n}\n\nfunction resDriver(res: ServerResponse): RuntimeResponse {\n return new NodeResDriver(res) as unknown as RuntimeResponse\n}\n\n/** Carries `trustProxy` from createApp() to the per-route handlers. */\nconst TRUST_PROXY = Symbol('kickjs.h3.trustProxy')\n\n/** Build the h3 event handler that runs the kickjs pipeline for one route. */\nfunction makeEventHandler(\n entry: RouteEntry,\n trustProxy: boolean,\n): (event: H3EventLike) => Promise<void> {\n const { getRouterParams, getQuery, readRawBody, readMultipartFormData } = h3()\n const upload = entry.meta.upload\n // Validation middleware built ONCE per route (parity with Express) — calling\n // validate() per request re-allocated the closure every time.\n const validator = entry.meta.validation ? validate(entry.meta.validation) : undefined\n return async (event: H3EventLike) => {\n const req = event.node.req as IncomingMessage & {\n requestId?: string\n body?: unknown\n params?: unknown\n query?: unknown\n file?: unknown\n files?: unknown\n }\n const res = event.node.res\n\n // h3 computes no client address of its own, so `resolveClientIp` fell\n // straight through to the socket — which behind a proxy is the proxy, and\n // put every caller in one rate-limit bucket with no way to configure out\n // of it (#614). Express and Fastify derive `req.ip` from their own\n // trust-proxy settings; this is h3's equivalent, and like theirs it is\n // OFF unless the app opts in, because an unverified `x-forwarded-for` is\n // client-controllable.\n if (trustProxy && !(req as { ip?: string }).ip) {\n const forwarded = req.headers['x-forwarded-for']\n const first = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(',')[0]?.trim()\n if (first) (req as { ip?: string }).ip = first\n }\n\n // Populate the express-shaped request fields h3 keeps elsewhere.\n req.params = getRouterParams(event) ?? {}\n req.query = getQuery(event) ?? {}\n if (upload && upload.mode !== 'none') {\n // @FileUpload: read the multipart body once (readBody would consume the\n // same stream, so the two are mutually exclusive). File parts → the\n // multer-shaped ctx.file/ctx.files; non-file fields → req.body.\n const form = (await readMultipartFormData(event).catch(() => undefined)) as\n | Array<{ name?: string; filename?: string; type?: string; data: Buffer }>\n | undefined\n const rawParts: RawUploadPart[] = []\n const fields: Record<string, unknown> = {}\n for (const part of form ?? []) {\n if (part.filename) {\n rawParts.push({\n fieldname: part.name ?? '',\n filename: part.filename,\n mimetype: part.type ?? 'application/octet-stream',\n buffer: part.data,\n })\n } else if (part.name) {\n fields[part.name] = part.data.toString('utf-8')\n }\n }\n const { file, files } = applyUploadConfig(rawParts, upload)\n req.file = file\n req.files = files\n req.body = fields\n } else if (BODY_METHODS.has(req.method ?? 'GET')) {\n // Swallow ONLY the absent-body case. `.catch(() => undefined)` also\n // swallowed a malformed one, so h3 answered 200 with the handler running\n // on `undefined` where Express and Fastify both answer 400 — a client\n // sending broken JSON got a success response, and the handler executed\n // against data that was never valid.\n const { 'content-length': length, 'transfer-encoding': encoding } = req.headers\n const sentBody = encoding !== undefined || (length !== undefined && length !== '0')\n if (sentBody) {\n // Read raw and apply the shared policy rather than calling `readBody`,\n // whose own content-type dispatch is the source of the divergence:\n // it matches `application/json` with `===` (so `; charset=utf-8` misses)\n // and routes everything unrecognised through `destr`, which returns the\n // RAW STRING for malformed input instead of throwing (#590).\n const kind = classifyMediaType(req.headers['content-type'])\n if (kind !== 'multipart') {\n const raw = await readRawBody(event, 'utf8')\n // Reject only once the body is known to be non-empty. `sentBody` is\n // true for an empty CHUNKED post — `transfer-encoding` is present\n // with no payload — and rejecting on that alone would 415 a request\n // that sent nothing, contradicting the rule the other runtimes apply.\n if (raw) {\n if (kind === 'unsupported') {\n // A body was sent in a format the framework does not parse. Say\n // so rather than handing the handler `undefined` and letting it\n // fail somewhere less obvious.\n throw unsupportedMediaTypeError(req.headers['content-type'])\n }\n if (kind === 'json') {\n try {\n req.body = JSON.parse(raw)\n } catch (err) {\n throw HttpException.badRequest(\n err instanceof Error ? err.message : 'Request body could not be parsed',\n )\n }\n } else if (kind === 'urlencoded') {\n req.body = Object.fromEntries(new URLSearchParams(raw))\n } else {\n req.body = raw\n }\n }\n }\n }\n }\n\n const headerId = req.headers['x-request-id']\n const requestId = (Array.isArray(headerId) ? headerId[0] : headerId) || req.requestId\n const store = createRequestStore(requestId)\n req.requestId = store.requestId\n\n // @PreDestroy teardown for REQUEST-scoped instances once the response\n // closes (finished or aborted) — parity with requestScopeMiddleware.\n res.once('close', () => disposeRequestStore(store))\n\n await requestStore.run(store, async () => {\n publishMatchedRoute(req, entry)\n const ctx = new RequestContext(req as never, res as never, NOOP_NEXT, resDriver(res))\n\n if (validator) {\n await new Promise<void>((resolve, reject) => {\n validator(req as never, undefined as never, (err?: unknown) =>\n err ? reject(err) : resolve(),\n )\n })\n }\n\n for (const mw of entry.middlewares) {\n let advanced = false\n await new Promise<void>((resolve, reject) => {\n const next = (err?: unknown): void => {\n advanced = true\n if (err) reject(err)\n else resolve()\n }\n Promise.resolve(mw(ctx, next)).catch(reject)\n })\n if (!advanced || res.writableEnded) return\n }\n\n if (entry.contributorRunner) await entry.contributorRunner(ctx)\n if (res.writableEnded) return\n const result = await entry.handler(ctx)\n // Return-value handlers (reply.ts): auto-send when nothing was written.\n if (!res.writableEnded && !res.headersSent) applyHandlerResult(ctx, result)\n })\n }\n}\n\n/**\n * The original error h3 wrapped.\n *\n * h3 routes every thrown value through `createError()`, which returns its own\n * error with the original on `cause`. Handing that wrapper to the shared error\n * middleware means it sees something Express and Fastify never give it, and the\n * responses diverge: `instanceof HttpException` fails, so a thrown\n * `HttpException(418)` falls through to the generic branch and picks up a\n * `requestId` the other two omit, and `describeError` walks the cause chain to\n * report `kaboom ← caused by kaboom`.\n *\n * Only an `Error` cause is unwrapped. h3's own 404 carries no cause, so the\n * not-found path above is unaffected.\n */\nfunction unwrapH3Error(error: unknown): unknown {\n if (!(error instanceof Error)) return error\n const { cause } = error as { cause?: unknown }\n return cause instanceof Error ? cause : error\n}\n\n/**\n * The h3 HTTP runtime. Pass to `bootstrap({ runtime: h3Runtime() })`.\n * h3 parses bodies itself (`readBody`), so the Application skips its default\n * `express.json()` (see `nativeBodyParsing`).\n */\nexport function h3Runtime(): HttpRuntime<H3AppLike> {\n return {\n name: 'h3',\n\n createApp(options: RuntimeAppOptions = {}): H3AppLike {\n const { createApp } = h3()\n const app = createApp({\n onError: (error: unknown, event: H3EventLike) => {\n const res = event.node.res\n if (res.writableEnded) return\n const state = app as {\n [ERROR_MW]?: ConnectMiddleware\n [NOTFOUND_MW]?: ConnectMiddleware\n }\n // The shared router throws a 404 on no match (it's terminal). Treat\n // that as \"not found\", not a server error: hand back to the outer\n // chain (Vite dev fall-through) when present, else run the framework's\n // notFound handler so it returns a proper 404 — only genuine handler\n // errors reach the error middleware (and the ErrorHandler log).\n const status = (error as { statusCode?: number } | undefined)?.statusCode\n if (status === 404) {\n const next = (event.node.req as IncomingMessage & { [NEXT]?: () => void })[NEXT]\n if (next) {\n next()\n return\n }\n const nf = state[NOTFOUND_MW]\n if (nf) {\n ;(nf as (req: unknown, res: unknown, next: () => void) => void)(\n event.node.req,\n resDriver(res),\n NOOP_NEXT,\n )\n return\n }\n }\n const mw = state[ERROR_MW]\n if (mw) {\n ;(mw as (e: unknown, req: unknown, res: unknown, next: () => void) => void)(\n unwrapH3Error(error),\n event.node.req,\n resDriver(res),\n NOOP_NEXT,\n )\n }\n },\n })\n // Stash the setting for mountRoutes — h3 has no app-level config object\n // of its own to hang it on.\n ;(app as { [TRUST_PROXY]?: unknown })[TRUST_PROXY] = options.trustProxy\n return app as H3AppLike\n },\n\n nodeHandler(app) {\n const { toNodeListener } = h3()\n // Register the shared router exactly once, AFTER all connect middleware\n // (serveStatic / useConnect) the setup phase added via `app.use` — the\n // router is terminal, so anything that must run for non-route paths has to\n // sit before it in the stack.\n const state = app as { [ROUTER]?: unknown; [ASSEMBLED]?: boolean }\n if (!state[ASSEMBLED]) {\n state[ASSEMBLED] = true\n if (state[ROUTER]) app.use(state[ROUTER])\n }\n const listener = toNodeListener(app)\n return (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => {\n if (next) (req as IncomingMessage & { [NEXT]?: (err?: unknown) => void })[NEXT] = next\n Promise.resolve(listener(req, res)).catch((err) => {\n if (next) next(err)\n })\n }\n },\n\n mountRoutes(app, table: RouteTable) {\n const { createRouter, eventHandler } = h3()\n const trustProxy = Boolean((app as { [TRUST_PROXY]?: unknown })[TRUST_PROXY])\n // One shared router per app — every mountRoutes call (controllers, health,\n // devtools, ad-hoc adapter routes) adds to it. `app.use(router)` is\n // deferred to nodeHandler so the router lands after connect middleware.\n const state = app as { [ROUTER]?: ReturnType<typeof createRouter> }\n if (!state[ROUTER]) state[ROUTER] = createRouter()\n const router = state[ROUTER]\n for (const { mountPath, routes } of table) {\n for (const entry of routes) {\n const url = joinPath(mountPath, entry.path)\n const method = entry.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch'\n router[method](url, eventHandler(makeEventHandler(entry, trustProxy)))\n }\n }\n },\n\n useConnect(app, mw: ConnectMiddleware, opts?: UseConnectOptions) {\n const { fromNodeMiddleware } = h3()\n const handler = fromNodeMiddleware(mw)\n if (opts?.path !== undefined) app.use(opts.path, handler)\n else app.use(handler)\n },\n\n serveStatic(app, prefix, dir) {\n // `serve-static` (the standalone connect middleware behind express.static)\n // so the h3 runtime carries no `express` dependency — bridged via\n // fromNodeMiddleware like any other connect middleware.\n const serveStatic = peerRequire('serve-static')\n this.useConnect(app, serveStatic(dir) as ConnectMiddleware, { path: prefix })\n },\n\n setNotFound(app, mw: ConnectMiddleware) {\n // Stash it — the shared router throws a 404 on no match, which `onError`\n // (createApp) dispatches to this handler (or to the Vite fall-through when\n // a `next` is present). A post-router `app.use` could never run, since the\n // terminal router throws before the stack advances.\n ;(app as { [NOTFOUND_MW]?: ConnectMiddleware })[NOTFOUND_MW] = mw\n },\n\n setErrorHandler(app, mw: ConnectMiddleware) {\n ;(app as { [ERROR_MW]?: ConnectMiddleware })[ERROR_MW] = mw\n },\n\n capabilities: {\n render: false,\n uploads: true,\n connectMiddleware: true,\n nativeBodyParsing: true,\n },\n }\n}\n\n/**\n * The h3 runtime's engine-native types — what the runtime-typed escape hatches\n * resolve to once the `kick/runtime` typegen emits a `KickRuntimeRegister`\n * augmentation pointing here (spec §4.3b). `AdapterContext.app` / `getRuntimeApp()`\n * become the h3 `App`; `ctx.req` / `ctx.res` the per-request `H3Event` (h3 v1\n * routes everything through the event). Mirrors `ExpressRuntimeTypes`.\n */\n// Resolved conditionally so this file typechecks against EITHER installed h3\n// major: v1 exports `App`; v2 (used by the sibling h3-web runtime) does not.\n// Runtime behavior is untouched — this block is type-only.\ntype H3AppType = typeof import('h3') extends { App: infer A } ? A : unknown\ntype H3EventType = typeof import('h3') extends { H3Event: new (...args: never[]) => infer E }\n ? E\n : unknown\n\nexport interface H3RuntimeTypes {\n request: H3EventType\n response: H3EventType\n app: H3AppType\n}\n\n/** Join a mount prefix and a route path; a root path maps to the prefix itself. */\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":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v8.3.
|
|
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{a as loadEnvFromSchema,i as loadEnv,n as defineEnv,o as reloadEnv,r as getEnv,s as resetEnvCache,t as baseEnvSchema}from"./env-DmEVaWGi.mjs";import{METADATA,Scope}from"./core/interfaces.mjs";import{a as isInjectionToken,c as KickError,i as createToken,l as formatKickError,n as tokenName,r as INJECTION_TOKEN,t as Container}from"./container-CzBE79TS.mjs";import{ConsoleLoggerProvider,Logger,createLogger,describeError}from"./core/logger.mjs";import{a as getMethodMeta,c as pushClassMeta,d as setInMetaMap,f as setInMetaRecord,i as getMetaRecord,l as pushMethodMeta,n as getClassMetaOrUndefined,o as getMethodMetaOrUndefined,p as setMethodMeta,r as getMetaMap,s as hasClassMeta,t as getClassMeta,u as setClassMeta}from"./metadata-CugcKCXW.mjs";import{ApiQueryParams,Asset,Autowired,Builder,Component,Controller,Delete,FileUpload,Get,Inject,Injectable,Middleware,Patch,Post,PostConstruct,PreDestroy,Put,Repository,Service,Value,normalizeApiQueryParamsConfig,withBuilder}from"./core/decorators.mjs";import{a as HEALTH_PROBE,c as DuplicateMountNameError,d as mountSort,f as defineModule,i as materializeRouter,l as MissingMountDepError,n as buildRoutes,o as HealthController,r as expressRuntime,s as healthModule,t as Application,u as MountCycleError}from"./application-DT1nNdC7.mjs";import{t as MutableModuleRegistry}from"./module-registry-D6lAL81K.mjs";import{CRON_META,Cron,getCronJobs}from"./core/cron.mjs";import{CacheEvict,Cacheable,MemoryCacheProvider,getCacheProvider,setCacheProvider}from"./core/cache.mjs";import{r as registerDisposable,t as disposeAll}from"./disposables-DsrxA4Xv.mjs";import{HttpException,HttpStatus,ProblemException,Problems,defaultProblemTitle,normalizeProblem}from"./core/errors.mjs";import{buildMountPath,joinPaths,normalizePath}from"./core/path.mjs";import{computed,isRef,reactive,ref,toRefs,unref,watch}from"./core/reactivity.mjs";import{l as resolveRouteFlags,o as getRouteFlags,r as defineRouteFlag,s as matchesFlagTest}from"./route-flag-BKYcu3L0.mjs";import{a as clearAssetCache,c as groupAssetKeys,i as assets,l as stripExt,n as ASSET_MANIFEST_VERSION,o as resolveAsset,r as UnknownAssetError,s as useAssets,t as ASSETS,u as defineContextDecorator}from"./core-vktCOX0c.mjs";import{i as MissingContributorError,n as DuplicateContributorError,r as MissingContextValueError,t as ContributorCycleError}from"./context-errors-CH2YMGwk.mjs";import{i as buildPipeline,r as buildRouteTable}from"./router-builder-B5Vbzba3.mjs";import{t as runContributors}from"./contributor-runner-Cf6M_ILY.mjs";import{t as defineAdapter}from"./define-adapter-BBA4UbXZ.mjs";import{n as bindRoutePolicy,r as offerRoutePolicy,t as RoutePolicyTable}from"./route-policy-YrOBnVpp.mjs";import{c as requestScopeMiddleware,i as reply,n as applyHandlerResult,r as isReply}from"./runtime-DhY6i4TY.mjs";import{REQUEST_ID_HEADER,requestId}from"./http/middleware/request-id.mjs";import{errorHandler,notFoundHandler}from"./http/middleware/error-handler.mjs";import{i as requestStore,n as getRequestStore,r as getRequestValue,t as RequestContext}from"./context-CCo_tD5m.mjs";import{a as parseQuery,c as resetQueryParsingDefaults,i as parsePagination,l as setQueryParsingDefaults,n as getQueryParsingDefaults,o as parseSearchQuery,r as parseFilters,s as parseSort,t as buildQueryParams,u as FILTER_OPERATORS}from"./query-Dc58mMd1.mjs";import{validate}from"./http/middleware/validate.mjs";import{r as resolveMimeTypes}from"./upload-config-BYINSxAp.mjs";import{buildUploadMiddleware,cleanupFiles,upload}from"./http/middleware/upload.mjs";import{n as isClusterPrimary,t as bootstrap}from"./bootstrap-C6-CrVgx.mjs";import{n as csrfGuard,r as randomHex,t as csrf}from"./csrf-DiOCjUPJ.mjs";import{requestLogger}from"./http/middleware/request-logger.mjs";import{helmet}from"./http/middleware/helmet.mjs";import{cors}from"./http/middleware/cors.mjs";import{rateLimit}from"./http/middleware/rate-limit.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-BQ5jox2K.mjs";import{session}from"./http/middleware/session.mjs";import{ViewAdapter}from"./http/middleware/views.mjs";import{SpaAdapter}from"./http/middleware/spa.mjs";import{n as createConfigService,t as ConfigService}from"./config-Bbqd77CD.mjs";var ModuleList=class extends Array{mount(module){return this.push(module),this}};function defineModules(...initial){let list=new ModuleList;for(let entry of initial)list.push(entry);return list}var CircuitOpenError=class extends Error{breakerName;constructor(breakerName){super(`Circuit breaker "${breakerName}" is open — request rejected`),this.breakerName=breakerName,this.name=`CircuitOpenError`}},CircuitBreaker=class{name;state=`closed`;failures=0;successes=0;halfOpenAttempts=0;lastFailure;openedAt;failureThreshold;resetTimeout;halfOpenMax;constructor(name,options){this.name=name,this.failureThreshold=options.failureThreshold,this.resetTimeout=options.resetTimeout,this.halfOpenMax=options.halfOpenMax??1}async execute(fn){if(this.maybeTransitionToHalfOpen(),this.state===`open`||this.state===`half_open`&&this.halfOpenAttempts>=this.halfOpenMax)throw new CircuitOpenError(this.name);this.state===`half_open`&&this.halfOpenAttempts++;try{let result=await fn();return this.onSuccess(),result}catch(error){throw this.onFailure(),error}}getState(){return this.maybeTransitionToHalfOpen(),this.state}getStats(){return this.maybeTransitionToHalfOpen(),{failures:this.failures,successes:this.successes,state:this.state,...this.lastFailure?{lastFailure:this.lastFailure}:{}}}reset(){this.state=`closed`,this.failures=0,this.successes=0,this.halfOpenAttempts=0,this.lastFailure=void 0,this.openedAt=void 0}onSuccess(){this.state===`half_open`&&(this.state=`closed`,this.failures=0,this.halfOpenAttempts=0,this.openedAt=void 0),this.successes++}onFailure(){this.failures++,this.lastFailure=new Date,this.state===`half_open`?(this.state=`open`,this.openedAt=Date.now(),this.halfOpenAttempts=0):this.state===`closed`&&this.failures>=this.failureThreshold&&(this.state=`open`,this.openedAt=Date.now())}maybeTransitionToHalfOpen(){this.state===`open`&&this.openedAt!=null&&Date.now()-this.openedAt>=this.resetTimeout&&(this.state=`half_open`,this.halfOpenAttempts=0)}};const mergeConfig=(defaults={},overrides={})=>({...defaults,...overrides}),composeName=(base,scope)=>`${base}:${scope}`;function definePlugin(options){let buildSync=(instanceName,scoped,overrides)=>{let config=mergeConfig(options.defaults,overrides),built=options.build(config,{name:instanceName,scoped});return built.name=instanceName,built},buildAsync=(instanceName,opts)=>{let inner,ensureInner=async container=>{if(inner)return inner;let deps=(opts.inject??[]).map(tok=>container.resolve(tok)),config=await opts.useFactory(...deps);return inner=buildSync(instanceName,!1,config),inner};return{name:instanceName,async onReady(container){let built=await ensureInner(container);built.register?.(container),await built.onReady?.(container)},async shutdown(){await inner?.shutdown?.()}}},factory=(config=>buildSync(options.name,!1,config));return factory.scoped=(scopeName,config)=>buildSync(composeName(options.name,scopeName),!0,config),factory.async=opts=>buildAsync(options.name,opts),Object.defineProperty(factory,"definition",{value:Object.freeze({...options})}),factory}function defineAugmentation(name,meta){}function defineHttpContextDecoratorImpl(spec){return defineContextDecorator(spec)}const defineHttpContextDecorator=Object.freeze(Object.assign(defineHttpContextDecoratorImpl,{withParams:()=>spec=>defineHttpContextDecoratorImpl(spec)})),TRACEPARENT_HEADER=`traceparent`,TRACEPARENT_RE=/^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;function generateTraceId(){return randomHex(16)}function generateSpanId(){return randomHex(8)}function parseTraceparent(header){let match=TRACEPARENT_RE.exec(header.trim().toLowerCase());if(!match)return null;let version=match[1],traceId=match[2],parentSpanId=match[3],flags=match[4];return version===`ff`||traceId===`00000000000000000000000000000000`||parentSpanId===`0000000000000000`?null:{version,traceId,parentSpanId,flags}}function traceContext(options={}){return(req,res,next)=>{let store=requestStore.getStore(),traceparentHeader=req.headers[TRACEPARENT_HEADER],raw=Array.isArray(traceparentHeader)?traceparentHeader[0]:traceparentHeader,parsed=raw?parseTraceparent(raw):null,traceId=parsed?.traceId??generateTraceId(),spanId=generateSpanId();store&&(store.values.set(`traceId`,traceId),store.values.set(`spanId`,spanId),parsed&&(store.values.set(`parentSpanId`,parsed.parentSpanId),store.values.set(`traceFlags`,parsed.flags),store.values.set(`traceVersion`,parsed.version))),req.traceId=traceId,req.spanId=spanId,options.propagateResponse&&res.setHeader(`traceresponse`,traceId),next()}}export{ASSETS,ASSET_MANIFEST_VERSION,ApiQueryParams,Application,Asset,Autowired,Builder,CRON_META,CacheEvict,Cacheable,CircuitBreaker,CircuitOpenError,Component,ConfigService,ConsoleLoggerProvider,Container,ContributorCycleError,Controller,Cron,Delete,DuplicateContributorError,DuplicateMountNameError,FILTER_OPERATORS,FileUpload,Get,HEALTH_PROBE,HealthController,HttpException,HttpStatus,INJECTION_TOKEN,Inject,Injectable,KickError,KvRateLimitStore,KvSessionStore,Logger,METADATA,MemoryCacheProvider,Middleware,MissingContextValueError,MissingContributorError,MissingMountDepError,ModuleList,MountCycleError,MutableModuleRegistry,Patch,Post,PostConstruct,PreDestroy,ProblemException,Problems,Put,REQUEST_ID_HEADER,Repository,RequestContext,RoutePolicyTable,Scope,Service,SpaAdapter,TRACEPARENT_HEADER,UnknownAssetError,Value,ViewAdapter,applyHandlerResult,assets,baseEnvSchema,bindRoutePolicy,bootstrap,buildMountPath,buildPipeline,buildQueryParams,buildRouteTable,buildRoutes,buildUploadMiddleware,cleanupFiles,clearAssetCache,computed,cors,createConfigService,createLogger,createToken,csrf,csrfGuard,defaultProblemTitle,defineAdapter,defineAugmentation,defineContextDecorator,defineEnv,defineHttpContextDecorator,defineModule,defineModules,definePlugin,defineRouteFlag,describeError,disposeAll,errorHandler,expressRuntime,formatKickError,getCacheProvider,getClassMeta,getClassMetaOrUndefined,getCronJobs,getEnv,getMetaMap,getMetaRecord,getMethodMeta,getMethodMetaOrUndefined,getQueryParsingDefaults,getRequestStore,getRequestValue,getRouteFlags,groupAssetKeys,hasClassMeta,healthModule,helmet,isClusterPrimary,isInjectionToken,isRef,isReply,joinPaths,loadEnv,loadEnvFromSchema,matchesFlagTest,materializeRouter,mountSort,normalizeApiQueryParamsConfig,normalizePath,normalizeProblem,notFoundHandler,offerRoutePolicy,parseFilters,parsePagination,parseQuery,parseSearchQuery,parseSort,parseTraceparent,pushClassMeta,pushMethodMeta,rateLimit,rateLimitGuard,reactive,ref,registerDisposable,reloadEnv,reply,requestId,requestLogger,requestScopeMiddleware,requestStore,resetEnvCache,resetQueryParsingDefaults,resolveAsset,resolveMimeTypes,resolveRouteFlags,runContributors,session,setCacheProvider,setClassMeta,setInMetaMap,setInMetaRecord,setMethodMeta,setQueryParsingDefaults,stripExt,toRefs,tokenName,traceContext,unref,upload,useAssets,validate,watch,withBuilder};
|
|
11
|
+
import{a as loadEnvFromSchema,i as loadEnv,n as defineEnv,o as reloadEnv,r as getEnv,s as resetEnvCache,t as baseEnvSchema}from"./env-Dzp4S93f.mjs";import{METADATA,Scope}from"./core/interfaces.mjs";import{a as isInjectionToken,c as KickError,i as createToken,l as formatKickError,n as tokenName,r as INJECTION_TOKEN,t as Container}from"./container-DYDEiKNR.mjs";import{ConsoleLoggerProvider,Logger,createLogger,describeError}from"./core/logger.mjs";import{a as getMethodMeta,c as pushClassMeta,d as setInMetaMap,f as setInMetaRecord,i as getMetaRecord,l as pushMethodMeta,n as getClassMetaOrUndefined,o as getMethodMetaOrUndefined,p as setMethodMeta,r as getMetaMap,s as hasClassMeta,t as getClassMeta,u as setClassMeta}from"./metadata-BfAea0Eh.mjs";import{ApiQueryParams,Asset,Autowired,Builder,Component,Controller,Delete,FileUpload,Get,Inject,Injectable,Middleware,Patch,Post,PostConstruct,PreDestroy,Put,Repository,Service,Value,normalizeApiQueryParamsConfig,withBuilder}from"./core/decorators.mjs";import{a as HEALTH_PROBE,c as DuplicateMountNameError,d as mountSort,f as defineModule,i as materializeRouter,l as MissingMountDepError,n as buildRoutes,o as HealthController,r as expressRuntime,s as healthModule,t as Application,u as MountCycleError}from"./application-B2SLsOAo.mjs";import{t as MutableModuleRegistry}from"./module-registry-BXUTVQZX.mjs";import{CRON_META,Cron,getCronJobs}from"./core/cron.mjs";import{CacheEvict,Cacheable,MemoryCacheProvider,getCacheProvider,setCacheProvider}from"./core/cache.mjs";import{r as registerDisposable,t as disposeAll}from"./disposables-DnXJYY9g.mjs";import{HttpException,HttpStatus,ProblemException,Problems,defaultProblemTitle,normalizeProblem}from"./core/errors.mjs";import{buildMountPath,joinPaths,normalizePath}from"./core/path.mjs";import{computed,isRef,reactive,ref,toRefs,unref,watch}from"./core/reactivity.mjs";import{l as resolveRouteFlags,o as getRouteFlags,r as defineRouteFlag,s as matchesFlagTest}from"./route-flag-gN8YUbh7.mjs";import{a as clearAssetCache,c as groupAssetKeys,i as assets,l as stripExt,n as ASSET_MANIFEST_VERSION,o as resolveAsset,r as UnknownAssetError,s as useAssets,t as ASSETS,u as defineContextDecorator}from"./core-C80EsmuT.mjs";import{i as MissingContributorError,n as DuplicateContributorError,r as MissingContextValueError,t as ContributorCycleError}from"./context-errors-DBe_u92r.mjs";import{i as buildPipeline,r as buildRouteTable}from"./router-builder-mGBlnvon.mjs";import{t as runContributors}from"./contributor-runner-DiDYluYn.mjs";import{t as defineAdapter}from"./define-adapter-CBXnH8kA.mjs";import{n as bindRoutePolicy,r as offerRoutePolicy,t as RoutePolicyTable}from"./route-policy-BmkX461L.mjs";import{c as requestScopeMiddleware,i as reply,n as applyHandlerResult,r as isReply}from"./runtime-Dr3WVroF.mjs";import{REQUEST_ID_HEADER,requestId}from"./http/middleware/request-id.mjs";import{errorHandler,notFoundHandler}from"./http/middleware/error-handler.mjs";import{i as requestStore,n as getRequestStore,r as getRequestValue,t as RequestContext}from"./context-DVzzDJqS.mjs";import{a as parseQuery,c as resetQueryParsingDefaults,i as parsePagination,l as setQueryParsingDefaults,n as getQueryParsingDefaults,o as parseSearchQuery,r as parseFilters,s as parseSort,t as buildQueryParams,u as FILTER_OPERATORS}from"./query-6wpoIQxU.mjs";import{validate}from"./http/middleware/validate.mjs";import{r as resolveMimeTypes}from"./upload-config-oWkCq0nK.mjs";import{buildUploadMiddleware,cleanupFiles,upload}from"./http/middleware/upload.mjs";import{n as isClusterPrimary,t as bootstrap}from"./bootstrap-CBoWnjli.mjs";import{n as csrfGuard,r as randomHex,t as csrf}from"./csrf-Ci-NmNHy.mjs";import{requestLogger}from"./http/middleware/request-logger.mjs";import{helmet}from"./http/middleware/helmet.mjs";import{cors}from"./http/middleware/cors.mjs";import{rateLimit}from"./http/middleware/rate-limit.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-CL1FlCuc.mjs";import{session}from"./http/middleware/session.mjs";import{ViewAdapter}from"./http/middleware/views.mjs";import{SpaAdapter}from"./http/middleware/spa.mjs";import{n as createConfigService,t as ConfigService}from"./config-xYtlQp17.mjs";var ModuleList=class extends Array{mount(module){return this.push(module),this}};function defineModules(...initial){let list=new ModuleList;for(let entry of initial)list.push(entry);return list}var CircuitOpenError=class extends Error{breakerName;constructor(breakerName){super(`Circuit breaker "${breakerName}" is open — request rejected`),this.breakerName=breakerName,this.name=`CircuitOpenError`}},CircuitBreaker=class{name;state=`closed`;failures=0;successes=0;halfOpenAttempts=0;lastFailure;openedAt;failureThreshold;resetTimeout;halfOpenMax;constructor(name,options){this.name=name,this.failureThreshold=options.failureThreshold,this.resetTimeout=options.resetTimeout,this.halfOpenMax=options.halfOpenMax??1}async execute(fn){if(this.maybeTransitionToHalfOpen(),this.state===`open`||this.state===`half_open`&&this.halfOpenAttempts>=this.halfOpenMax)throw new CircuitOpenError(this.name);this.state===`half_open`&&this.halfOpenAttempts++;try{let result=await fn();return this.onSuccess(),result}catch(error){throw this.onFailure(),error}}getState(){return this.maybeTransitionToHalfOpen(),this.state}getStats(){return this.maybeTransitionToHalfOpen(),{failures:this.failures,successes:this.successes,state:this.state,...this.lastFailure?{lastFailure:this.lastFailure}:{}}}reset(){this.state=`closed`,this.failures=0,this.successes=0,this.halfOpenAttempts=0,this.lastFailure=void 0,this.openedAt=void 0}onSuccess(){this.state===`half_open`&&(this.state=`closed`,this.failures=0,this.halfOpenAttempts=0,this.openedAt=void 0),this.successes++}onFailure(){this.failures++,this.lastFailure=new Date,this.state===`half_open`?(this.state=`open`,this.openedAt=Date.now(),this.halfOpenAttempts=0):this.state===`closed`&&this.failures>=this.failureThreshold&&(this.state=`open`,this.openedAt=Date.now())}maybeTransitionToHalfOpen(){this.state===`open`&&this.openedAt!=null&&Date.now()-this.openedAt>=this.resetTimeout&&(this.state=`half_open`,this.halfOpenAttempts=0)}};const mergeConfig=(defaults={},overrides={})=>({...defaults,...overrides}),composeName=(base,scope)=>`${base}:${scope}`;function definePlugin(options){let buildSync=(instanceName,scoped,overrides)=>{let config=mergeConfig(options.defaults,overrides),built=options.build(config,{name:instanceName,scoped});return built.name=instanceName,built},buildAsync=(instanceName,opts)=>{let inner,ensureInner=async container=>{if(inner)return inner;let deps=(opts.inject??[]).map(tok=>container.resolve(tok)),config=await opts.useFactory(...deps);return inner=buildSync(instanceName,!1,config),inner};return{name:instanceName,async onReady(container){let built=await ensureInner(container);built.register?.(container),await built.onReady?.(container)},async shutdown(){await inner?.shutdown?.()}}},factory=(config=>buildSync(options.name,!1,config));return factory.scoped=(scopeName,config)=>buildSync(composeName(options.name,scopeName),!0,config),factory.async=opts=>buildAsync(options.name,opts),Object.defineProperty(factory,"definition",{value:Object.freeze({...options})}),factory}function defineAugmentation(name,meta){}function defineHttpContextDecoratorImpl(spec){return defineContextDecorator(spec)}const defineHttpContextDecorator=Object.freeze(Object.assign(defineHttpContextDecoratorImpl,{withParams:()=>spec=>defineHttpContextDecoratorImpl(spec)})),TRACEPARENT_HEADER=`traceparent`,TRACEPARENT_RE=/^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;function generateTraceId(){return randomHex(16)}function generateSpanId(){return randomHex(8)}function parseTraceparent(header){let match=TRACEPARENT_RE.exec(header.trim().toLowerCase());if(!match)return null;let version=match[1],traceId=match[2],parentSpanId=match[3],flags=match[4];return version===`ff`||traceId===`00000000000000000000000000000000`||parentSpanId===`0000000000000000`?null:{version,traceId,parentSpanId,flags}}function traceContext(options={}){return(req,res,next)=>{let store=requestStore.getStore(),traceparentHeader=req.headers[TRACEPARENT_HEADER],raw=Array.isArray(traceparentHeader)?traceparentHeader[0]:traceparentHeader,parsed=raw?parseTraceparent(raw):null,traceId=parsed?.traceId??generateTraceId(),spanId=generateSpanId();store&&(store.values.set(`traceId`,traceId),store.values.set(`spanId`,spanId),parsed&&(store.values.set(`parentSpanId`,parsed.parentSpanId),store.values.set(`traceFlags`,parsed.flags),store.values.set(`traceVersion`,parsed.version))),req.traceId=traceId,req.spanId=spanId,options.propagateResponse&&res.setHeader(`traceresponse`,traceId),next()}}export{ASSETS,ASSET_MANIFEST_VERSION,ApiQueryParams,Application,Asset,Autowired,Builder,CRON_META,CacheEvict,Cacheable,CircuitBreaker,CircuitOpenError,Component,ConfigService,ConsoleLoggerProvider,Container,ContributorCycleError,Controller,Cron,Delete,DuplicateContributorError,DuplicateMountNameError,FILTER_OPERATORS,FileUpload,Get,HEALTH_PROBE,HealthController,HttpException,HttpStatus,INJECTION_TOKEN,Inject,Injectable,KickError,KvRateLimitStore,KvSessionStore,Logger,METADATA,MemoryCacheProvider,Middleware,MissingContextValueError,MissingContributorError,MissingMountDepError,ModuleList,MountCycleError,MutableModuleRegistry,Patch,Post,PostConstruct,PreDestroy,ProblemException,Problems,Put,REQUEST_ID_HEADER,Repository,RequestContext,RoutePolicyTable,Scope,Service,SpaAdapter,TRACEPARENT_HEADER,UnknownAssetError,Value,ViewAdapter,applyHandlerResult,assets,baseEnvSchema,bindRoutePolicy,bootstrap,buildMountPath,buildPipeline,buildQueryParams,buildRouteTable,buildRoutes,buildUploadMiddleware,cleanupFiles,clearAssetCache,computed,cors,createConfigService,createLogger,createToken,csrf,csrfGuard,defaultProblemTitle,defineAdapter,defineAugmentation,defineContextDecorator,defineEnv,defineHttpContextDecorator,defineModule,defineModules,definePlugin,defineRouteFlag,describeError,disposeAll,errorHandler,expressRuntime,formatKickError,getCacheProvider,getClassMeta,getClassMetaOrUndefined,getCronJobs,getEnv,getMetaMap,getMetaRecord,getMethodMeta,getMethodMetaOrUndefined,getQueryParsingDefaults,getRequestStore,getRequestValue,getRouteFlags,groupAssetKeys,hasClassMeta,healthModule,helmet,isClusterPrimary,isInjectionToken,isRef,isReply,joinPaths,loadEnv,loadEnvFromSchema,matchesFlagTest,materializeRouter,mountSort,normalizeApiQueryParamsConfig,normalizePath,normalizeProblem,notFoundHandler,offerRoutePolicy,parseFilters,parsePagination,parseQuery,parseSearchQuery,parseSort,parseTraceparent,pushClassMeta,pushMethodMeta,rateLimit,rateLimitGuard,reactive,ref,registerDisposable,reloadEnv,reply,requestId,requestLogger,requestScopeMiddleware,requestStore,resetEnvCache,resetQueryParsingDefaults,resolveAsset,resolveMimeTypes,resolveRouteFlags,runContributors,session,setCacheProvider,setClassMeta,setInMetaMap,setInMetaRecord,setMethodMeta,setQueryParsingDefaults,stripExt,toRefs,tokenName,traceContext,unref,upload,useAssets,validate,watch,withBuilder};
|
|
12
12
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v8.3.
|
|
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{n as assertFlagTest,s as matchesFlagTest}from"./route-flag-
|
|
12
|
-
//# sourceMappingURL=kv-stores-
|
|
11
|
+
import{n as assertFlagTest,s as matchesFlagTest}from"./route-flag-gN8YUbh7.mjs";var LazyMemoryStore=class{windowMs;hits=new Map;constructor(windowMs){this.windowMs=windowMs}async increment(key){let now=Date.now(),entry=this.hits.get(key);if(entry&&entry.reset>now)return entry.hits++,{totalHits:entry.hits,resetTime:new Date(entry.reset)};if(this.hits.size>1e4)for(let[k,e]of this.hits)e.reset<=now&&this.hits.delete(k);let fresh={hits:1,reset:now+this.windowMs};return this.hits.set(key,fresh),{totalHits:1,resetTime:new Date(fresh.reset)}}async decrement(key){let entry=this.hits.get(key);entry&&entry.hits>0&&entry.hits--}async reset(key){this.hits.delete(key)}};function defaultKey(ctx){return ctx.ip??`global`}function rateLimitGuard(options={}){let max=options.max??100,windowMs=options.windowMs??6e4,message=options.message??`Too Many Requests`,statusCode=options.statusCode??429,keyGenerator=options.keyGenerator??defaultKey,sendHeaders=options.headers??!0,store=options.store??new LazyMemoryStore(windowMs),exemptWhen=options.exemptWhen;return exemptWhen!==void 0&&assertFlagTest(exemptWhen,`rateLimitGuard({ exemptWhen })`),async(ctx,next)=>{if(options.skip?.(ctx)||exemptWhen!==void 0&&matchesFlagTest(exemptWhen,ctx.route?.flags,ctx.route))return next();let{totalHits,resetTime}=await store.increment(keyGenerator(ctx)),remaining=Math.max(0,max-totalHits);if(sendHeaders&&(ctx.setHeader(`X-RateLimit-Limit`,String(max)),ctx.setHeader(`X-RateLimit-Remaining`,String(remaining)),ctx.setHeader(`X-RateLimit-Reset`,String(Math.ceil(resetTime.getTime()/1e3)))),totalHits>max){let retryAfter=Math.max(1,Math.ceil((resetTime.getTime()-Date.now())/1e3));sendHeaders&&ctx.setHeader(`Retry-After`,String(retryAfter)),ctx.json({message},statusCode);return}next()}}function kvTtlSeconds(ms){return Math.max(60,Math.ceil(ms/1e3))}var KvRateLimitStore=class{kv;prefix;windowMs;constructor(kv,options){this.kv=kv,this.windowMs=options.windowMs,this.prefix=options.prefix??`rl:`}async increment(key){let now=Date.now(),entry=await this.read(key);if(entry&&entry.reset>now)return entry.hits++,await this.write(key,entry),{totalHits:entry.hits,resetTime:new Date(entry.reset)};let fresh={hits:1,reset:now+this.windowMs};return await this.write(key,fresh),{totalHits:1,resetTime:new Date(fresh.reset)}}async decrement(key){let entry=await this.read(key);entry&&entry.hits>0&&(entry.hits--,await this.write(key,entry))}async reset(key){await this.kv.delete(this.prefix+key)}async read(key){let raw=await this.kv.get(this.prefix+key);if(raw===null)return null;try{return JSON.parse(raw)}catch{return null}}async write(key,entry){await this.kv.put(this.prefix+key,JSON.stringify(entry),{expirationTtl:kvTtlSeconds(Math.max(entry.reset-Date.now(),1e3))})}},KvSessionStore=class{kv;prefix;constructor(kv,options={}){this.kv=kv,this.prefix=options.prefix??`sess:`}async get(sid){let raw=await this.kv.get(this.prefix+sid);if(raw===null)return null;try{return JSON.parse(raw)}catch{return null}}async set(sid,data,maxAge){await this.kv.put(this.prefix+sid,JSON.stringify(data),{expirationTtl:kvTtlSeconds(maxAge)})}async destroy(sid){await this.kv.delete(this.prefix+sid)}async touch(sid,maxAge){let data=await this.get(sid);data!==null&&await this.set(sid,data,maxAge)}};export{KvSessionStore as n,rateLimitGuard as r,KvRateLimitStore as t};
|
|
12
|
+
//# sourceMappingURL=kv-stores-CL1FlCuc.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kv-stores-BQ5jox2K.mjs","names":[],"sources":["../src/http/middleware/rate-limit-guard.ts","../src/http/middleware/kv-stores.ts"],"sourcesContent":["// Ctx-style rate limiter — runs on BOTH pipelines: the node runtimes (via\n// `@Middleware()` / class middleware) and the `@forinda/kickjs/web` fetch\n// entry (via `@Middleware()` or `createWebApp({ middleware })`), unlike the\n// connect-style `rateLimit()` which is node-only. Zero runtime imports —\n// part of the edge purity graph.\nimport { assertFlagTest, matchesFlagTest, type RouteFlagTest } from '../../core/route-flag'\nimport type { MiddlewareHandler } from '../../core/decorators'\nimport type { RequestContext } from '../context'\nimport type { RateLimitStore } from './rate-limit'\n\nexport interface RateLimitGuardOptions {\n /** Maximum number of requests per window (default: 100). */\n max?: number\n /** Time window in milliseconds (default: 60_000). */\n windowMs?: number\n /** Response message when the limit is exceeded (default: 'Too Many Requests'). */\n message?: string\n /** HTTP status code when the limit is exceeded (default: 429). */\n statusCode?: number\n /**\n * Rate-limit key per request. Defaults to `ctx.ip`, falling back to\n * `'global'`. Return a user/tenant id here for authenticated quotas.\n *\n * ::: warning Behind a proxy, configure `trustProxy` or every client shares\n * one bucket\n * On a node runtime `ctx.ip` is the runtime's vetted client address —\n * Express derives it from `trust proxy`, Fastify from `trustProxy`. Without\n * that setting it is the SOCKET address, which behind a load balancer or\n * Cloudflare is the proxy's, so every caller lands in the same bucket and one\n * noisy client exhausts the allowance for everyone.\n *\n * `bootstrap({ trustProxy: true })` is the fix. Forwarded headers are\n * deliberately not consulted here: `x-forwarded-for` is client-controllable\n * unless a proxy overwrites it, so trusting it unconditionally would let a\n * direct caller mint a fresh allowance per request by varying the header —\n * evading the limit rather than enforcing it. The header chain\n * (`cf-connecting-ip` → `x-forwarded-for` → `x-real-ip`) applies only on the\n * `@forinda/kickjs/web` edge entry, where there is no socket and the platform\n * has already terminated the connection.\n * :::\n */\n keyGenerator?: (ctx: RequestContext) => string\n /** Send `X-RateLimit-*` / `Retry-After` headers (default: true). */\n headers?: boolean\n /**\n * Counter backend. Defaults to a per-isolate in-memory map — fine for a\n * single node process, useless on edge where isolates recycle: pass a\n * `KvRateLimitStore` (or Redis-backed store) there.\n */\n store?: RateLimitStore\n /** Skip limiting for a request (health checks, allowlists). */\n skip?: (ctx: RequestContext) => boolean\n /**\n * Skip limiting on routes carrying a route flag — a name, a list of names\n * (any-of), or a predicate. Declared on the route rather than restated as a\n * path here, so it cannot drift when `apiPrefix` or a module's `version`\n * changes:\n *\n * ```ts\n * const Public = defineRouteFlag('auth.public')\n * rateLimitGuard({ max: 60, exemptWhen: 'auth.public' })\n * ```\n *\n * Only meaningful where a route has been matched — as `@Middleware()` on a\n * class or method, or in `createWebApp({ middleware })`. Mounted app-wide\n * ahead of routing there is no route to read, and nothing is exempted.\n */\n exemptWhen?: RouteFlagTest\n}\n\n/** Timer-free in-memory store: sweeps expired entries when the map grows. */\nclass LazyMemoryStore implements RateLimitStore {\n private readonly hits = new Map<string, { hits: number; reset: number }>()\n\n constructor(private readonly windowMs: number) {}\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n if (entry && entry.reset > now) {\n entry.hits++\n return { totalHits: entry.hits, resetTime: new Date(entry.reset) }\n }\n // ponytail: sweep-on-growth instead of a cleanup interval — no timer to\n // dispose, edge-safe. Bound is 10k live keys before a full sweep.\n if (this.hits.size > 10_000) {\n for (const [k, e] of this.hits) if (e.reset <= now) this.hits.delete(k)\n }\n const fresh = { hits: 1, reset: now + this.windowMs }\n this.hits.set(key, fresh)\n return { totalHits: 1, resetTime: new Date(fresh.reset) }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.hits > 0) entry.hits--\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n}\n\nfunction defaultKey(ctx: RequestContext): string {\n // `ctx.ip` carries the resolution that used to live here: prefer the\n // runtime-computed address (Express derives it from `trust proxy`), fall\n // back to forwarded headers only for runtimes that compute none.\n return ctx.ip ?? 'global'\n}\n\n/**\n * Rate limiting as a `(ctx, next)` middleware.\n *\n * ```ts\n * // Edge (Cloudflare Workers) — app-wide:\n * const app = createWebApp({\n * h3, modules,\n * middlewares: [rateLimitGuard({ max: 60, windowMs: 60_000, store: new KvRateLimitStore(env.KV, { windowMs: 60_000 }) })],\n * })\n *\n * // Any runtime — per controller/route:\n * @Middleware(rateLimitGuard({ max: 10 }))\n * @Post('/login')\n * ```\n */\nexport function rateLimitGuard(options: RateLimitGuardOptions = {}): MiddlewareHandler {\n const max = options.max ?? 100\n const windowMs = options.windowMs ?? 60_000\n const message = options.message ?? 'Too Many Requests'\n const statusCode = options.statusCode ?? 429\n const keyGenerator = options.keyGenerator ?? defaultKey\n const sendHeaders = options.headers ?? true\n const store = options.store ?? new LazyMemoryStore(windowMs)\n\n const exemptWhen = options.exemptWhen\n if (exemptWhen !== undefined) assertFlagTest(exemptWhen, 'rateLimitGuard({ exemptWhen })')\n\n return async (ctx: RequestContext, next: () => void): Promise<void> => {\n if (options.skip?.(ctx)) return next()\n if (exemptWhen !== undefined && matchesFlagTest(exemptWhen, ctx.route?.flags, ctx.route)) {\n return next()\n }\n\n const { totalHits, resetTime } = await store.increment(keyGenerator(ctx))\n const remaining = Math.max(0, max - totalHits)\n\n if (sendHeaders) {\n ctx.setHeader('X-RateLimit-Limit', String(max))\n ctx.setHeader('X-RateLimit-Remaining', String(remaining))\n ctx.setHeader('X-RateLimit-Reset', String(Math.ceil(resetTime.getTime() / 1000)))\n }\n\n if (totalHits > max) {\n const retryAfter = Math.max(1, Math.ceil((resetTime.getTime() - Date.now()) / 1000))\n if (sendHeaders) ctx.setHeader('Retry-After', String(retryAfter))\n ctx.json({ message }, statusCode)\n return\n }\n\n next()\n }\n}\n","// Edge-safe store adapters over a minimal KV surface. Zero runtime imports —\n// this file must stay importable from the `@forinda/kickjs/web` purity graph.\nimport type { RateLimitStore } from './rate-limit'\nimport type { SessionData, SessionStore } from './session'\n\n/**\n * The minimal async KV surface both stores need. Structurally compatible\n * with a Cloudflare Workers `KVNamespace` binding — pass it straight in:\n *\n * ```ts\n * export default {\n * fetch(req: Request, env: Env) {\n * const store = new KvRateLimitStore(env.MY_KV, { windowMs: 60_000 })\n * // ...\n * },\n * }\n * ```\n *\n * Anything with the same shape works too (Deno KV wrapper, Upstash Redis\n * REST client adapter, a Map-backed fake in tests).\n */\nexport interface KvLike {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/** Cloudflare KV rejects TTLs under 60 seconds — clamp up, never down. */\nfunction kvTtlSeconds(ms: number): number {\n return Math.max(60, Math.ceil(ms / 1000))\n}\n\nexport interface KvRateLimitStoreOptions {\n /** Window length in ms — must match the `windowMs` given to the limiter. */\n windowMs: number\n /** Key prefix in the KV namespace (default `'rl:'`). */\n prefix?: string\n}\n\n/**\n * {@link RateLimitStore} over a {@link KvLike} namespace, for edge deployments\n * where in-memory counters reset on every isolate.\n *\n * KV is eventually consistent and this is a read-modify-write counter, so\n * limiting is **approximate** under concurrency — bursts racing across\n * isolates can each read the same count. That is the accepted trade-off for\n * abuse throttling; use a Durable Object / Redis store when you need an\n * exact ceiling (billing quotas, strict per-key locks).\n */\nexport class KvRateLimitStore implements RateLimitStore {\n private readonly prefix: string\n private readonly windowMs: number\n\n constructor(\n private readonly kv: KvLike,\n options: KvRateLimitStoreOptions,\n ) {\n this.windowMs = options.windowMs\n this.prefix = options.prefix ?? 'rl:'\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = await this.read(key)\n if (entry && entry.reset > now) {\n entry.hits++\n await this.write(key, entry)\n return { totalHits: entry.hits, resetTime: new Date(entry.reset) }\n }\n const fresh = { hits: 1, reset: now + this.windowMs }\n await this.write(key, fresh)\n return { totalHits: 1, resetTime: new Date(fresh.reset) }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = await this.read(key)\n if (entry && entry.hits > 0) {\n entry.hits--\n await this.write(key, entry)\n }\n }\n\n async reset(key: string): Promise<void> {\n await this.kv.delete(this.prefix + key)\n }\n\n private async read(key: string): Promise<{ hits: number; reset: number } | null> {\n const raw = await this.kv.get(this.prefix + key)\n if (raw === null) return null\n try {\n return JSON.parse(raw) as { hits: number; reset: number }\n } catch {\n return null\n }\n }\n\n private async write(key: string, entry: { hits: number; reset: number }): Promise<void> {\n await this.kv.put(this.prefix + key, JSON.stringify(entry), {\n expirationTtl: kvTtlSeconds(Math.max(entry.reset - Date.now(), 1000)),\n })\n }\n}\n\nexport interface KvSessionStoreOptions {\n /** Key prefix in the KV namespace (default `'sess:'`). */\n prefix?: string\n}\n\n/**\n * {@link SessionStore} over a {@link KvLike} namespace. Plug into the node\n * `session()` middleware today (e.g. backed by Upstash/Cloudflare KV via\n * REST) so sessions survive restarts and horizontal scaling; TTL expiry is\n * delegated to the KV layer via `expirationTtl`.\n *\n * Cost note: `touch()` is a get + put round-trip (KvLike has no TTL-only\n * refresh), so `session({ rolling: true })` doubles KV operations per\n * request — per-operation-billed platforms may prefer `rolling: false`.\n */\nexport class KvSessionStore implements SessionStore {\n private readonly prefix: string\n\n constructor(\n private readonly kv: KvLike,\n options: KvSessionStoreOptions = {},\n ) {\n this.prefix = options.prefix ?? 'sess:'\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const raw = await this.kv.get(this.prefix + sid)\n if (raw === null) return null\n try {\n return JSON.parse(raw) as SessionData\n } catch {\n return null\n }\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n await this.kv.put(this.prefix + sid, JSON.stringify(data), {\n expirationTtl: kvTtlSeconds(maxAge),\n })\n }\n\n async destroy(sid: string): Promise<void> {\n await this.kv.delete(this.prefix + sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const data = await this.get(sid)\n if (data !== null) await this.set(sid, data, maxAge)\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"kv-stores-CL1FlCuc.mjs","names":[],"sources":["../src/http/middleware/rate-limit-guard.ts","../src/http/middleware/kv-stores.ts"],"sourcesContent":["// Ctx-style rate limiter — runs on BOTH pipelines: the node runtimes (via\n// `@Middleware()` / class middleware) and the `@forinda/kickjs/web` fetch\n// entry (via `@Middleware()` or `createWebApp({ middleware })`), unlike the\n// connect-style `rateLimit()` which is node-only. Zero runtime imports —\n// part of the edge purity graph.\nimport { assertFlagTest, matchesFlagTest, type RouteFlagTest } from '../../core/route-flag'\nimport type { MiddlewareHandler } from '../../core/decorators'\nimport type { RequestContext } from '../context'\nimport type { RateLimitStore } from './rate-limit'\n\nexport interface RateLimitGuardOptions {\n /** Maximum number of requests per window (default: 100). */\n max?: number\n /** Time window in milliseconds (default: 60_000). */\n windowMs?: number\n /** Response message when the limit is exceeded (default: 'Too Many Requests'). */\n message?: string\n /** HTTP status code when the limit is exceeded (default: 429). */\n statusCode?: number\n /**\n * Rate-limit key per request. Defaults to `ctx.ip`, falling back to\n * `'global'`. Return a user/tenant id here for authenticated quotas.\n *\n * ::: warning Behind a proxy, configure `trustProxy` or every client shares\n * one bucket\n * On a node runtime `ctx.ip` is the runtime's vetted client address —\n * Express derives it from `trust proxy`, Fastify from `trustProxy`. Without\n * that setting it is the SOCKET address, which behind a load balancer or\n * Cloudflare is the proxy's, so every caller lands in the same bucket and one\n * noisy client exhausts the allowance for everyone.\n *\n * `bootstrap({ trustProxy: true })` is the fix. Forwarded headers are\n * deliberately not consulted here: `x-forwarded-for` is client-controllable\n * unless a proxy overwrites it, so trusting it unconditionally would let a\n * direct caller mint a fresh allowance per request by varying the header —\n * evading the limit rather than enforcing it. The header chain\n * (`cf-connecting-ip` → `x-forwarded-for` → `x-real-ip`) applies only on the\n * `@forinda/kickjs/web` edge entry, where there is no socket and the platform\n * has already terminated the connection.\n * :::\n */\n keyGenerator?: (ctx: RequestContext) => string\n /** Send `X-RateLimit-*` / `Retry-After` headers (default: true). */\n headers?: boolean\n /**\n * Counter backend. Defaults to a per-isolate in-memory map — fine for a\n * single node process, useless on edge where isolates recycle: pass a\n * `KvRateLimitStore` (or Redis-backed store) there.\n */\n store?: RateLimitStore\n /** Skip limiting for a request (health checks, allowlists). */\n skip?: (ctx: RequestContext) => boolean\n /**\n * Skip limiting on routes carrying a route flag — a name, a list of names\n * (any-of), or a predicate. Declared on the route rather than restated as a\n * path here, so it cannot drift when `apiPrefix` or a module's `version`\n * changes:\n *\n * ```ts\n * const Public = defineRouteFlag('auth.public')\n * rateLimitGuard({ max: 60, exemptWhen: 'auth.public' })\n * ```\n *\n * Only meaningful where a route has been matched — as `@Middleware()` on a\n * class or method, or in `createWebApp({ middleware })`. Mounted app-wide\n * ahead of routing there is no route to read, and nothing is exempted.\n */\n exemptWhen?: RouteFlagTest\n}\n\n/** Timer-free in-memory store: sweeps expired entries when the map grows. */\nclass LazyMemoryStore implements RateLimitStore {\n private readonly hits = new Map<string, { hits: number; reset: number }>()\n\n constructor(private readonly windowMs: number) {}\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n if (entry && entry.reset > now) {\n entry.hits++\n return { totalHits: entry.hits, resetTime: new Date(entry.reset) }\n }\n // ponytail: sweep-on-growth instead of a cleanup interval — no timer to\n // dispose, edge-safe. Bound is 10k live keys before a full sweep.\n if (this.hits.size > 10_000) {\n for (const [k, e] of this.hits) if (e.reset <= now) this.hits.delete(k)\n }\n const fresh = { hits: 1, reset: now + this.windowMs }\n this.hits.set(key, fresh)\n return { totalHits: 1, resetTime: new Date(fresh.reset) }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.hits > 0) entry.hits--\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n}\n\nfunction defaultKey(ctx: RequestContext): string {\n // `ctx.ip` carries the resolution that used to live here: prefer the\n // runtime-computed address (Express derives it from `trust proxy`), fall\n // back to forwarded headers only for runtimes that compute none.\n return ctx.ip ?? 'global'\n}\n\n/**\n * Rate limiting as a `(ctx, next)` middleware.\n *\n * ```ts\n * // Edge (Cloudflare Workers) — app-wide:\n * const app = createWebApp({\n * h3, modules,\n * middlewares: [rateLimitGuard({ max: 60, windowMs: 60_000, store: new KvRateLimitStore(env.KV, { windowMs: 60_000 }) })],\n * })\n *\n * // Any runtime — per controller/route:\n * @Middleware(rateLimitGuard({ max: 10 }))\n * @Post('/login')\n * ```\n */\nexport function rateLimitGuard(options: RateLimitGuardOptions = {}): MiddlewareHandler {\n const max = options.max ?? 100\n const windowMs = options.windowMs ?? 60_000\n const message = options.message ?? 'Too Many Requests'\n const statusCode = options.statusCode ?? 429\n const keyGenerator = options.keyGenerator ?? defaultKey\n const sendHeaders = options.headers ?? true\n const store = options.store ?? new LazyMemoryStore(windowMs)\n\n const exemptWhen = options.exemptWhen\n if (exemptWhen !== undefined) assertFlagTest(exemptWhen, 'rateLimitGuard({ exemptWhen })')\n\n return async (ctx: RequestContext, next: () => void): Promise<void> => {\n if (options.skip?.(ctx)) return next()\n if (exemptWhen !== undefined && matchesFlagTest(exemptWhen, ctx.route?.flags, ctx.route)) {\n return next()\n }\n\n const { totalHits, resetTime } = await store.increment(keyGenerator(ctx))\n const remaining = Math.max(0, max - totalHits)\n\n if (sendHeaders) {\n ctx.setHeader('X-RateLimit-Limit', String(max))\n ctx.setHeader('X-RateLimit-Remaining', String(remaining))\n ctx.setHeader('X-RateLimit-Reset', String(Math.ceil(resetTime.getTime() / 1000)))\n }\n\n if (totalHits > max) {\n const retryAfter = Math.max(1, Math.ceil((resetTime.getTime() - Date.now()) / 1000))\n if (sendHeaders) ctx.setHeader('Retry-After', String(retryAfter))\n ctx.json({ message }, statusCode)\n return\n }\n\n next()\n }\n}\n","// Edge-safe store adapters over a minimal KV surface. Zero runtime imports —\n// this file must stay importable from the `@forinda/kickjs/web` purity graph.\nimport type { RateLimitStore } from './rate-limit'\nimport type { SessionData, SessionStore } from './session'\n\n/**\n * The minimal async KV surface both stores need. Structurally compatible\n * with a Cloudflare Workers `KVNamespace` binding — pass it straight in:\n *\n * ```ts\n * export default {\n * fetch(req: Request, env: Env) {\n * const store = new KvRateLimitStore(env.MY_KV, { windowMs: 60_000 })\n * // ...\n * },\n * }\n * ```\n *\n * Anything with the same shape works too (Deno KV wrapper, Upstash Redis\n * REST client adapter, a Map-backed fake in tests).\n */\nexport interface KvLike {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/** Cloudflare KV rejects TTLs under 60 seconds — clamp up, never down. */\nfunction kvTtlSeconds(ms: number): number {\n return Math.max(60, Math.ceil(ms / 1000))\n}\n\nexport interface KvRateLimitStoreOptions {\n /** Window length in ms — must match the `windowMs` given to the limiter. */\n windowMs: number\n /** Key prefix in the KV namespace (default `'rl:'`). */\n prefix?: string\n}\n\n/**\n * {@link RateLimitStore} over a {@link KvLike} namespace, for edge deployments\n * where in-memory counters reset on every isolate.\n *\n * KV is eventually consistent and this is a read-modify-write counter, so\n * limiting is **approximate** under concurrency — bursts racing across\n * isolates can each read the same count. That is the accepted trade-off for\n * abuse throttling; use a Durable Object / Redis store when you need an\n * exact ceiling (billing quotas, strict per-key locks).\n */\nexport class KvRateLimitStore implements RateLimitStore {\n private readonly prefix: string\n private readonly windowMs: number\n\n constructor(\n private readonly kv: KvLike,\n options: KvRateLimitStoreOptions,\n ) {\n this.windowMs = options.windowMs\n this.prefix = options.prefix ?? 'rl:'\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = await this.read(key)\n if (entry && entry.reset > now) {\n entry.hits++\n await this.write(key, entry)\n return { totalHits: entry.hits, resetTime: new Date(entry.reset) }\n }\n const fresh = { hits: 1, reset: now + this.windowMs }\n await this.write(key, fresh)\n return { totalHits: 1, resetTime: new Date(fresh.reset) }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = await this.read(key)\n if (entry && entry.hits > 0) {\n entry.hits--\n await this.write(key, entry)\n }\n }\n\n async reset(key: string): Promise<void> {\n await this.kv.delete(this.prefix + key)\n }\n\n private async read(key: string): Promise<{ hits: number; reset: number } | null> {\n const raw = await this.kv.get(this.prefix + key)\n if (raw === null) return null\n try {\n return JSON.parse(raw) as { hits: number; reset: number }\n } catch {\n return null\n }\n }\n\n private async write(key: string, entry: { hits: number; reset: number }): Promise<void> {\n await this.kv.put(this.prefix + key, JSON.stringify(entry), {\n expirationTtl: kvTtlSeconds(Math.max(entry.reset - Date.now(), 1000)),\n })\n }\n}\n\nexport interface KvSessionStoreOptions {\n /** Key prefix in the KV namespace (default `'sess:'`). */\n prefix?: string\n}\n\n/**\n * {@link SessionStore} over a {@link KvLike} namespace. Plug into the node\n * `session()` middleware today (e.g. backed by Upstash/Cloudflare KV via\n * REST) so sessions survive restarts and horizontal scaling; TTL expiry is\n * delegated to the KV layer via `expirationTtl`.\n *\n * Cost note: `touch()` is a get + put round-trip (KvLike has no TTL-only\n * refresh), so `session({ rolling: true })` doubles KV operations per\n * request — per-operation-billed platforms may prefer `rolling: false`.\n */\nexport class KvSessionStore implements SessionStore {\n private readonly prefix: string\n\n constructor(\n private readonly kv: KvLike,\n options: KvSessionStoreOptions = {},\n ) {\n this.prefix = options.prefix ?? 'sess:'\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const raw = await this.kv.get(this.prefix + sid)\n if (raw === null) return null\n try {\n return JSON.parse(raw) as SessionData\n } catch {\n return null\n }\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n await this.kv.put(this.prefix + sid, JSON.stringify(data), {\n expirationTtl: kvTtlSeconds(maxAge),\n })\n }\n\n async destroy(sid: string): Promise<void> {\n await this.kv.delete(this.prefix + sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const data = await this.get(sid)\n if (data !== null) await this.set(sid, data, maxAge)\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v8.3.
|
|
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"reflect-metadata";function setClassMeta(key,value,target){Reflect.defineMetadata(key,value,target)}function setMethodMeta(key,value,target,method){Reflect.defineMetadata(key,value,target,method)}function getClassMeta(key,target,fallback){return Reflect.getMetadata(key,target)??fallback}function getMethodMeta(key,target,method,fallback){return Reflect.getMetadata(key,target,method)??fallback}function hasClassMeta(key,target){return Reflect.hasMetadata(key,target)}function getClassMetaOrUndefined(key,target){return Reflect.getMetadata(key,target)}function getMethodMetaOrUndefined(key,target,method){return Reflect.getMetadata(key,target,method)}function pushClassMeta(key,target,...items){let existing=Reflect.getOwnMetadata(key,target)??[];Reflect.defineMetadata(key,[...existing,...items],target)}function pushMethodMeta(key,target,method,...items){let existing=Reflect.getOwnMetadata(key,target,method)??[];Reflect.defineMetadata(key,[...existing,...items],target,method)}function setInMetaMap(key,target,mapKey,mapValue){let inherited=Reflect.getOwnMetadata(key,target),map=inherited?new Map(inherited):new Map;map.set(mapKey,mapValue),Reflect.defineMetadata(key,map,target)}function getMetaMap(key,target){return Reflect.getMetadata(key,target)??new Map}function setInMetaRecord(key,target,recKey,recValue){let inherited=Reflect.getOwnMetadata(key,target),rec=inherited?{...inherited}:{};rec[recKey]=recValue,Reflect.defineMetadata(key,rec,target)}function getMetaRecord(key,target){return Reflect.getMetadata(key,target)??{}}export{getMethodMeta as a,pushClassMeta as c,setInMetaMap as d,setInMetaRecord as f,getMetaRecord as i,pushMethodMeta as l,getClassMetaOrUndefined as n,getMethodMetaOrUndefined as o,setMethodMeta as p,getMetaMap as r,hasClassMeta as s,getClassMeta as t,setClassMeta as u};
|
|
12
|
-
//# sourceMappingURL=metadata-
|
|
12
|
+
//# sourceMappingURL=metadata-BfAea0Eh.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-
|
|
1
|
+
{"version":3,"file":"metadata-BfAea0Eh.mjs","names":[],"sources":["../src/core/metadata.ts"],"sourcesContent":["import 'reflect-metadata'\nimport type { InjectionToken } from './token'\n\n/**\n * Typed utilities for reflect-metadata operations.\n *\n * Replaces scattered Reflect.defineMetadata/getMetadata calls with a single\n * source of truth. If we later move to Stage 3 decorators or a WeakMap-based\n * store, only this file changes — all 24+ consumer files stay untouched.\n *\n * Keys may be symbols, strings, or `InjectionToken<T>` objects produced by\n * `createToken()`. Using `createToken` is the preferred pattern because the\n * token's phantom type flows through the helpers, enabling type-safe reads.\n */\n\ntype MetaKey = symbol | string | InjectionToken<unknown>\n\n// ── Setters ─────────────────────────────────────────────────────\n\n/** Set metadata on a class (or prototype for property decorators) */\nexport function setClassMeta<T>(key: MetaKey, value: T, target: object): void {\n Reflect.defineMetadata(key, value, target)\n}\n\n/** Set metadata on a specific method */\nexport function setMethodMeta<T>(key: MetaKey, value: T, target: object, method: string): void {\n Reflect.defineMetadata(key, value, target, method)\n}\n\n// ── Getters ─────────────────────────────────────────────────────\n\n/** Get metadata from a class, returning `fallback` if not set */\nexport function getClassMeta<T>(key: MetaKey, target: object, fallback: T): T {\n return (Reflect.getMetadata(key, target) as T) ?? fallback\n}\n\n/** Get metadata from a specific method, returning `fallback` if not set */\nexport function getMethodMeta<T>(key: MetaKey, target: object, method: string, fallback: T): T {\n return (Reflect.getMetadata(key, target, method) as T) ?? fallback\n}\n\n/** Check if metadata exists on a class */\nexport function hasClassMeta(key: MetaKey, target: object): boolean {\n return Reflect.hasMetadata(key, target)\n}\n\n/** Get metadata from a class with no default (returns undefined if absent) */\nexport function getClassMetaOrUndefined<T>(key: MetaKey, target: object): T | undefined {\n return Reflect.getMetadata(key, target) as T | undefined\n}\n\n/** Get metadata from a method with no default (returns undefined if absent) */\nexport function getMethodMetaOrUndefined<T>(\n key: MetaKey,\n target: object,\n method: string,\n): T | undefined {\n return Reflect.getMetadata(key, target, method) as T | undefined\n}\n\n// ── Accumulate: Arrays ──────────────────────────────────────────\n\n/** Append items to an array stored in class metadata */\nexport function pushClassMeta<T>(key: MetaKey, target: object, ...items: T[]): void {\n const existing: T[] = Reflect.getOwnMetadata(key, target) ?? []\n Reflect.defineMetadata(key, [...existing, ...items], target)\n}\n\n/** Append items to an array stored in method metadata */\nexport function pushMethodMeta<T>(\n key: MetaKey,\n target: object,\n method: string,\n ...items: T[]\n): void {\n const existing: T[] = Reflect.getOwnMetadata(key, target, method) ?? []\n Reflect.defineMetadata(key, [...existing, ...items], target, method)\n}\n\n// ── Accumulate: Maps ────────────────────────────────────────────\n\n/** Set a key/value in a Map stored in class metadata */\nexport function setInMetaMap<K, V>(key: MetaKey, target: object, mapKey: K, mapValue: V): void {\n const inherited: Map<K, V> | undefined = Reflect.getOwnMetadata(key, target)\n const map = inherited ? new Map(inherited) : new Map<K, V>()\n map.set(mapKey, mapValue)\n Reflect.defineMetadata(key, map, target)\n}\n\n/** Get a Map from class metadata (returns empty Map if absent) */\nexport function getMetaMap<K, V>(key: MetaKey, target: object): Map<K, V> {\n return Reflect.getMetadata(key, target) ?? new Map()\n}\n\n// ── Accumulate: Records ─────────────────────────────────────────\n\n/** Set a key/value in a Record stored in class metadata */\nexport function setInMetaRecord<V>(\n key: MetaKey,\n target: object,\n recKey: string | number,\n recValue: V,\n): void {\n const inherited: Record<string | number, V> | undefined = Reflect.getOwnMetadata(key, target)\n const rec = inherited ? { ...inherited } : {}\n rec[recKey] = recValue\n Reflect.defineMetadata(key, rec, target)\n}\n\n/** Get a Record from class metadata (returns empty object if absent) */\nexport function getMetaRecord<V>(key: MetaKey, target: object): Record<string | number, V> {\n return Reflect.getMetadata(key, target) ?? {}\n}\n"],"mappings":";;;;;;;;;;yBAoBA,SAAgB,aAAgB,IAAc,MAAU,OAAsB,CAC5E,QAAQ,eAAe,IAAK,MAAO,MAAM,CAC3C,CAGA,SAAgB,cAAiB,IAAc,MAAU,OAAgB,OAAsB,CAC7F,QAAQ,eAAe,IAAK,MAAO,OAAQ,MAAM,CACnD,CAKA,SAAgB,aAAgB,IAAc,OAAgB,SAAgB,CAC5E,OAAQ,QAAQ,YAAY,IAAK,MAAM,GAAW,QACpD,CAGA,SAAgB,cAAiB,IAAc,OAAgB,OAAgB,SAAgB,CAC7F,OAAQ,QAAQ,YAAY,IAAK,OAAQ,MAAM,GAAW,QAC5D,CAGA,SAAgB,aAAa,IAAc,OAAyB,CAClE,OAAO,QAAQ,YAAY,IAAK,MAAM,CACxC,CAGA,SAAgB,wBAA2B,IAAc,OAA+B,CACtF,OAAO,QAAQ,YAAY,IAAK,MAAM,CACxC,CAGA,SAAgB,yBACd,IACA,OACA,OACe,CACf,OAAO,QAAQ,YAAY,IAAK,OAAQ,MAAM,CAChD,CAKA,SAAgB,cAAiB,IAAc,OAAgB,GAAG,MAAkB,CAClF,IAAM,SAAgB,QAAQ,eAAe,IAAK,MAAM,GAAK,CAAC,EAC9D,QAAQ,eAAe,IAAK,CAAC,GAAG,SAAU,GAAG,KAAK,EAAG,MAAM,CAC7D,CAGA,SAAgB,eACd,IACA,OACA,OACA,GAAG,MACG,CACN,IAAM,SAAgB,QAAQ,eAAe,IAAK,OAAQ,MAAM,GAAK,CAAC,EACtE,QAAQ,eAAe,IAAK,CAAC,GAAG,SAAU,GAAG,KAAK,EAAG,OAAQ,MAAM,CACrE,CAKA,SAAgB,aAAmB,IAAc,OAAgB,OAAW,SAAmB,CAC7F,IAAM,UAAmC,QAAQ,eAAe,IAAK,MAAM,EACrE,IAAM,UAAY,IAAI,IAAI,SAAS,EAAI,IAAI,IACjD,IAAI,IAAI,OAAQ,QAAQ,EACxB,QAAQ,eAAe,IAAK,IAAK,MAAM,CACzC,CAGA,SAAgB,WAAiB,IAAc,OAA2B,CACxE,OAAO,QAAQ,YAAY,IAAK,MAAM,GAAK,IAAI,GACjD,CAKA,SAAgB,gBACd,IACA,OACA,OACA,SACM,CACN,IAAM,UAAoD,QAAQ,eAAe,IAAK,MAAM,EACtF,IAAM,UAAY,CAAE,GAAG,SAAU,EAAI,CAAC,EAC5C,IAAI,QAAU,SACd,QAAQ,eAAe,IAAK,IAAK,MAAM,CACzC,CAGA,SAAgB,cAAiB,IAAc,OAA4C,CACzF,OAAO,QAAQ,YAAY,IAAK,MAAM,GAAK,CAAC,CAC9C"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v8.3.
|
|
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
|
var MutableModuleRegistry=class{entries=[];mount(module){this.entries.push(module)}};export{MutableModuleRegistry as t};
|
|
12
|
-
//# sourceMappingURL=module-registry-
|
|
12
|
+
//# sourceMappingURL=module-registry-BXUTVQZX.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-registry-
|
|
1
|
+
{"version":3,"file":"module-registry-BXUTVQZX.mjs","names":[],"sources":["../src/core/module-registry.ts"],"sourcesContent":["import type { AppModuleEntry } from './app-module'\n\n/**\n * Imperative receiver passed to {@link KickPlugin.setup} and the\n * top-level `bootstrap({ setup })` callback so adopters can register\n * modules conditionally — based on env flags, runtime config,\n * tenant lists, etc. — instead of being limited to the static\n * `modules: [...]` array.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules: [HelloModule()], // static — always mounted\n * setup(registry) {\n * if (env.ENABLE_ADMIN) registry.mount(AdminModule())\n * for (const tenant of env.TENANTS) {\n * registry.mount(TenantModule.scoped(tenant.id, { id: tenant.id }))\n * }\n * },\n * })\n * ```\n *\n * **Currently exposes only `.mount(module)` — the HTTP-feature path.**\n * A future `.use(module)` for non-HTTP modules (queues, cron, workers,\n * DI-only seeds) is planned but not yet implemented; existing non-HTTP\n * modules continue returning `null` from `routes()` and registering\n * via `.mount()`.\n *\n * Same dispatch rules as the static `modules: [...]` array — class\n * entries get `new`-ed at boot, `defineModule` factory output is used\n * directly. The registry doesn't validate the entry shape itself; the\n * Application loader does the typeof discrimination at mount time.\n */\nexport interface ModuleRegistry {\n /**\n * Register a module for the full HTTP lifecycle: `register()` runs,\n * `import.meta.glob` decorator side-effects fire, `routes()` is\n * called and its result mounted, `contributors()` are merged at\n * `'module'` precedence and applied to the mounted routes.\n *\n * Modules are mounted in registration order (first call → first\n * mount). Order across sources is: plugin static modules → plugin\n * `setup()` calls → user static modules → user `setup()` callback.\n */\n mount(module: AppModuleEntry): void\n}\n\n/**\n * Internal collector implementing {@link ModuleRegistry}. Application\n * passes one of these to every `setup()` callback during bootstrap;\n * the collected `entries` array is then run through the same\n * class-vs-instance dispatch as the static `modules: [...]` array.\n *\n * Not exported from the public surface — adopters interact only\n * through the {@link ModuleRegistry} interface.\n */\nexport class MutableModuleRegistry implements ModuleRegistry {\n readonly entries: AppModuleEntry[] = []\n\n mount(module: AppModuleEntry): void {\n this.entries.push(module)\n }\n}\n"],"mappings":";;;;;;;;;;AAwDA,IAAa,sBAAb,KAA6D,CAC3D,QAAqC,CAAC,EAEtC,MAAM,OAA8B,CAClC,KAAK,QAAQ,KAAK,MAAM,CAC1B,CACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs v8.3.2
|
|
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-6wpoIQxU.mjs.map
|