@forinda/kickjs 7.4.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +1 -0
  2. package/dist/{adapter-DB1jE7nd.d.mts → adapter-OjEKLePS.d.mts} +6 -5
  3. package/dist/{adapter-DB1jE7nd.d.mts.map → adapter-OjEKLePS.d.mts.map} +1 -1
  4. package/dist/{app-module-Cr4qnc9a.d.mts → app-module-CmIHP7uW.d.mts} +40 -4
  5. package/dist/{app-module-Cr4qnc9a.d.mts.map → app-module-CmIHP7uW.d.mts.map} +1 -1
  6. package/dist/{application-BjsHkh3L.d.mts → application-BmJaP130.d.mts} +16 -16
  7. package/dist/application-BmJaP130.d.mts.map +1 -0
  8. package/dist/application-DrOxJWUm.mjs +12 -0
  9. package/dist/application-DrOxJWUm.mjs.map +1 -0
  10. package/dist/{bootstrap-DnAlEMPs.d.mts → bootstrap-2QoNFOmO.d.mts} +2 -2
  11. package/dist/bootstrap-2QoNFOmO.d.mts.map +1 -0
  12. package/dist/{bootstrap-CK8OXB9Y.mjs → bootstrap-CJkXHDeQ.mjs} +3 -3
  13. package/dist/{bootstrap-CK8OXB9Y.mjs.map → bootstrap-CJkXHDeQ.mjs.map} +1 -1
  14. package/dist/{client-ip-De2ynoAd.mjs → client-ip-D_XUCo45.mjs} +2 -2
  15. package/dist/{client-ip-De2ynoAd.mjs.map → client-ip-D_XUCo45.mjs.map} +1 -1
  16. package/dist/config/index.d.mts +1 -1
  17. package/dist/config/index.mjs +2 -2
  18. package/dist/config-Bi6kDjo9.mjs +12 -0
  19. package/dist/{config-BxW5Zr48.mjs.map → config-Bi6kDjo9.mjs.map} +1 -1
  20. package/dist/{container-BW_MlgLj.d.mts → container-BA-O7uBF.d.mts} +18 -1
  21. package/dist/container-BA-O7uBF.d.mts.map +1 -0
  22. package/dist/container-jtn5BmL5.mjs +117 -0
  23. package/dist/container-jtn5BmL5.mjs.map +1 -0
  24. package/dist/{context-Dh01xr3X.mjs → context-caMuzOpA.mjs} +3 -3
  25. package/dist/context-caMuzOpA.mjs.map +1 -0
  26. package/dist/{context-errors--pMnF37M.mjs → context-errors-D0EbFrTN.mjs} +2 -2
  27. package/dist/{context-errors--pMnF37M.mjs.map → context-errors-D0EbFrTN.mjs.map} +1 -1
  28. package/dist/core/adapter.d.mts +1 -1
  29. package/dist/core/adapter.mjs +1 -1
  30. package/dist/core/app-module.d.mts +2 -2
  31. package/dist/core/app-module.mjs +1 -1
  32. package/dist/core/cache.mjs +1 -1
  33. package/dist/core/container.d.mts +1 -1
  34. package/dist/core/container.mjs +2 -2
  35. package/dist/core/cron.mjs +2 -2
  36. package/dist/core/decorators.d.mts +1 -1
  37. package/dist/core/decorators.mjs +2 -2
  38. package/dist/core/decorators.mjs.map +1 -1
  39. package/dist/core/errors.mjs +1 -1
  40. package/dist/core/interfaces.mjs +1 -1
  41. package/dist/core/logger.mjs +1 -1
  42. package/dist/core/path.mjs +1 -1
  43. package/dist/core/plugin.d.mts +1 -1
  44. package/dist/core/plugin.mjs +1 -1
  45. package/dist/core/reactivity.mjs +1 -1
  46. package/dist/{core-BGsV3RjD.mjs → core-B1PK7S5p.mjs} +3 -3
  47. package/dist/{core-BGsV3RjD.mjs.map → core-B1PK7S5p.mjs.map} +1 -1
  48. package/dist/{cors-BuDU9CpA.d.mts → cors-CsibNTQ6.d.mts} +3 -3
  49. package/dist/{cors-BuDU9CpA.d.mts.map → cors-CsibNTQ6.d.mts.map} +1 -1
  50. package/dist/csrf-Cab0-YPs.mjs +12 -0
  51. package/dist/csrf-Cab0-YPs.mjs.map +1 -0
  52. package/dist/{csrf-vR23CJas.d.mts → csrf-DwOhkOC6.d.mts} +3 -3
  53. package/dist/csrf-DwOhkOC6.d.mts.map +1 -0
  54. package/dist/decorate-C0Rr6ywx.mjs +11 -0
  55. package/dist/{decorators-Bks7cgBJ.d.mts → decorators-BbtQ3Vf6.d.mts} +11 -2
  56. package/dist/decorators-BbtQ3Vf6.d.mts.map +1 -0
  57. package/dist/{define-adapter-VXKsMUNb.mjs → define-adapter-DUBru2GV.mjs} +2 -2
  58. package/dist/{define-adapter-VXKsMUNb.mjs.map → define-adapter-DUBru2GV.mjs.map} +1 -1
  59. package/dist/{disposables-Bwm-JdIp.mjs → disposables-jJAxz_e1.mjs} +2 -2
  60. package/dist/{disposables-Bwm-JdIp.mjs.map → disposables-jJAxz_e1.mjs.map} +1 -1
  61. package/dist/{env-BG0pRPr7.mjs → env-C4hwX5of.mjs} +3 -3
  62. package/dist/{env-BG0pRPr7.mjs.map → env-C4hwX5of.mjs.map} +1 -1
  63. package/dist/error-handler-ade-elv6.d.mts +63 -0
  64. package/dist/error-handler-ade-elv6.d.mts.map +1 -0
  65. package/dist/handler-BuzdkiTz.mjs +13 -0
  66. package/dist/handler-BuzdkiTz.mjs.map +1 -0
  67. package/dist/{helmet-CFQUTVig.d.mts → helmet-C2QEU6S-.d.mts} +5 -3
  68. package/dist/helmet-C2QEU6S-.d.mts.map +1 -0
  69. package/dist/http/application.d.mts +1 -1
  70. package/dist/http/application.mjs +2 -2
  71. package/dist/http/bootstrap.d.mts +1 -1
  72. package/dist/http/bootstrap.mjs +2 -2
  73. package/dist/http/context.d.mts +2 -2
  74. package/dist/http/context.mjs +2 -2
  75. package/dist/http/middleware/cors.d.mts +1 -1
  76. package/dist/http/middleware/cors.mjs +1 -1
  77. package/dist/http/middleware/cors.mjs.map +1 -1
  78. package/dist/http/middleware/csrf.d.mts +1 -1
  79. package/dist/http/middleware/csrf.mjs +2 -2
  80. package/dist/http/middleware/error-handler.d.mts +2 -2
  81. package/dist/http/middleware/error-handler.mjs +2 -2
  82. package/dist/http/middleware/error-handler.mjs.map +1 -1
  83. package/dist/http/middleware/helmet.d.mts +2 -2
  84. package/dist/http/middleware/helmet.mjs +2 -2
  85. package/dist/http/middleware/helmet.mjs.map +1 -1
  86. package/dist/http/middleware/rate-limit.d.mts +1 -1
  87. package/dist/http/middleware/rate-limit.mjs +2 -2
  88. package/dist/http/middleware/rate-limit.mjs.map +1 -1
  89. package/dist/http/middleware/request-id.mjs +1 -1
  90. package/dist/http/middleware/request-logger.d.mts +2 -2
  91. package/dist/http/middleware/request-logger.mjs +2 -2
  92. package/dist/http/middleware/request-logger.mjs.map +1 -1
  93. package/dist/http/middleware/session.d.mts +1 -1
  94. package/dist/http/middleware/session.mjs +2 -2
  95. package/dist/http/middleware/session.mjs.map +1 -1
  96. package/dist/http/middleware/spa.d.mts +1 -1
  97. package/dist/http/middleware/spa.mjs +2 -2
  98. package/dist/http/middleware/upload.d.mts +1 -1
  99. package/dist/http/middleware/upload.mjs +3 -3
  100. package/dist/http/middleware/upload.mjs.map +1 -1
  101. package/dist/http/middleware/validate.mjs +1 -1
  102. package/dist/http/middleware/views.d.mts +1 -1
  103. package/dist/http/middleware/views.mjs +2 -2
  104. package/dist/http/query/index.mjs +2 -2
  105. package/dist/http/router-builder.d.mts +1 -1
  106. package/dist/http/router-builder.mjs +2 -2
  107. package/dist/http/runtimes/fastify.d.mts +13 -1
  108. package/dist/http/runtimes/fastify.d.mts.map +1 -1
  109. package/dist/http/runtimes/fastify.mjs +3 -3
  110. package/dist/http/runtimes/fastify.mjs.map +1 -1
  111. package/dist/http/runtimes/h3-web.d.mts +1 -1
  112. package/dist/http/runtimes/h3-web.mjs +2 -2
  113. package/dist/http/runtimes/h3.d.mts +1 -1
  114. package/dist/http/runtimes/h3.d.mts.map +1 -1
  115. package/dist/http/runtimes/h3.mjs +2 -2
  116. package/dist/http/runtimes/h3.mjs.map +1 -1
  117. package/dist/{index-M6TF75MS.d.mts → index-BaMA8_NF.d.mts} +3 -3
  118. package/dist/{index-M6TF75MS.d.mts.map → index-BaMA8_NF.d.mts.map} +1 -1
  119. package/dist/index.d.mts +19 -19
  120. package/dist/index.mjs +2 -2
  121. package/dist/index.mjs.map +1 -1
  122. package/dist/{kv-stores-CgwQScG3.d.mts → kv-stores-BPxJjf56.d.mts} +26 -9
  123. package/dist/kv-stores-BPxJjf56.d.mts.map +1 -0
  124. package/dist/{kv-stores-Dgfu4Kgr.mjs → kv-stores-CosFzIYm.mjs} +2 -2
  125. package/dist/kv-stores-CosFzIYm.mjs.map +1 -0
  126. package/dist/{metadata-B5J7GRqy.mjs → metadata-DUJjjW0o.mjs} +2 -2
  127. package/dist/{metadata-B5J7GRqy.mjs.map → metadata-DUJjjW0o.mjs.map} +1 -1
  128. package/dist/{module-registry-wgY_h4QH.mjs → module-registry-CpXYcf4e.mjs} +2 -2
  129. package/dist/{module-registry-wgY_h4QH.mjs.map → module-registry-CpXYcf4e.mjs.map} +1 -1
  130. package/dist/{query-CrSAtLqV.mjs → query-u5LBf5yW.mjs} +2 -2
  131. package/dist/{query-CrSAtLqV.mjs.map → query-u5LBf5yW.mjs.map} +1 -1
  132. package/dist/{rate-limit-Dme747JJ.d.mts → rate-limit-D_cKx6Uw.d.mts} +3 -3
  133. package/dist/{rate-limit-Dme747JJ.d.mts.map → rate-limit-D_cKx6Uw.d.mts.map} +1 -1
  134. package/dist/{reply-DVH0keKZ.mjs → reply-Ca-H1Ozr.mjs} +3 -3
  135. package/dist/reply-Ca-H1Ozr.mjs.map +1 -0
  136. package/dist/{request-logger-D_pf4xzE.d.mts → request-logger-TgcKRNDD.d.mts} +16 -3
  137. package/dist/request-logger-TgcKRNDD.d.mts.map +1 -0
  138. package/dist/respond-C6fe709p.mjs +12 -0
  139. package/dist/respond-C6fe709p.mjs.map +1 -0
  140. package/dist/{router-builder-BlReYbNA.d.mts → router-builder-DUi2RHBv.d.mts} +2 -2
  141. package/dist/{router-builder-BlReYbNA.d.mts.map → router-builder-DUi2RHBv.d.mts.map} +1 -1
  142. package/dist/{router-builder-A4jcCl4J.mjs → router-builder-Dqs8ITU7.mjs} +3 -3
  143. package/dist/{router-builder-A4jcCl4J.mjs.map → router-builder-Dqs8ITU7.mjs.map} +1 -1
  144. package/dist/{session-z9iewOXA.d.mts → session-BHgRyfqZ.d.mts} +2 -2
  145. package/dist/{session-z9iewOXA.d.mts.map → session-BHgRyfqZ.d.mts.map} +1 -1
  146. package/dist/{spa-1Uc4QIif.d.mts → spa-DAoE9C2S.d.mts} +75 -17
  147. package/dist/spa-DAoE9C2S.d.mts.map +1 -0
  148. package/dist/{upload-config-BvSLBwom.mjs → upload-config-Da_g7Rig.mjs} +2 -2
  149. package/dist/{upload-config-BvSLBwom.mjs.map → upload-config-Da_g7Rig.mjs.map} +1 -1
  150. package/dist/{upload-aQzE6lT0.d.mts → upload-dSsVE0BW.d.mts} +3 -3
  151. package/dist/{upload-aQzE6lT0.d.mts.map → upload-dSsVE0BW.d.mts.map} +1 -1
  152. package/dist/web.d.mts +6 -6
  153. package/dist/web.d.mts.map +1 -1
  154. package/dist/web.mjs +2 -2
  155. package/dist/web.mjs.map +1 -1
  156. package/package.json +5 -5
  157. package/dist/application-BjsHkh3L.d.mts.map +0 -1
  158. package/dist/application-DiDdUVvX.mjs +0 -12
  159. package/dist/application-DiDdUVvX.mjs.map +0 -1
  160. package/dist/bootstrap-DnAlEMPs.d.mts.map +0 -1
  161. package/dist/config-BxW5Zr48.mjs +0 -12
  162. package/dist/container-BW_MlgLj.d.mts.map +0 -1
  163. package/dist/container-CGkGmNHM.mjs +0 -117
  164. package/dist/container-CGkGmNHM.mjs.map +0 -1
  165. package/dist/context-Dh01xr3X.mjs.map +0 -1
  166. package/dist/csrf-vR23CJas.d.mts.map +0 -1
  167. package/dist/csrf-z7Dd3k3U.mjs +0 -12
  168. package/dist/csrf-z7Dd3k3U.mjs.map +0 -1
  169. package/dist/decorators-Bks7cgBJ.d.mts.map +0 -1
  170. package/dist/error-handler-BE1iKFde.d.mts +0 -43
  171. package/dist/error-handler-BE1iKFde.d.mts.map +0 -1
  172. package/dist/handler-D34xTXN1.mjs +0 -13
  173. package/dist/handler-D34xTXN1.mjs.map +0 -1
  174. package/dist/helmet-CFQUTVig.d.mts.map +0 -1
  175. package/dist/kv-stores-CgwQScG3.d.mts.map +0 -1
  176. package/dist/kv-stores-Dgfu4Kgr.mjs.map +0 -1
  177. package/dist/reply-DVH0keKZ.mjs.map +0 -1
  178. package/dist/request-logger-D_pf4xzE.d.mts.map +0 -1
  179. package/dist/spa-1Uc4QIif.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"error-handler.mjs","names":[],"sources":["../../../src/http/middleware/error-handler.ts"],"sourcesContent":["import type { RuntimeResponse } from '../runtime'\n\n/**\n * The slice of the request this handler reads, spelled from node's\n * `IncomingMessage` rather than Express's `Request`.\n *\n * Fastify and h3 hand the error handler `request.raw` — a plain\n * `IncomingMessage` — so anything Express-only is `undefined` there. This is\n * the default handler for EVERY runtime, so it must not assume Express.\n */\ninterface ErrorRequest {\n method?: string\n url?: string\n /** Express-only; absent on the raw node request the other runtimes pass. */\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n}\nimport {\n HttpException,\n ProblemException,\n normalizeProblem,\n createLogger,\n describeError,\n} from '../../core'\n\nconst log = createLogger('ErrorHandler')\n\n/** Catch-all for unmatched routes */\nexport function notFoundHandler() {\n return (_req: ErrorRequest, res: RuntimeResponse, _next: () => void) => {\n res.status(404).json({ message: 'Not Found' })\n }\n}\n\n/**\n * Global error handler.\n *\n * Default behavior strips error `details` from the response in production\n * (NODE_ENV === 'production') so internal context (DB errors, validation\n * issues, custom payloads) does not leak to clients. Apps that want to\n * expose details in production — for client-facing field-level validation,\n * for example — should pass their own `onError` to `bootstrap()` and decide\n * the policy explicitly.\n *\n * Unexpected 500s carry `requestId` in every environment (the correlation\n * handle back to the log line) and, outside production, the error summary\n * plus stack. Production bodies stay opaque.\n *\n * {@link ProblemException} is dispatched first and emits an\n * `application/problem+json` response per RFC 9457. Plain\n * {@link HttpException} keeps the existing `{ message, errors? }` shape\n * for backward compatibility.\n */\nexport function errorHandler() {\n const isProduction = process.env.NODE_ENV === 'production'\n return (err: any, req: ErrorRequest, res: RuntimeResponse, _next: () => void) => {\n // Don't write after headers are already sent\n if (res.headersSent) {\n log.warn(`Error after headers sent: ${err?.message || 'Unknown'}`)\n return\n }\n\n // RFC 9457 Problem Details — checked before HttpException because\n // ProblemException extends HttpException; instanceof on the base\n // would otherwise swallow it.\n if (err instanceof ProblemException) {\n if (err.problem.status >= 500) {\n log.error(err, err.message)\n }\n if (err.headers) {\n for (const [k, v] of Object.entries(err.headers)) {\n res.setHeader(k, v)\n }\n }\n const body = normalizeProblem(err.problem)\n res.setHeader('Content-Type', 'application/problem+json')\n return res.status(body.status).json(body)\n }\n\n // Zod validation errors\n if (err?.name === 'ZodError') {\n const firstIssue = err.issues?.[0]\n return res.status(422).json({\n message: firstIssue?.message || 'Validation failed',\n ...(isProduction ? {} : { errors: err.issues }),\n })\n }\n\n // HttpException (expected application errors)\n if (err instanceof HttpException) {\n if (err.status >= 500) {\n log.error(err, err.message)\n }\n if (err.headers) {\n for (const [k, v] of Object.entries(err.headers)) {\n res.setHeader(k, v)\n }\n }\n const exposeDetails = !isProduction && err.details !== undefined\n return res.status(err.status).json({\n message: err.message,\n ...(exposeDetails ? { errors: err.details } : {}),\n })\n }\n\n // Unexpected errors — always log\n const status = err.status || err.statusCode || 500\n const requestId = (req as any).requestId ?? req.headers['x-request-id']\n log.error(\n err,\n // `originalUrl` is Express-only. Fastify and h3 pass `request.raw`, so\n // this logged `GET undefined — <error>` on every error under those\n // runtimes — the path silently dropped from the one line meant to\n // identify the failing request.\n `${req.method} ${req.originalUrl ?? req.url} — ${describeError(err)}${\n requestId ? ` [${requestId}]` : ''\n }`,\n )\n\n if (status !== 500) {\n return res.status(status).json({\n message: err.message || 'Error',\n ...(requestId ? { requestId } : {}),\n })\n }\n\n // A 500 body must never carry the raw error in production — it can\n // contain table names, SQL, connection strings, or user data. But\n // returning a bare `{ message: 'Internal Server Error' }` in\n // development means the one place a developer is looking tells them\n // nothing, and the failure has to be re-diagnosed from the database.\n //\n // Development gets the full picture. Production gets the requestId,\n // which is the correlation handle back to the (now stack-carrying)\n // log line — without it an opaque 500 can't even be tied to its own\n // log entry.\n res.status(500).json({\n message: 'Internal Server Error',\n ...(requestId ? { requestId } : {}),\n ...(isProduction\n ? {}\n : {\n error: describeError(err),\n ...(typeof err?.stack === 'string' ? { stack: err.stack.split('\\n') } : {}),\n }),\n })\n }\n}\n"],"mappings":";;;;;;;;;;iLAyBA,MAAM,IAAM,aAAa,cAAc,EAGvC,SAAgB,iBAAkB,CAChC,OAAQ,KAAoB,IAAsB,QAAsB,CACtE,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,CAAE,QAAS,WAAY,CAAC,CAC/C,CACF,CAqBA,SAAgB,cAAe,CAC7B,IAAM,aAAe,QAAQ,IAAI,WAAa,aAC9C,OAAQ,IAAU,IAAmB,IAAsB,QAAsB,CAE/E,GAAI,IAAI,YAAa,CACnB,IAAI,KAAK,6BAA6B,KAAK,SAAW,WAAW,EACjE,MACF,CAKA,GAAI,eAAe,iBAAkB,CAInC,GAHI,IAAI,QAAQ,QAAU,KACxB,IAAI,MAAM,IAAK,IAAI,OAAO,EAExB,IAAI,QACN,IAAK,GAAM,CAAC,EAAG,KAAM,OAAO,QAAQ,IAAI,OAAO,EAC7C,IAAI,UAAU,EAAG,CAAC,EAGtB,IAAM,KAAO,iBAAiB,IAAI,OAAO,EAEzC,OADA,IAAI,UAAU,eAAgB,0BAA0B,EACjD,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,IAAI,CAC1C,CAGA,GAAI,KAAK,OAAS,WAAY,CAC5B,IAAM,WAAa,IAAI,SAAS,GAChC,OAAO,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,CAC1B,QAAS,YAAY,SAAW,oBAChC,GAAI,aAAe,CAAC,EAAI,CAAE,OAAQ,IAAI,MAAO,CAC/C,CAAC,CACH,CAGA,GAAI,eAAe,cAAe,CAIhC,GAHI,IAAI,QAAU,KAChB,IAAI,MAAM,IAAK,IAAI,OAAO,EAExB,IAAI,QACN,IAAK,GAAM,CAAC,EAAG,KAAM,OAAO,QAAQ,IAAI,OAAO,EAC7C,IAAI,UAAU,EAAG,CAAC,EAGtB,IAAM,cAAgB,CAAC,cAAgB,IAAI,UAAY,IAAA,GACvD,OAAO,IAAI,OAAO,IAAI,MAAM,CAAC,CAAC,KAAK,CACjC,QAAS,IAAI,QACb,GAAI,cAAgB,CAAE,OAAQ,IAAI,OAAQ,EAAI,CAAC,CACjD,CAAC,CACH,CAGA,IAAM,OAAS,IAAI,QAAU,IAAI,YAAc,IACzC,UAAa,IAAY,WAAa,IAAI,QAAQ,gBAYxD,GAXA,IAAI,MACF,IAKA,GAAG,IAAI,OAAO,GAAG,IAAI,aAAe,IAAI,IAAI,KAAK,cAAc,GAAG,IAChE,UAAY,KAAK,UAAU,GAAK,IAEpC,EAEI,SAAW,IACb,OAAO,IAAI,OAAO,MAAM,CAAC,CAAC,KAAK,CAC7B,QAAS,IAAI,SAAW,QACxB,GAAI,UAAY,CAAE,SAAU,EAAI,CAAC,CACnC,CAAC,EAaH,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,CACnB,QAAS,wBACT,GAAI,UAAY,CAAE,SAAU,EAAI,CAAC,EACjC,GAAI,aACA,CAAC,EACD,CACE,MAAO,cAAc,GAAG,EACxB,GAAI,OAAO,KAAK,OAAU,SAAW,CAAE,MAAO,IAAI,MAAM,MAAM;CAAI,CAAE,EAAI,CAAC,CAC3E,CACN,CAAC,CACH,CACF"}
1
+ {"version":3,"file":"error-handler.mjs","names":[],"sources":["../../../src/http/middleware/error-handler.ts"],"sourcesContent":["import type { RuntimeResponse } from '../runtime'\n\n/**\n * The slice of the request this handler reads, spelled from node's\n * `IncomingMessage` rather than Express's `Request`.\n *\n * Fastify and h3 hand the error handler `request.raw` — a plain\n * `IncomingMessage` — so anything Express-only is `undefined` there. This is\n * the default handler for EVERY runtime, so it must not assume Express.\n */\ninterface ErrorRequest {\n method?: string\n url?: string\n /** Express-only; absent on the raw node request the other runtimes pass. */\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n}\nimport {\n HttpException,\n ProblemException,\n normalizeProblem,\n createLogger,\n describeError,\n} from '../../core'\n\nconst log = createLogger('ErrorHandler')\n\n/** A mounted route, as the catch-all needs to see it. */\nexport interface MountedRoute {\n method: string\n /** Full path as mounted, `:param` segments included. */\n path: string\n}\n\n/** `/things/:id` → `^/things/[^/]+$`, so a concrete path can be matched. */\nfunction patternToRegExp(pattern: string): RegExp {\n const source = pattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\/:[^/]+/g, '/[^/]+')\n .replace(/\\*/g, '.*')\n return new RegExp(`^${source}/?$`)\n}\n\n/** Path without query string or hash. */\nfunction pathOf(req: ErrorRequest): string {\n const url = req.originalUrl ?? req.url ?? '/'\n const cut = url.search(/[?#]/)\n return cut === -1 ? url : url.slice(0, cut)\n}\n\n/**\n * Catch-all for unmatched routes.\n *\n * Answers RFC 9457 problem details, like every other error the framework\n * produces — the catch-all was the one path still emitting a bare\n * `{ message }`, so a client parsing `application/problem+json` had to special\n * case it.\n *\n * Distinguishes a path that does not exist from a path that does not accept\n * this method. `DELETE /things/1` where only `GET` is mounted is a **405** with\n * an `Allow` header, not a 404: the resource is there, the verb is not, and\n * 404 tells a client to stop looking for something that exists. Requires the\n * mounted route table, so an app that does not pass one keeps answering 404.\n */\nexport function notFoundHandler(routes: readonly MountedRoute[] = []) {\n const matchers = routes.map((route) => ({\n method: route.method.toUpperCase(),\n matches: patternToRegExp(route.path),\n }))\n\n return (req: ErrorRequest, res: RuntimeResponse, _next: () => void) => {\n const path = pathOf(req)\n const method = (req.method ?? 'GET').toUpperCase()\n const allowed = [...new Set(matchers.filter((m) => m.matches.test(path)).map((m) => m.method))]\n\n if (allowed.length > 0 && !allowed.includes(method)) {\n // Sorted once: the header and the detail must list the same verbs in the\n // same order, and `sort()` would mutate `allowed` under the second read.\n const verbs = allowed.toSorted().join(', ')\n // RFC 9110 §15.5.6 requires Allow on a 405.\n res.setHeader('Allow', verbs)\n res.setHeader('Content-Type', 'application/problem+json')\n return res.status(405).json(\n normalizeProblem({\n status: 405,\n detail: `${method} is not supported for this resource. Allowed: ${verbs}.`,\n }),\n )\n }\n\n res.setHeader('Content-Type', 'application/problem+json')\n return res.status(404).json(normalizeProblem({ status: 404 }))\n }\n}\n\n/**\n * Global error handler.\n *\n * Default behavior strips error `details` from the response in production\n * (NODE_ENV === 'production') so internal context (DB errors, validation\n * issues, custom payloads) does not leak to clients. Apps that want to\n * expose details in production — for client-facing field-level validation,\n * for example — should pass their own `onError` to `bootstrap()` and decide\n * the policy explicitly.\n *\n * Unexpected 500s carry `requestId` in every environment (the correlation\n * handle back to the log line) and, outside production, the error summary\n * plus stack. Production bodies stay opaque.\n *\n * {@link ProblemException} and plain {@link HttpException} both emit\n * `application/problem+json` per RFC 9457. `ProblemException` carries a\n * caller-supplied `type` / `title` / extensions; an `HttpException`'s message\n * becomes `detail`, with `title` and `type` defaulted from the status. One\n * shape, so a client never has to branch on which one the app threw.\n */\nexport function errorHandler() {\n const isProduction = process.env.NODE_ENV === 'production'\n return (err: any, req: ErrorRequest, res: RuntimeResponse, _next: () => void) => {\n // Don't write after headers are already sent\n if (res.headersSent) {\n log.warn(`Error after headers sent: ${err?.message || 'Unknown'}`)\n return\n }\n\n // RFC 9457 Problem Details — checked before HttpException because\n // ProblemException extends HttpException; instanceof on the base\n // would otherwise swallow it.\n if (err instanceof ProblemException) {\n if (err.problem.status >= 500) {\n log.error(err, err.message)\n }\n if (err.headers) {\n for (const [k, v] of Object.entries(err.headers)) {\n res.setHeader(k, v)\n }\n }\n const body = normalizeProblem(err.problem)\n res.setHeader('Content-Type', 'application/problem+json')\n return res.status(body.status).json(body)\n }\n\n // Zod validation errors\n if (err?.name === 'ZodError') {\n const firstIssue = err.issues?.[0]\n return res.status(422).json({\n message: firstIssue?.message || 'Validation failed',\n ...(isProduction ? {} : { errors: err.issues }),\n })\n }\n\n // HttpException (expected application errors) — RFC 9457, same as\n // ProblemException. It used to answer a bare `{ message }`, which made it\n // THE path a client parsing `application/problem+json` had to special-case\n // (#611) — the exact situation #587 removed from the catch-all. The\n // message becomes `detail`; `title` and `type` default from the status.\n if (err instanceof HttpException) {\n if (err.status >= 500) {\n log.error(err, err.message)\n }\n if (err.headers) {\n for (const [k, v] of Object.entries(err.headers)) {\n res.setHeader(k, v)\n }\n }\n const exposeDetails = !isProduction && err.details !== undefined\n res.setHeader('Content-Type', 'application/problem+json')\n return res.status(err.status).json(\n normalizeProblem({\n status: err.status,\n detail: err.message,\n // Extension member, permitted by RFC 9457 §3.2. Held back in\n // production for the same reason as before: `details` can carry the\n // shape of the request that failed.\n ...(exposeDetails ? { errors: err.details } : {}),\n }),\n )\n }\n\n // Unexpected errors — always log\n const status = err.status || err.statusCode || 500\n const requestId = (req as any).requestId ?? req.headers['x-request-id']\n log.error(\n err,\n // `originalUrl` is Express-only. Fastify and h3 pass `request.raw`, so\n // this logged `GET undefined — <error>` on every error under those\n // runtimes — the path silently dropped from the one line meant to\n // identify the failing request.\n `${req.method} ${req.originalUrl ?? req.url} — ${describeError(err)}${\n requestId ? ` [${requestId}]` : ''\n }`,\n )\n\n if (status !== 500) {\n return res.status(status).json({\n message: err.message || 'Error',\n ...(requestId ? { requestId } : {}),\n })\n }\n\n // A 500 body must never carry the raw error in production — it can\n // contain table names, SQL, connection strings, or user data. But\n // returning a bare `{ message: 'Internal Server Error' }` in\n // development means the one place a developer is looking tells them\n // nothing, and the failure has to be re-diagnosed from the database.\n //\n // Development gets the full picture. Production gets the requestId,\n // which is the correlation handle back to the (now stack-carrying)\n // log line — without it an opaque 500 can't even be tied to its own\n // log entry.\n res.status(500).json({\n message: 'Internal Server Error',\n ...(requestId ? { requestId } : {}),\n ...(isProduction\n ? {}\n : {\n error: describeError(err),\n ...(typeof err?.stack === 'string' ? { stack: err.stack.split('\\n') } : {}),\n }),\n })\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as helmet, t as HelmetOptions } from "../../helmet-CFQUTVig.mjs";
3
- export { HelmetOptions, helmet };
2
+ import { n as HelmetOptions, r as helmet, t as HELMET_MIDDLEWARE } from "../../helmet-C2QEU6S-.mjs";
3
+ export { HELMET_MIDDLEWARE, HelmetOptions, helmet };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- function helmet(options={}){let{noSniff=!0,frameguard=`DENY`,hsts={maxAge:31536e3,includeSubDomains:!0},dnsPrefetch=!1,hidePoweredBy=!0,referrerPolicy=`no-referrer`,xssFilter=!0,contentSecurityPolicy=!1}=options;return(req,res,next)=>{if(hidePoweredBy&&res.removeHeader(`X-Powered-By`),noSniff&&res.setHeader(`X-Content-Type-Options`,`nosniff`),frameguard&&res.setHeader(`X-Frame-Options`,frameguard),xssFilter&&res.setHeader(`X-XSS-Protection`,`0`),referrerPolicy&&res.setHeader(`Referrer-Policy`,referrerPolicy),dnsPrefetch||res.setHeader(`X-DNS-Prefetch-Control`,`off`),hsts){let value=`max-age=${hsts.maxAge??31536e3}`;hsts.includeSubDomains&&(value+=`; includeSubDomains`),hsts.preload&&(value+=`; preload`),res.setHeader(`Strict-Transport-Security`,value)}if(contentSecurityPolicy){let policy=typeof contentSecurityPolicy==`object`?Object.entries(contentSecurityPolicy).map(([key,values])=>`${key} ${values.join(` `)}`).join(`; `):`default-src 'self'`;res.setHeader(`Content-Security-Policy`,policy)}next()}}export{helmet};
11
+ const HELMET_MIDDLEWARE=Symbol.for(`kick/http/helmet`);function helmet(options={}){let{noSniff=!0,frameguard=`DENY`,hsts={maxAge:31536e3,includeSubDomains:!0},dnsPrefetch=!1,hidePoweredBy=!0,referrerPolicy=`no-referrer`,xssFilter=!0,contentSecurityPolicy=!1}=options;return Object.defineProperty((req,res,next)=>{if(hidePoweredBy&&res.removeHeader(`X-Powered-By`),noSniff&&res.setHeader(`X-Content-Type-Options`,`nosniff`),frameguard&&res.setHeader(`X-Frame-Options`,frameguard),xssFilter&&res.setHeader(`X-XSS-Protection`,`0`),referrerPolicy&&res.setHeader(`Referrer-Policy`,referrerPolicy),dnsPrefetch||res.setHeader(`X-DNS-Prefetch-Control`,`off`),hsts){let value=`max-age=${hsts.maxAge??31536e3}`;hsts.includeSubDomains&&(value+=`; includeSubDomains`),hsts.preload&&(value+=`; preload`),res.setHeader(`Strict-Transport-Security`,value)}if(contentSecurityPolicy){let policy=typeof contentSecurityPolicy==`object`?Object.entries(contentSecurityPolicy).map(([key,values])=>`${key} ${values.join(` `)}`).join(`; `):`default-src 'self'`;res.setHeader(`Content-Security-Policy`,policy)}next()},HELMET_MIDDLEWARE,{value:!0})}export{HELMET_MIDDLEWARE,helmet};
12
12
  //# sourceMappingURL=helmet.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"helmet.mjs","names":[],"sources":["../../../src/http/middleware/helmet.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface HelmetOptions {\n /** Set Content-Security-Policy header (default: self-only policy) */\n contentSecurityPolicy?: boolean | Record<string, string[]>\n /** Set X-Content-Type-Options: nosniff (default: true) */\n noSniff?: boolean\n /** Set X-Frame-Options (default: 'DENY') */\n frameguard?: false | 'DENY' | 'SAMEORIGIN'\n /** Set Strict-Transport-Security (default: max-age=31536000; includeSubDomains) */\n hsts?: false | { maxAge?: number; includeSubDomains?: boolean; preload?: boolean }\n /** Set X-DNS-Prefetch-Control (default: off) */\n dnsPrefetch?: boolean\n /** Remove X-Powered-By header (default: true) */\n hidePoweredBy?: boolean\n /** Set Referrer-Policy (default: 'no-referrer') */\n referrerPolicy?: false | string\n /** Set X-XSS-Protection: 0 (default: true — disables legacy XSS auditor) */\n xssFilter?: boolean\n}\n\n/**\n * Security headers middleware. Lightweight alternative to the `helmet` npm package\n * with sensible defaults for API servers.\n *\n * @example\n * ```ts\n * bootstrap({\n * middleware: [helmet(), requestId(), express.json()],\n * })\n * ```\n */\nexport function helmet(options: HelmetOptions = {}) {\n const {\n noSniff = true,\n frameguard = 'DENY',\n hsts = { maxAge: 31536000, includeSubDomains: true },\n dnsPrefetch = false,\n hidePoweredBy = true,\n referrerPolicy = 'no-referrer',\n xssFilter = true,\n contentSecurityPolicy = false,\n } = options\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (hidePoweredBy) res.removeHeader('X-Powered-By')\n if (noSniff) res.setHeader('X-Content-Type-Options', 'nosniff')\n if (frameguard) res.setHeader('X-Frame-Options', frameguard)\n if (xssFilter) res.setHeader('X-XSS-Protection', '0')\n if (referrerPolicy) res.setHeader('Referrer-Policy', referrerPolicy)\n if (!dnsPrefetch) res.setHeader('X-DNS-Prefetch-Control', 'off')\n\n if (hsts) {\n const maxAge = hsts.maxAge ?? 31536000\n let value = `max-age=${maxAge}`\n if (hsts.includeSubDomains) value += '; includeSubDomains'\n if (hsts.preload) value += '; preload'\n res.setHeader('Strict-Transport-Security', value)\n }\n\n if (contentSecurityPolicy) {\n const policy =\n typeof contentSecurityPolicy === 'object'\n ? Object.entries(contentSecurityPolicy)\n .map(([key, values]) => `${key} ${values.join(' ')}`)\n .join('; ')\n : \"default-src 'self'\"\n res.setHeader('Content-Security-Policy', policy)\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAgCA,SAAgB,OAAO,QAAyB,CAAC,EAAG,CAClD,GAAM,CACJ,QAAU,GACV,WAAa,OACb,KAAO,CAAE,OAAQ,QAAU,kBAAmB,EAAK,EACnD,YAAc,GACd,cAAgB,GAChB,eAAiB,cACjB,UAAY,GACZ,sBAAwB,IACtB,QAEJ,OAAQ,IAAc,IAAe,OAAuB,CAQ1D,GAPI,eAAe,IAAI,aAAa,cAAc,EAC9C,SAAS,IAAI,UAAU,yBAA0B,SAAS,EAC1D,YAAY,IAAI,UAAU,kBAAmB,UAAU,EACvD,WAAW,IAAI,UAAU,mBAAoB,GAAG,EAChD,gBAAgB,IAAI,UAAU,kBAAmB,cAAc,EAC9D,aAAa,IAAI,UAAU,yBAA0B,KAAK,EAE3D,KAAM,CAER,IAAI,MAAQ,WADG,KAAK,QAAU,UAE1B,KAAK,oBAAmB,OAAS,uBACjC,KAAK,UAAS,OAAS,aAC3B,IAAI,UAAU,4BAA6B,KAAK,CAClD,CAEA,GAAI,sBAAuB,CACzB,IAAM,OACJ,OAAO,uBAA0B,SAC7B,OAAO,QAAQ,qBAAqB,CAAC,CAClC,KAAK,CAAC,IAAK,UAAY,GAAG,IAAI,GAAG,OAAO,KAAK,GAAG,GAAG,CAAC,CACpD,KAAK,IAAI,EACZ,qBACN,IAAI,UAAU,0BAA2B,MAAM,CACjD,CAEA,KAAK,CACP,CACF"}
1
+ {"version":3,"file":"helmet.mjs","names":[],"sources":["../../../src/http/middleware/helmet.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\n/** Brand identifying a `helmet()` handler, so auto-injection can stand down. */\nexport const HELMET_MIDDLEWARE = Symbol.for('kick/http/helmet')\n\nexport interface HelmetOptions {\n /** Set Content-Security-Policy header (default: self-only policy) */\n contentSecurityPolicy?: boolean | Record<string, string[]>\n /** Set X-Content-Type-Options: nosniff (default: true) */\n noSniff?: boolean\n /** Set X-Frame-Options (default: 'DENY') */\n frameguard?: false | 'DENY' | 'SAMEORIGIN'\n /** Set Strict-Transport-Security (default: max-age=31536000; includeSubDomains) */\n hsts?: false | { maxAge?: number; includeSubDomains?: boolean; preload?: boolean }\n /** Set X-DNS-Prefetch-Control (default: off) */\n dnsPrefetch?: boolean\n /** Remove X-Powered-By header (default: true) */\n hidePoweredBy?: boolean\n /** Set Referrer-Policy (default: 'no-referrer') */\n referrerPolicy?: false | string\n /** Set X-XSS-Protection: 0 (default: true — disables legacy XSS auditor) */\n xssFilter?: boolean\n}\n\n/**\n * Security headers middleware. Lightweight alternative to the `helmet` npm package\n * with sensible defaults for API servers.\n *\n * @example\n * ```ts\n * bootstrap({\n * middlewares: [helmet(), requestId(), express.json()],\n * })\n * ```\n */\nexport function helmet(options: HelmetOptions = {}) {\n const {\n noSniff = true,\n frameguard = 'DENY',\n hsts = { maxAge: 31536000, includeSubDomains: true },\n dnsPrefetch = false,\n hidePoweredBy = true,\n referrerPolicy = 'no-referrer',\n xssFilter = true,\n contentSecurityPolicy = false,\n } = options\n\n const handler = (req: Request, res: Response, next: NextFunction) => {\n if (hidePoweredBy) res.removeHeader('X-Powered-By')\n if (noSniff) res.setHeader('X-Content-Type-Options', 'nosniff')\n if (frameguard) res.setHeader('X-Frame-Options', frameguard)\n if (xssFilter) res.setHeader('X-XSS-Protection', '0')\n if (referrerPolicy) res.setHeader('Referrer-Policy', referrerPolicy)\n if (!dnsPrefetch) res.setHeader('X-DNS-Prefetch-Control', 'off')\n\n if (hsts) {\n const maxAge = hsts.maxAge ?? 31536000\n let value = `max-age=${maxAge}`\n if (hsts.includeSubDomains) value += '; includeSubDomains'\n if (hsts.preload) value += '; preload'\n res.setHeader('Strict-Transport-Security', value)\n }\n\n if (contentSecurityPolicy) {\n const policy =\n typeof contentSecurityPolicy === 'object'\n ? Object.entries(contentSecurityPolicy)\n .map(([key, values]) => `${key} ${values.join(' ')}`)\n .join('; ')\n : \"default-src 'self'\"\n res.setHeader('Content-Security-Policy', policy)\n }\n\n next()\n }\n\n // Brand the handler so the Application skips its own auto-injection. Without\n // that, auto-helmet runs first with defaults and a user helmet can only ADD\n // headers — an explicit `frameguard: false` would leave the auto-injected\n // `X-Frame-Options: DENY` standing.\n return Object.defineProperty(handler, HELMET_MIDDLEWARE, { value: true })\n}\n"],"mappings":";;;;;;;;;;AAGA,MAAa,kBAAoB,OAAO,IAAI,kBAAkB,EAgC9D,SAAgB,OAAO,QAAyB,CAAC,EAAG,CAClD,GAAM,CACJ,QAAU,GACV,WAAa,OACb,KAAO,CAAE,OAAQ,QAAU,kBAAmB,EAAK,EACnD,YAAc,GACd,cAAgB,GAChB,eAAiB,cACjB,UAAY,GACZ,sBAAwB,IACtB,QAmCJ,OAAO,OAAO,gBAjCG,IAAc,IAAe,OAAuB,CAQnE,GAPI,eAAe,IAAI,aAAa,cAAc,EAC9C,SAAS,IAAI,UAAU,yBAA0B,SAAS,EAC1D,YAAY,IAAI,UAAU,kBAAmB,UAAU,EACvD,WAAW,IAAI,UAAU,mBAAoB,GAAG,EAChD,gBAAgB,IAAI,UAAU,kBAAmB,cAAc,EAC9D,aAAa,IAAI,UAAU,yBAA0B,KAAK,EAE3D,KAAM,CAER,IAAI,MAAQ,WADG,KAAK,QAAU,UAE1B,KAAK,oBAAmB,OAAS,uBACjC,KAAK,UAAS,OAAS,aAC3B,IAAI,UAAU,4BAA6B,KAAK,CAClD,CAEA,GAAI,sBAAuB,CACzB,IAAM,OACJ,OAAO,uBAA0B,SAC7B,OAAO,QAAQ,qBAAqB,CAAC,CAClC,KAAK,CAAC,IAAK,UAAY,GAAG,IAAI,GAAG,OAAO,KAAK,GAAG,GAAG,CAAC,CACpD,KAAK,IAAI,EACZ,qBACN,IAAI,UAAU,0BAA2B,MAAM,CACjD,CAEA,KAAK,CACP,EAMsC,kBAAmB,CAAE,MAAO,EAAK,CAAC,CAC1E"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as RateLimitStore, r as rateLimit, t as RateLimitOptions } from "../../rate-limit-Dme747JJ.mjs";
2
+ import { n as RateLimitStore, r as rateLimit, t as RateLimitOptions } from "../../rate-limit-D_cKx6Uw.mjs";
3
3
  export { RateLimitOptions, RateLimitStore, rateLimit };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{r as registerDisposable}from"../../disposables-Bwm-JdIp.mjs";import{n as resolvePathname,t as resolveClientIp}from"../../client-ip-De2ynoAd.mjs";var MemoryStore=class{windowMs;hits=new Map;cleanupTimer;constructor(windowMs){this.windowMs=windowMs,this.cleanupTimer=setInterval(()=>this.cleanup(),windowMs),this.cleanupTimer.unref&&this.cleanupTimer.unref(),registerDisposable(()=>this.dispose())}dispose(){clearInterval(this.cleanupTimer),this.hits.clear()}async increment(key){let now=Date.now(),entry=this.hits.get(key);if(entry&&entry.resetTime.getTime()>now)return entry.totalHits++,{totalHits:entry.totalHits,resetTime:entry.resetTime};let resetTime=new Date(now+this.windowMs),newEntry={totalHits:1,resetTime};return this.hits.set(key,newEntry),{totalHits:1,resetTime}}async decrement(key){let entry=this.hits.get(key);entry&&entry.totalHits>0&&entry.totalHits--}async reset(key){this.hits.delete(key)}cleanup(){let now=Date.now();for(let[key,entry]of this.hits)entry.resetTime.getTime()<=now&&this.hits.delete(key)}};function rateLimit(options={}){let max=options.max??100,windowMs=options.windowMs??6e4,message=options.message??`Too Many Requests`,statusCode=options.statusCode??429,keyGenerator=options.keyGenerator??(req=>resolveClientIp(req)??`global`),sendHeaders=options.headers??!0,store=options.store??new MemoryStore(windowMs),skip=options.skip,skipPaths=new Set(options.skipPaths??[]);return async(req,res,next)=>{if(skipPaths.has(resolvePathname(req))||skip&&skip(req))return next();let key=keyGenerator(req),{totalHits,resetTime}=await store.increment(key),remaining=Math.max(0,max-totalHits);if(sendHeaders&&(res.setHeader(`RateLimit-Limit`,max),res.setHeader(`RateLimit-Remaining`,remaining),res.setHeader(`RateLimit-Reset`,Math.ceil(resetTime.getTime()/1e3))),totalHits>max)return res.status(statusCode).json({message});next()}}export{rateLimit};
11
+ import{r as registerDisposable}from"../../disposables-jJAxz_e1.mjs";import{n as resolvePathname,t as resolveClientIp}from"../../client-ip-D_XUCo45.mjs";import{r as sendJson}from"../../respond-C6fe709p.mjs";var MemoryStore=class{windowMs;hits=new Map;cleanupTimer;constructor(windowMs){this.windowMs=windowMs,this.cleanupTimer=setInterval(()=>this.cleanup(),windowMs),this.cleanupTimer.unref&&this.cleanupTimer.unref(),registerDisposable(()=>this.dispose())}dispose(){clearInterval(this.cleanupTimer),this.hits.clear()}async increment(key){let now=Date.now(),entry=this.hits.get(key);if(entry&&entry.resetTime.getTime()>now)return entry.totalHits++,{totalHits:entry.totalHits,resetTime:entry.resetTime};let resetTime=new Date(now+this.windowMs),newEntry={totalHits:1,resetTime};return this.hits.set(key,newEntry),{totalHits:1,resetTime}}async decrement(key){let entry=this.hits.get(key);entry&&entry.totalHits>0&&entry.totalHits--}async reset(key){this.hits.delete(key)}cleanup(){let now=Date.now();for(let[key,entry]of this.hits)entry.resetTime.getTime()<=now&&this.hits.delete(key)}};function rateLimit(options={}){let max=options.max??100,windowMs=options.windowMs??6e4,message=options.message??`Too Many Requests`,statusCode=options.statusCode??429,keyGenerator=options.keyGenerator??(req=>resolveClientIp(req)??`global`),sendHeaders=options.headers??!0,store=options.store??new MemoryStore(windowMs),skip=options.skip,skipPaths=new Set(options.skipPaths??[]);return async(req,res,next)=>{if(skipPaths.has(resolvePathname(req))||skip&&skip(req))return next();let key=keyGenerator(req),{totalHits,resetTime}=await store.increment(key),remaining=Math.max(0,max-totalHits);if(sendHeaders&&(res.setHeader(`RateLimit-Limit`,max),res.setHeader(`RateLimit-Remaining`,remaining),res.setHeader(`RateLimit-Reset`,Math.ceil(resetTime.getTime()/1e3))),totalHits>max)return sendJson(res,statusCode,{message});next()}}export{rateLimit};
12
12
  //# sourceMappingURL=rate-limit.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../../src/http/middleware/rate-limit.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { resolveClientIp, resolvePathname, type ClientRequestLike } from '../client-ip'\n\nimport { registerDisposable } from '../../core/disposables'\n\nexport interface RateLimitStore {\n increment(key: string): Promise<{ totalHits: number; resetTime: Date }>\n decrement(key: string): Promise<void>\n reset(key: string): Promise<void>\n}\n\nexport interface RateLimitOptions {\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 rate limit is exceeded (default: 'Too Many Requests') */\n message?: string\n /** HTTP status code when rate limit is exceeded (default: 429) */\n statusCode?: number\n /** Function to generate the key for rate limiting (default: req.ip) */\n keyGenerator?: (req: Request) => string\n /** Whether to send rate limit headers (default: true) */\n headers?: boolean\n /** Custom store implementation (default: in-memory Map) */\n store?: RateLimitStore\n /** Function to skip rate limiting for certain requests */\n skip?: (req: Request) => boolean\n /** Paths to exclude from rate limiting */\n skipPaths?: string[]\n}\n\ninterface MemoryStoreEntry {\n totalHits: number\n resetTime: Date\n}\n\nclass MemoryStore implements RateLimitStore {\n private hits = new Map<string, MemoryStoreEntry>()\n private cleanupTimer: ReturnType<typeof setInterval>\n\n constructor(private windowMs: number) {\n this.cleanupTimer = setInterval(() => this.cleanup(), windowMs)\n // Allow the process to exit without waiting for the timer\n if (this.cleanupTimer.unref) {\n this.cleanupTimer.unref()\n }\n // Symmetric teardown: Application.shutdown() drains the disposables\n // registry — without this, every in-process app re-create (tests, HMR)\n // leaked one live interval + its hits Map.\n registerDisposable(() => this.dispose())\n }\n\n /** Clear the cleanup interval and drop all hit counters. */\n dispose(): void {\n clearInterval(this.cleanupTimer)\n this.hits.clear()\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n\n if (entry && entry.resetTime.getTime() > now) {\n entry.totalHits++\n return { totalHits: entry.totalHits, resetTime: entry.resetTime }\n }\n\n const resetTime = new Date(now + this.windowMs)\n const newEntry: MemoryStoreEntry = { totalHits: 1, resetTime }\n this.hits.set(key, newEntry)\n return { totalHits: 1, resetTime }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.totalHits > 0) {\n entry.totalHits--\n }\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n\n private cleanup(): void {\n const now = Date.now()\n for (const [key, entry] of this.hits) {\n if (entry.resetTime.getTime() <= now) {\n this.hits.delete(key)\n }\n }\n }\n}\n\n/**\n * Rate limiting middleware.\n *\n * Limits the number of requests a client can make within a time window.\n * Uses an in-memory store by default, but accepts a custom store for\n * distributed deployments (e.g. Redis).\n *\n * @example\n * ```ts\n * import { rateLimit } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * rateLimit({ max: 100, windowMs: 60_000 }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function rateLimit(options: RateLimitOptions = {}) {\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 // `req.ip` is Express-only. Under Fastify and h3 it is undefined, so EVERY\n // caller fell back to the same `'127.0.0.1'` literal — one shared bucket,\n // letting a single client exhaust the limit for everyone.\n const keyGenerator =\n options.keyGenerator ??\n ((req: Request) => resolveClientIp(req as ClientRequestLike) ?? 'global')\n const sendHeaders = options.headers ?? true\n const store = options.store ?? new MemoryStore(windowMs)\n const skip = options.skip\n const skipPaths = new Set(options.skipPaths ?? [])\n\n return async (req: Request, res: Response, next: NextFunction) => {\n // Skip if path is in the skip list\n // `req.path` is Express-only; `has(undefined)` never matched, so\n // configured skips were silently dead on the other runtimes.\n if (skipPaths.has(resolvePathname(req as ClientRequestLike))) {\n return next()\n }\n\n // Skip if the skip function returns true\n if (skip && skip(req)) {\n return next()\n }\n\n const key = keyGenerator(req)\n const { totalHits, resetTime } = await store.increment(key)\n const remaining = Math.max(0, max - totalHits)\n\n if (sendHeaders) {\n res.setHeader('RateLimit-Limit', max)\n res.setHeader('RateLimit-Remaining', remaining)\n res.setHeader('RateLimit-Reset', Math.ceil(resetTime.getTime() / 1000))\n }\n\n if (totalHits > max) {\n return res.status(statusCode).json({ message })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;wJAqCA,IAAM,YAAN,KAA4C,CAItB,SAHpB,KAAe,IAAI,IACnB,aAEA,YAAY,SAA0B,CAAlB,KAAA,SAAA,SAClB,KAAK,aAAe,gBAAkB,KAAK,QAAQ,EAAG,QAAQ,EAE1D,KAAK,aAAa,OACpB,KAAK,aAAa,MAAM,EAK1B,uBAAyB,KAAK,QAAQ,CAAC,CACzC,CAGA,SAAgB,CACd,cAAc,KAAK,YAAY,EAC/B,KAAK,KAAK,MAAM,CAClB,CAEA,MAAM,UAAU,IAA8D,CAC5E,IAAM,IAAM,KAAK,IAAI,EACf,MAAQ,KAAK,KAAK,IAAI,GAAG,EAE/B,GAAI,OAAS,MAAM,UAAU,QAAQ,EAAI,IAEvC,MADA,OAAM,YACC,CAAE,UAAW,MAAM,UAAW,UAAW,MAAM,SAAU,EAGlE,IAAM,UAAY,IAAI,KAAK,IAAM,KAAK,QAAQ,EACxC,SAA6B,CAAE,UAAW,EAAG,SAAU,EAE7D,OADA,KAAK,KAAK,IAAI,IAAK,QAAQ,EACpB,CAAE,UAAW,EAAG,SAAU,CACnC,CAEA,MAAM,UAAU,IAA4B,CAC1C,IAAM,MAAQ,KAAK,KAAK,IAAI,GAAG,EAC3B,OAAS,MAAM,UAAY,GAC7B,MAAM,WAEV,CAEA,MAAM,MAAM,IAA4B,CACtC,KAAK,KAAK,OAAO,GAAG,CACtB,CAEA,SAAwB,CACtB,IAAM,IAAM,KAAK,IAAI,EACrB,IAAK,GAAM,CAAC,IAAK,SAAU,KAAK,KAC1B,MAAM,UAAU,QAAQ,GAAK,KAC/B,KAAK,KAAK,OAAO,GAAG,CAG1B,CACF,EAsBA,SAAgB,UAAU,QAA4B,CAAC,EAAG,CACxD,IAAM,IAAM,QAAQ,KAAO,IACrB,SAAW,QAAQ,UAAY,IAC/B,QAAU,QAAQ,SAAW,oBAC7B,WAAa,QAAQ,YAAc,IAInC,aACJ,QAAQ,eACN,KAAiB,gBAAgB,GAAwB,GAAK,UAC5D,YAAc,QAAQ,SAAW,GACjC,MAAQ,QAAQ,OAAS,IAAI,YAAY,QAAQ,EACjD,KAAO,QAAQ,KACf,UAAY,IAAI,IAAI,QAAQ,WAAa,CAAC,CAAC,EAEjD,OAAO,MAAO,IAAc,IAAe,OAAuB,CAShE,GALI,UAAU,IAAI,gBAAgB,GAAwB,CAAC,GAKvD,MAAQ,KAAK,GAAG,EAClB,OAAO,KAAK,EAGd,IAAM,IAAM,aAAa,GAAG,EACtB,CAAE,UAAW,WAAc,MAAM,MAAM,UAAU,GAAG,EACpD,UAAY,KAAK,IAAI,EAAG,IAAM,SAAS,EAQ7C,GANI,cACF,IAAI,UAAU,kBAAmB,GAAG,EACpC,IAAI,UAAU,sBAAuB,SAAS,EAC9C,IAAI,UAAU,kBAAmB,KAAK,KAAK,UAAU,QAAQ,EAAI,GAAI,CAAC,GAGpE,UAAY,IACd,OAAO,IAAI,OAAO,UAAU,CAAC,CAAC,KAAK,CAAE,OAAQ,CAAC,EAGhD,KAAK,CACP,CACF"}
1
+ {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../../src/http/middleware/rate-limit.ts"],"sourcesContent":["import { sendJson } from './respond'\nimport type { Request, Response, NextFunction } from 'express'\nimport { resolveClientIp, resolvePathname, type ClientRequestLike } from '../client-ip'\n\nimport { registerDisposable } from '../../core/disposables'\n\nexport interface RateLimitStore {\n increment(key: string): Promise<{ totalHits: number; resetTime: Date }>\n decrement(key: string): Promise<void>\n reset(key: string): Promise<void>\n}\n\nexport interface RateLimitOptions {\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 rate limit is exceeded (default: 'Too Many Requests') */\n message?: string\n /** HTTP status code when rate limit is exceeded (default: 429) */\n statusCode?: number\n /** Function to generate the key for rate limiting (default: req.ip) */\n keyGenerator?: (req: Request) => string\n /** Whether to send rate limit headers (default: true) */\n headers?: boolean\n /** Custom store implementation (default: in-memory Map) */\n store?: RateLimitStore\n /** Function to skip rate limiting for certain requests */\n skip?: (req: Request) => boolean\n /** Paths to exclude from rate limiting */\n skipPaths?: string[]\n}\n\ninterface MemoryStoreEntry {\n totalHits: number\n resetTime: Date\n}\n\nclass MemoryStore implements RateLimitStore {\n private hits = new Map<string, MemoryStoreEntry>()\n private cleanupTimer: ReturnType<typeof setInterval>\n\n constructor(private windowMs: number) {\n this.cleanupTimer = setInterval(() => this.cleanup(), windowMs)\n // Allow the process to exit without waiting for the timer\n if (this.cleanupTimer.unref) {\n this.cleanupTimer.unref()\n }\n // Symmetric teardown: Application.shutdown() drains the disposables\n // registry — without this, every in-process app re-create (tests, HMR)\n // leaked one live interval + its hits Map.\n registerDisposable(() => this.dispose())\n }\n\n /** Clear the cleanup interval and drop all hit counters. */\n dispose(): void {\n clearInterval(this.cleanupTimer)\n this.hits.clear()\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n\n if (entry && entry.resetTime.getTime() > now) {\n entry.totalHits++\n return { totalHits: entry.totalHits, resetTime: entry.resetTime }\n }\n\n const resetTime = new Date(now + this.windowMs)\n const newEntry: MemoryStoreEntry = { totalHits: 1, resetTime }\n this.hits.set(key, newEntry)\n return { totalHits: 1, resetTime }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.totalHits > 0) {\n entry.totalHits--\n }\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n\n private cleanup(): void {\n const now = Date.now()\n for (const [key, entry] of this.hits) {\n if (entry.resetTime.getTime() <= now) {\n this.hits.delete(key)\n }\n }\n }\n}\n\n/**\n * Rate limiting middleware.\n *\n * Limits the number of requests a client can make within a time window.\n * Uses an in-memory store by default, but accepts a custom store for\n * distributed deployments (e.g. Redis).\n *\n * @example\n * ```ts\n * import { rateLimit } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middlewares: [\n * rateLimit({ max: 100, windowMs: 60_000 }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function rateLimit(options: RateLimitOptions = {}) {\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 // `req.ip` is Express-only. Under Fastify and h3 it is undefined, so EVERY\n // caller fell back to the same `'127.0.0.1'` literal — one shared bucket,\n // letting a single client exhaust the limit for everyone.\n const keyGenerator =\n options.keyGenerator ??\n ((req: Request) => resolveClientIp(req as ClientRequestLike) ?? 'global')\n const sendHeaders = options.headers ?? true\n const store = options.store ?? new MemoryStore(windowMs)\n const skip = options.skip\n const skipPaths = new Set(options.skipPaths ?? [])\n\n return async (req: Request, res: Response, next: NextFunction) => {\n // Skip if path is in the skip list\n // `req.path` is Express-only; `has(undefined)` never matched, so\n // configured skips were silently dead on the other runtimes.\n if (skipPaths.has(resolvePathname(req as ClientRequestLike))) {\n return next()\n }\n\n // Skip if the skip function returns true\n if (skip && skip(req)) {\n return next()\n }\n\n const key = keyGenerator(req)\n const { totalHits, resetTime } = await store.increment(key)\n const remaining = Math.max(0, max - totalHits)\n\n if (sendHeaders) {\n res.setHeader('RateLimit-Limit', max)\n res.setHeader('RateLimit-Remaining', remaining)\n res.setHeader('RateLimit-Reset', Math.ceil(resetTime.getTime() / 1000))\n }\n\n if (totalHits > max) {\n return sendJson(res, statusCode, { message })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;8MAsCA,IAAM,YAAN,KAA4C,CAItB,SAHpB,KAAe,IAAI,IACnB,aAEA,YAAY,SAA0B,CAAlB,KAAA,SAAA,SAClB,KAAK,aAAe,gBAAkB,KAAK,QAAQ,EAAG,QAAQ,EAE1D,KAAK,aAAa,OACpB,KAAK,aAAa,MAAM,EAK1B,uBAAyB,KAAK,QAAQ,CAAC,CACzC,CAGA,SAAgB,CACd,cAAc,KAAK,YAAY,EAC/B,KAAK,KAAK,MAAM,CAClB,CAEA,MAAM,UAAU,IAA8D,CAC5E,IAAM,IAAM,KAAK,IAAI,EACf,MAAQ,KAAK,KAAK,IAAI,GAAG,EAE/B,GAAI,OAAS,MAAM,UAAU,QAAQ,EAAI,IAEvC,MADA,OAAM,YACC,CAAE,UAAW,MAAM,UAAW,UAAW,MAAM,SAAU,EAGlE,IAAM,UAAY,IAAI,KAAK,IAAM,KAAK,QAAQ,EACxC,SAA6B,CAAE,UAAW,EAAG,SAAU,EAE7D,OADA,KAAK,KAAK,IAAI,IAAK,QAAQ,EACpB,CAAE,UAAW,EAAG,SAAU,CACnC,CAEA,MAAM,UAAU,IAA4B,CAC1C,IAAM,MAAQ,KAAK,KAAK,IAAI,GAAG,EAC3B,OAAS,MAAM,UAAY,GAC7B,MAAM,WAEV,CAEA,MAAM,MAAM,IAA4B,CACtC,KAAK,KAAK,OAAO,GAAG,CACtB,CAEA,SAAwB,CACtB,IAAM,IAAM,KAAK,IAAI,EACrB,IAAK,GAAM,CAAC,IAAK,SAAU,KAAK,KAC1B,MAAM,UAAU,QAAQ,GAAK,KAC/B,KAAK,KAAK,OAAO,GAAG,CAG1B,CACF,EAsBA,SAAgB,UAAU,QAA4B,CAAC,EAAG,CACxD,IAAM,IAAM,QAAQ,KAAO,IACrB,SAAW,QAAQ,UAAY,IAC/B,QAAU,QAAQ,SAAW,oBAC7B,WAAa,QAAQ,YAAc,IAInC,aACJ,QAAQ,eACN,KAAiB,gBAAgB,GAAwB,GAAK,UAC5D,YAAc,QAAQ,SAAW,GACjC,MAAQ,QAAQ,OAAS,IAAI,YAAY,QAAQ,EACjD,KAAO,QAAQ,KACf,UAAY,IAAI,IAAI,QAAQ,WAAa,CAAC,CAAC,EAEjD,OAAO,MAAO,IAAc,IAAe,OAAuB,CAShE,GALI,UAAU,IAAI,gBAAgB,GAAwB,CAAC,GAKvD,MAAQ,KAAK,GAAG,EAClB,OAAO,KAAK,EAGd,IAAM,IAAM,aAAa,GAAG,EACtB,CAAE,UAAW,WAAc,MAAM,MAAM,UAAU,GAAG,EACpD,UAAY,KAAK,IAAI,EAAG,IAAM,SAAS,EAQ7C,GANI,cACF,IAAI,UAAU,kBAAmB,GAAG,EACpC,IAAI,UAAU,sBAAuB,SAAS,EAC9C,IAAI,UAAU,kBAAmB,KAAK,KAAK,UAAU,QAAQ,EAAI,GAAI,CAAC,GAGpE,UAAY,IACd,OAAO,SAAS,IAAK,WAAY,CAAE,OAAQ,CAAC,EAG9C,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as requestLogger, t as RequestLoggerOptions } from "../../request-logger-D_pf4xzE.mjs";
3
- export { RequestLoggerOptions, requestLogger };
2
+ import { i as requestLogger, n as LoggedResponse, r as RequestLoggerOptions, t as LoggedRequest } from "../../request-logger-TgcKRNDD.mjs";
3
+ export { LoggedRequest, LoggedResponse, RequestLoggerOptions, requestLogger };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{createLogger}from"../../core/logger.mjs";import"../../core-BGsV3RjD.mjs";function pathnameOf(url){if(!url)return`/`;let q=url.indexOf(`?`);return(q===-1?url:url.slice(0,q))||`/`}function requestLogger(options={}){let log=createLogger(options.name??`HTTP`),level=options.level??`info`,skip=options.skip??[];return(req,res,next)=>{let pathname=req.path??pathnameOf(req.url);if(skip.some(prefix=>pathname.startsWith(prefix)))return next();let start=Date.now();res.on(`finish`,()=>{let duration=Date.now()-start,requestId=req.requestId||req.headers[`x-request-id`]||`-`,status=res.statusCode,path=req.originalUrl??req.url??`/`;log[level](`${req.method} ${path} ${status} ${duration}ms ${requestId}`)}),next()}}export{requestLogger};
11
+ import{createLogger}from"../../core/logger.mjs";import"../../core-B1PK7S5p.mjs";function pathnameOf(url){if(!url)return`/`;let q=url.indexOf(`?`);return(q===-1?url:url.slice(0,q))||`/`}function requestLogger(options={}){let log=createLogger(options.name??`HTTP`),level=options.level??`info`,skip=options.skip??[];return(req,res,next)=>{let pathname=req.path??pathnameOf(req.url);if(skip.some(prefix=>pathname.startsWith(prefix)))return next();let start=Date.now();res.on(`finish`,()=>{let duration=Date.now()-start,requestId=req.requestId||req.headers[`x-request-id`]||`-`,status=res.statusCode,path=req.originalUrl??req.url??`/`;log[level](`${req.method} ${path} ${status} ${duration}ms ${requestId}`)}),next()}}export{requestLogger};
12
12
  //# sourceMappingURL=request-logger.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-logger.mjs","names":[],"sources":["../../../src/http/middleware/request-logger.ts"],"sourcesContent":["/**\n * The slice of the request/response this logger touches, from node's shapes\n * rather than Express's. This runs under EVERY runtime — Fastify and h3 hand\n * connect middleware the raw node objects — so anything Express-only is\n * `undefined` there.\n */\ninterface LoggedRequest {\n method?: string\n url?: string\n /** Express-only convenience; the raw node request has only `url`. */\n path?: string\n /** Express-only; absent on the raw node request the other runtimes pass. */\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n}\n\ninterface LoggedResponse {\n statusCode: number\n on(event: 'finish', listener: () => void): unknown\n}\n\n/** Pathname only — `req.url` carries the query string. */\nfunction pathnameOf(url: string | undefined): string {\n if (!url) return '/'\n const q = url.indexOf('?')\n return (q === -1 ? url : url.slice(0, q)) || '/'\n}\nimport { createLogger } from '../../core'\n\nexport interface RequestLoggerOptions {\n /** Logger name (default: 'HTTP') */\n name?: string\n /** Log level for successful requests (default: 'info') */\n level?: 'info' | 'debug' | 'trace'\n /** Skip logging for paths matching these prefixes (e.g. ['/health', '/_debug']) */\n skip?: string[]\n}\n\n/**\n * Middleware that logs every request with method, URL, status, duration, and request ID.\n *\n * @example\n * ```ts\n * bootstrap({\n * middleware: [requestId(), requestLogger(), express.json()],\n * })\n * ```\n *\n * Output:\n * ```\n * [HTTP] GET /api/v1/users 200 12ms req-abc123\n * [HTTP] POST /api/v1/users 201 45ms req-def456\n * ```\n */\nexport function requestLogger(options: RequestLoggerOptions = {}) {\n const log = createLogger(options.name ?? 'HTTP')\n const level = options.level ?? 'info'\n const skip = options.skip ?? []\n\n return (req: LoggedRequest, res: LoggedResponse, next: () => void) => {\n // `req.path` is Express-only. Under Fastify and h3 it is `undefined`, so\n // this threw `Cannot read properties of undefined (reading 'startsWith')`\n // on EVERY request as soon as a `skip` prefix was configured — the\n // middleware crashed the request rather than declining to log it.\n const pathname = req.path ?? pathnameOf(req.url)\n if (skip.some((prefix) => pathname.startsWith(prefix))) {\n return next()\n }\n\n const start = Date.now()\n\n res.on('finish', () => {\n const duration = Date.now() - start\n const requestId = (req as any).requestId || req.headers['x-request-id'] || '-'\n const status = res.statusCode\n\n // `originalUrl` is Express-only — on Fastify and h3 this logged\n // `GET undefined 200 12ms`, dropping the path from every access-log\n // line under those runtimes.\n // Both are optional, so the pair still needs a floor — otherwise the\n // line reads `GET undefined` again, just via a different route.\n const path = req.originalUrl ?? req.url ?? '/'\n log[level](`${req.method} ${path} ${status} ${duration}ms ${requestId}`)\n })\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;gFAsBA,SAAS,WAAW,IAAiC,CACnD,GAAI,CAAC,IAAK,MAAO,IACjB,IAAM,EAAI,IAAI,QAAQ,GAAG,EACzB,OAAQ,IAAM,GAAK,IAAM,IAAI,MAAM,EAAG,CAAC,IAAM,GAC/C,CA4BA,SAAgB,cAAc,QAAgC,CAAC,EAAG,CAChE,IAAM,IAAM,aAAa,QAAQ,MAAQ,MAAM,EACzC,MAAQ,QAAQ,OAAS,OACzB,KAAO,QAAQ,MAAQ,CAAC,EAE9B,OAAQ,IAAoB,IAAqB,OAAqB,CAKpE,IAAM,SAAW,IAAI,MAAQ,WAAW,IAAI,GAAG,EAC/C,GAAI,KAAK,KAAM,QAAW,SAAS,WAAW,MAAM,CAAC,EACnD,OAAO,KAAK,EAGd,IAAM,MAAQ,KAAK,IAAI,EAEvB,IAAI,GAAG,aAAgB,CACrB,IAAM,SAAW,KAAK,IAAI,EAAI,MACxB,UAAa,IAAY,WAAa,IAAI,QAAQ,iBAAmB,IACrE,OAAS,IAAI,WAOb,KAAO,IAAI,aAAe,IAAI,KAAO,IAC3C,IAAI,MAAM,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,SAAS,KAAK,WAAW,CACzE,CAAC,EAED,KAAK,CACP,CACF"}
1
+ {"version":3,"file":"request-logger.mjs","names":[],"sources":["../../../src/http/middleware/request-logger.ts"],"sourcesContent":["/**\n * The slice of the request/response this logger touches, from node's shapes\n * rather than Express's. This runs under EVERY runtime — Fastify and h3 hand\n * connect middleware the raw node objects — so anything Express-only is\n * `undefined` there.\n */\n/**\n * The request shape this middleware reads.\n *\n * Exported because it appears in the returned handler's signature: a consumer\n * following the documented `src/middleware/index.ts` layout exports the\n * middleware array with an inferred type, and a type the entry does not\n * re-export cannot be named there — `TS4023: Exported variable 'middleware'\n * has or is using name 'LoggedRequest' … but cannot be named`.\n *\n * Deliberately structural rather than Express's `Request`: the Fastify and h3\n * runtimes pass a raw Node request, which has `url` but no `path`.\n */\nexport interface LoggedRequest {\n method?: string\n url?: string\n /** Express-only convenience; the raw node request has only `url`. */\n path?: string\n /** Express-only; absent on the raw node request the other runtimes pass. */\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n}\n\n/** The response shape this middleware reads. Exported for the same reason as {@link LoggedRequest}. */\nexport interface LoggedResponse {\n statusCode: number\n on(event: 'finish', listener: () => void): unknown\n}\n\n/** Pathname only — `req.url` carries the query string. */\nfunction pathnameOf(url: string | undefined): string {\n if (!url) return '/'\n const q = url.indexOf('?')\n return (q === -1 ? url : url.slice(0, q)) || '/'\n}\nimport { createLogger } from '../../core'\n\nexport interface RequestLoggerOptions {\n /** Logger name (default: 'HTTP') */\n name?: string\n /** Log level for successful requests (default: 'info') */\n level?: 'info' | 'debug' | 'trace'\n /** Skip logging for paths matching these prefixes (e.g. ['/health', '/_debug']) */\n skip?: string[]\n}\n\n/**\n * Middleware that logs every request with method, URL, status, duration, and request ID.\n *\n * @example\n * ```ts\n * bootstrap({\n * middlewares: [requestId(), requestLogger(), express.json()],\n * })\n * ```\n *\n * Output:\n * ```\n * [HTTP] GET /api/v1/users 200 12ms req-abc123\n * [HTTP] POST /api/v1/users 201 45ms req-def456\n * ```\n */\nexport function requestLogger(options: RequestLoggerOptions = {}) {\n const log = createLogger(options.name ?? 'HTTP')\n const level = options.level ?? 'info'\n const skip = options.skip ?? []\n\n return (req: LoggedRequest, res: LoggedResponse, next: () => void) => {\n // `req.path` is Express-only. Under Fastify and h3 it is `undefined`, so\n // this threw `Cannot read properties of undefined (reading 'startsWith')`\n // on EVERY request as soon as a `skip` prefix was configured — the\n // middleware crashed the request rather than declining to log it.\n const pathname = req.path ?? pathnameOf(req.url)\n if (skip.some((prefix) => pathname.startsWith(prefix))) {\n return next()\n }\n\n const start = Date.now()\n\n res.on('finish', () => {\n const duration = Date.now() - start\n const requestId = (req as any).requestId || req.headers['x-request-id'] || '-'\n const status = res.statusCode\n\n // `originalUrl` is Express-only — on Fastify and h3 this logged\n // `GET undefined 200 12ms`, dropping the path from every access-log\n // line under those runtimes.\n // Both are optional, so the pair still needs a floor — otherwise the\n // line reads `GET undefined` again, just via a different route.\n const path = req.originalUrl ?? req.url ?? '/'\n log[level](`${req.method} ${path} ${status} ${duration}ms ${requestId}`)\n })\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;gFAmCA,SAAS,WAAW,IAAiC,CACnD,GAAI,CAAC,IAAK,MAAO,IACjB,IAAM,EAAI,IAAI,QAAQ,GAAG,EACzB,OAAQ,IAAM,GAAK,IAAM,IAAI,MAAM,EAAG,CAAC,IAAM,GAC/C,CA4BA,SAAgB,cAAc,QAAgC,CAAC,EAAG,CAChE,IAAM,IAAM,aAAa,QAAQ,MAAQ,MAAM,EACzC,MAAQ,QAAQ,OAAS,OACzB,KAAO,QAAQ,MAAQ,CAAC,EAE9B,OAAQ,IAAoB,IAAqB,OAAqB,CAKpE,IAAM,SAAW,IAAI,MAAQ,WAAW,IAAI,GAAG,EAC/C,GAAI,KAAK,KAAM,QAAW,SAAS,WAAW,MAAM,CAAC,EACnD,OAAO,KAAK,EAGd,IAAM,MAAQ,KAAK,IAAI,EAEvB,IAAI,GAAG,aAAgB,CACrB,IAAM,SAAW,KAAK,IAAI,EAAI,MACxB,UAAa,IAAY,WAAa,IAAI,QAAQ,iBAAmB,IACrE,OAAS,IAAI,WAOb,KAAO,IAAI,aAAe,IAAI,KAAO,IAC3C,IAAI,MAAM,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,SAAS,KAAK,WAAW,CACzE,CAAC,EAED,KAAK,CACP,CACF"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { a as session, i as SessionStore, n as SessionData, r as SessionOptions, t as Session } from "../../session-z9iewOXA.mjs";
2
+ import { a as session, i as SessionStore, n as SessionData, r as SessionOptions, t as Session } from "../../session-BHgRyfqZ.mjs";
3
3
  export { Session, SessionData, SessionOptions, SessionStore, session };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{r as registerDisposable}from"../../disposables-Bwm-JdIp.mjs";import{createHmac,randomUUID,timingSafeEqual}from"node:crypto";var MemoryStore=class{sessions=new Map;cleanupInterval;constructor(){this.cleanupInterval=setInterval(()=>this.purge(),6e4),this.cleanupInterval.unref(),registerDisposable(()=>this.dispose())}dispose(){clearInterval(this.cleanupInterval),this.sessions.clear()}async get(sid){let entry=this.sessions.get(sid);return entry?Date.now()>entry.expires?(this.sessions.delete(sid),null):entry.data:null}async set(sid,data,maxAge){this.sessions.set(sid,{data,expires:Date.now()+maxAge})}async destroy(sid){this.sessions.delete(sid)}async touch(sid,maxAge){let entry=this.sessions.get(sid);entry&&(entry.expires=Date.now()+maxAge)}purge(){let now=Date.now();for(let[sid,entry]of this.sessions)now>entry.expires&&this.sessions.delete(sid)}};function parseCookieHeader(header){let out={};for(let pair of header.split(`;`)){let eq=pair.indexOf(`=`);if(eq===-1)continue;let key=pair.slice(0,eq).trim();if(!key)continue;let value=pair.slice(eq+1).trim();value.startsWith(`"`)&&value.endsWith(`"`)&&(value=value.slice(1,-1));try{out[key]=decodeURIComponent(value)}catch{out[key]=value}}return out}function sign(value,secret){return`s:${value}.${createHmac(`sha256`,secret).update(value).digest(`base64url`)}`}function unsign(signed,secret){if(!signed.startsWith(`s:`))return!1;let raw=signed.slice(2),dotIndex=raw.lastIndexOf(`.`);if(dotIndex===-1)return!1;let value=raw.slice(0,dotIndex),providedSig=raw.slice(dotIndex+1),expectedSig=createHmac(`sha256`,secret).update(value).digest(`base64url`),a=Buffer.from(providedSig),b=Buffer.from(expectedSig);return a.length!==b.length||!timingSafeEqual(a,b)?!1:value}function session(options){let{secret,cookieName=`kick.sid`,maxAge=864e5,rolling=!1,saveUninitialized=!0,store=new MemoryStore,cookie:cookieOpts={}}=options,cookieDefaults={httpOnly:cookieOpts.httpOnly??!0,secure:cookieOpts.secure??process.env.NODE_ENV===`production`,sameSite:cookieOpts.sameSite??`lax`,path:cookieOpts.path??`/`,...cookieOpts.domain?{domain:cookieOpts.domain}:{},maxAge};return async(req,res,next)=>{let cookies=req.cookies;if(!cookies){let header=req.headers.cookie;cookies=header?parseCookieHeader(header):{}}let signedCookie=cookies[cookieName],sid=!1,sessionData=null,isNew=!1;signedCookie&&(sid=unsign(signedCookie,secret),sid&&(sessionData=await store.get(sid))),(!sid||!sessionData)&&(sid=randomUUID(),sessionData={},isNew=!0);let currentSid=sid,currentData={...sessionData},destroyed=!1,sessionObj={get id(){return currentSid},data:currentData,async regenerate(){await store.destroy(currentSid),currentSid=randomUUID(),currentData={},sessionObj.data=currentData,await store.set(currentSid,currentData,maxAge),res.cookie(cookieName,sign(currentSid,secret),cookieDefaults)},async destroy(){await store.destroy(currentSid),destroyed=!0,currentData={},sessionObj.data=currentData,res.clearCookie(cookieName,{path:cookieDefaults.path})},async save(){destroyed||await store.set(currentSid,currentData,maxAge)}};req.session=sessionObj,isNew&&res.cookie(cookieName,sign(currentSid,secret),cookieDefaults),res.on(`finish`,async()=>{destroyed||isNew&&!saveUninitialized&&Object.keys(currentData).length===0||(await store.set(currentSid,currentData,maxAge),rolling&&!isNew&&store.touch&&await store.touch(currentSid,maxAge))}),rolling&&!isNew&&res.cookie(cookieName,sign(currentSid,secret),cookieDefaults),next()}}export{session};
11
+ import{r as registerDisposable}from"../../disposables-jJAxz_e1.mjs";import{i as setCookie,t as parseCookieHeader}from"../../respond-C6fe709p.mjs";import{createHmac,randomUUID,timingSafeEqual}from"node:crypto";var MemoryStore=class{sessions=new Map;cleanupInterval;constructor(){this.cleanupInterval=setInterval(()=>this.purge(),6e4),this.cleanupInterval.unref(),registerDisposable(()=>this.dispose())}dispose(){clearInterval(this.cleanupInterval),this.sessions.clear()}async get(sid){let entry=this.sessions.get(sid);return entry?Date.now()>entry.expires?(this.sessions.delete(sid),null):entry.data:null}async set(sid,data,maxAge){this.sessions.set(sid,{data,expires:Date.now()+maxAge})}async destroy(sid){this.sessions.delete(sid)}async touch(sid,maxAge){let entry=this.sessions.get(sid);entry&&(entry.expires=Date.now()+maxAge)}purge(){let now=Date.now();for(let[sid,entry]of this.sessions)now>entry.expires&&this.sessions.delete(sid)}};function sign(value,secret){return`s:${value}.${createHmac(`sha256`,secret).update(value).digest(`base64url`)}`}function unsign(signed,secret){if(!signed.startsWith(`s:`))return!1;let raw=signed.slice(2),dotIndex=raw.lastIndexOf(`.`);if(dotIndex===-1)return!1;let value=raw.slice(0,dotIndex),providedSig=raw.slice(dotIndex+1),expectedSig=createHmac(`sha256`,secret).update(value).digest(`base64url`),a=Buffer.from(providedSig),b=Buffer.from(expectedSig);return a.length!==b.length||!timingSafeEqual(a,b)?!1:value}function session(options){let{secret,cookieName=`kick.sid`,maxAge=864e5,rolling=!1,saveUninitialized=!0,store=new MemoryStore,cookie:cookieOpts={}}=options,cookieDefaults={httpOnly:cookieOpts.httpOnly??!0,secure:cookieOpts.secure??process.env.NODE_ENV===`production`,sameSite:cookieOpts.sameSite??`lax`,path:cookieOpts.path??`/`,...cookieOpts.domain?{domain:cookieOpts.domain}:{},maxAge};return async(req,res,next)=>{let cookies=req.cookies;if(!cookies){let header=req.headers.cookie;cookies=header?parseCookieHeader(header):{}}let signedCookie=cookies[cookieName],sid=!1,sessionData=null,isNew=!1;signedCookie&&(sid=unsign(signedCookie,secret),sid&&(sessionData=await store.get(sid))),(!sid||!sessionData)&&(sid=randomUUID(),sessionData={},isNew=!0);let currentSid=sid,currentData={...sessionData},destroyed=!1,sessionObj={get id(){return currentSid},data:currentData,async regenerate(){await store.destroy(currentSid),currentSid=randomUUID(),currentData={},sessionObj.data=currentData,await store.set(currentSid,currentData,maxAge),setCookie(res,cookieName,sign(currentSid,secret),cookieDefaults)},async destroy(){await store.destroy(currentSid),destroyed=!0,currentData={},sessionObj.data=currentData,res.clearCookie(cookieName,{path:cookieDefaults.path})},async save(){destroyed||await store.set(currentSid,currentData,maxAge)}};req.session=sessionObj,isNew&&setCookie(res,cookieName,sign(currentSid,secret),cookieDefaults),res.on(`finish`,async()=>{destroyed||isNew&&!saveUninitialized&&Object.keys(currentData).length===0||(await store.set(currentSid,currentData,maxAge),rolling&&!isNew&&store.touch&&await store.touch(currentSid,maxAge))}),rolling&&!isNew&&setCookie(res,cookieName,sign(currentSid,secret),cookieDefaults),next()}}export{session};
12
12
  //# sourceMappingURL=session.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"session.mjs","names":[],"sources":["../../../src/http/middleware/session.ts"],"sourcesContent":["import { randomUUID, createHmac, timingSafeEqual } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nimport { registerDisposable } from '../../core/disposables'\n\nexport interface SessionData {\n [key: string]: unknown\n}\n\nexport interface SessionStore {\n get(sid: string): Promise<SessionData | null>\n set(sid: string, data: SessionData, maxAge: number): Promise<void>\n destroy(sid: string): Promise<void>\n touch?(sid: string, maxAge: number): Promise<void>\n}\n\nexport interface Session {\n id: string\n data: SessionData\n regenerate(): Promise<void>\n destroy(): Promise<void>\n save(): Promise<void>\n}\n\nexport interface SessionOptions {\n /** Secret used to sign the session cookie (required) */\n secret: string\n /** Cookie name (default: 'kick.sid') */\n cookieName?: string\n /** Session max age in milliseconds (default: 86400000 = 24h) */\n maxAge?: number\n /** Reset maxAge on every response (default: false) */\n rolling?: boolean\n /** Save new sessions that have not been modified (default: true) */\n saveUninitialized?: boolean\n /** Cookie options */\n cookie?: {\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n path?: string\n domain?: string\n }\n /** Custom session store (default: in-memory store with TTL cleanup) */\n store?: SessionStore\n}\n\n// ── In-Memory Store ───────────────────────────────────────────────────\n\nclass MemoryStore implements SessionStore {\n private readonly sessions = new Map<string, { data: SessionData; expires: number }>()\n private readonly cleanupInterval: ReturnType<typeof setInterval>\n\n constructor() {\n // Purge expired sessions every 60 seconds\n this.cleanupInterval = setInterval(() => this.purge(), 60_000)\n this.cleanupInterval.unref()\n // Symmetric teardown: Application.shutdown() drains the disposables\n // registry — without this, every in-process app re-create (tests, HMR)\n // leaked one live interval + its session Map.\n registerDisposable(() => this.dispose())\n }\n\n /** Clear the cleanup interval and drop all sessions. */\n dispose(): void {\n clearInterval(this.cleanupInterval)\n this.sessions.clear()\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const entry = this.sessions.get(sid)\n if (!entry) return null\n if (Date.now() > entry.expires) {\n this.sessions.delete(sid)\n return null\n }\n return entry.data\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n this.sessions.set(sid, { data, expires: Date.now() + maxAge })\n }\n\n async destroy(sid: string): Promise<void> {\n this.sessions.delete(sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const entry = this.sessions.get(sid)\n if (entry) {\n entry.expires = Date.now() + maxAge\n }\n }\n\n private purge() {\n const now = Date.now()\n for (const [sid, entry] of this.sessions) {\n if (now > entry.expires) {\n this.sessions.delete(sid)\n }\n }\n }\n}\n\n// ── Cookie Header Parsing ─────────────────────────────────────────────\n\nfunction parseCookieHeader(header: string): Record<string, string> {\n const out: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const eq = pair.indexOf('=')\n if (eq === -1) continue\n const key = pair.slice(0, eq).trim()\n if (!key) continue\n let value = pair.slice(eq + 1).trim()\n if (value.startsWith('\"') && value.endsWith('\"')) {\n value = value.slice(1, -1)\n }\n try {\n out[key] = decodeURIComponent(value)\n } catch {\n out[key] = value\n }\n }\n return out\n}\n\n// ── Cookie Signing ────────────────────────────────────────────────────\n\nfunction sign(value: string, secret: string): string {\n const signature = createHmac('sha256', secret).update(value).digest('base64url')\n return `s:${value}.${signature}`\n}\n\nfunction unsign(signed: string, secret: string): string | false {\n if (!signed.startsWith('s:')) return false\n const raw = signed.slice(2)\n const dotIndex = raw.lastIndexOf('.')\n if (dotIndex === -1) return false\n\n const value = raw.slice(0, dotIndex)\n const providedSig = raw.slice(dotIndex + 1)\n const expectedSig = createHmac('sha256', secret).update(value).digest('base64url')\n\n const a = Buffer.from(providedSig)\n const b = Buffer.from(expectedSig)\n\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n return false\n }\n\n return value\n}\n\n// ── Middleware Factory ────────────────────────────────────────────────\n\n/**\n * Session management middleware.\n *\n * Attaches a `req.session` object with `id`, `data`, `regenerate()`,\n * `destroy()`, and `save()` methods. Session IDs are signed with\n * HMAC-SHA256 to prevent cookie tampering.\n *\n * Reads the session cookie from `req.cookies` when an upstream cookie\n * parser is present; otherwise parses `req.headers.cookie` directly, so\n * no cookie parser is required upstream.\n *\n * @example\n * ```ts\n * import { session } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * session({ secret: process.env.SESSION_SECRET! }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function session(options: SessionOptions) {\n const {\n secret,\n cookieName = 'kick.sid',\n maxAge = 86_400_000,\n rolling = false,\n saveUninitialized = true,\n store = new MemoryStore(),\n cookie: cookieOpts = {},\n } = options\n\n const cookieDefaults = {\n httpOnly: cookieOpts.httpOnly ?? true,\n secure: cookieOpts.secure ?? process.env.NODE_ENV === 'production',\n sameSite: cookieOpts.sameSite ?? ('lax' as const),\n path: cookieOpts.path ?? '/',\n ...(cookieOpts.domain ? { domain: cookieOpts.domain } : {}),\n maxAge,\n }\n\n return async (req: Request, res: Response, next: NextFunction) => {\n let cookies = (req as any).cookies as Record<string, string> | undefined\n if (!cookies) {\n const header = req.headers.cookie\n cookies = header ? parseCookieHeader(header) : {}\n }\n const signedCookie = cookies[cookieName]\n let sid: string | false = false\n let sessionData: SessionData | null = null\n let isNew = false\n\n // Attempt to recover existing session\n if (signedCookie) {\n sid = unsign(signedCookie, secret)\n if (sid) {\n sessionData = await store.get(sid)\n }\n }\n\n // Create new session if none found\n if (!sid || !sessionData) {\n sid = randomUUID()\n sessionData = {}\n isNew = true\n }\n\n let currentSid = sid\n let currentData = { ...sessionData }\n let destroyed = false\n\n const sessionObj: Session = {\n get id() {\n return currentSid\n },\n data: currentData,\n\n async regenerate() {\n await store.destroy(currentSid)\n currentSid = randomUUID()\n currentData = {}\n sessionObj.data = currentData\n await store.set(currentSid, currentData, maxAge)\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n },\n\n async destroy() {\n await store.destroy(currentSid)\n destroyed = true\n currentData = {}\n sessionObj.data = currentData\n res.clearCookie(cookieName, { path: cookieDefaults.path })\n },\n\n async save() {\n if (!destroyed) {\n await store.set(currentSid, currentData, maxAge)\n }\n },\n }\n\n ;(req as any).session = sessionObj\n\n // Set cookie for new sessions\n if (isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n // Auto-save on response finish\n res.on('finish', async () => {\n if (destroyed) return\n if (isNew && !saveUninitialized && Object.keys(currentData).length === 0) return\n\n await store.set(currentSid, currentData, maxAge)\n\n if (rolling && !isNew) {\n if (store.touch) {\n await store.touch(currentSid, maxAge)\n }\n }\n })\n\n // Rolling: refresh cookie on every response\n if (rolling && !isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;mIAiDA,IAAM,YAAN,KAA0C,CACxC,SAA4B,IAAI,IAChC,gBAEA,aAAc,CAEZ,KAAK,gBAAkB,gBAAkB,KAAK,MAAM,EAAG,GAAM,EAC7D,KAAK,gBAAgB,MAAM,EAI3B,uBAAyB,KAAK,QAAQ,CAAC,CACzC,CAGA,SAAgB,CACd,cAAc,KAAK,eAAe,EAClC,KAAK,SAAS,MAAM,CACtB,CAEA,MAAM,IAAI,IAA0C,CAClD,IAAM,MAAQ,KAAK,SAAS,IAAI,GAAG,EAMnC,OALK,MACD,KAAK,IAAI,EAAI,MAAM,SACrB,KAAK,SAAS,OAAO,GAAG,EACjB,MAEF,MAAM,KALM,IAMrB,CAEA,MAAM,IAAI,IAAa,KAAmB,OAA+B,CACvE,KAAK,SAAS,IAAI,IAAK,CAAE,KAAM,QAAS,KAAK,IAAI,EAAI,MAAO,CAAC,CAC/D,CAEA,MAAM,QAAQ,IAA4B,CACxC,KAAK,SAAS,OAAO,GAAG,CAC1B,CAEA,MAAM,MAAM,IAAa,OAA+B,CACtD,IAAM,MAAQ,KAAK,SAAS,IAAI,GAAG,EAC/B,QACF,MAAM,QAAU,KAAK,IAAI,EAAI,OAEjC,CAEA,OAAgB,CACd,IAAM,IAAM,KAAK,IAAI,EACrB,IAAK,GAAM,CAAC,IAAK,SAAU,KAAK,SAC1B,IAAM,MAAM,SACd,KAAK,SAAS,OAAO,GAAG,CAG9B,CACF,EAIA,SAAS,kBAAkB,OAAwC,CACjE,IAAM,IAA8B,CAAC,EACrC,IAAK,IAAM,QAAQ,OAAO,MAAM,GAAG,EAAG,CACpC,IAAM,GAAK,KAAK,QAAQ,GAAG,EAC3B,GAAI,KAAO,GAAI,SACf,IAAM,IAAM,KAAK,MAAM,EAAG,EAAE,CAAC,CAAC,KAAK,EACnC,GAAI,CAAC,IAAK,SACV,IAAI,MAAQ,KAAK,MAAM,GAAK,CAAC,CAAC,CAAC,KAAK,EAChC,MAAM,WAAW,GAAG,GAAK,MAAM,SAAS,GAAG,IAC7C,MAAQ,MAAM,MAAM,EAAG,EAAE,GAE3B,GAAI,CACF,IAAI,KAAO,mBAAmB,KAAK,CACrC,MAAQ,CACN,IAAI,KAAO,KACb,CACF,CACA,OAAO,GACT,CAIA,SAAS,KAAK,MAAe,OAAwB,CAEnD,MAAO,KAAK,MAAM,GADA,WAAW,SAAU,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,WACvC,GAC/B,CAEA,SAAS,OAAO,OAAgB,OAAgC,CAC9D,GAAI,CAAC,OAAO,WAAW,IAAI,EAAG,MAAO,GACrC,IAAM,IAAM,OAAO,MAAM,CAAC,EACpB,SAAW,IAAI,YAAY,GAAG,EACpC,GAAI,WAAa,GAAI,MAAO,GAE5B,IAAM,MAAQ,IAAI,MAAM,EAAG,QAAQ,EAC7B,YAAc,IAAI,MAAM,SAAW,CAAC,EACpC,YAAc,WAAW,SAAU,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,WAAW,EAE3E,EAAI,OAAO,KAAK,WAAW,EAC3B,EAAI,OAAO,KAAK,WAAW,EAMjC,OAJI,EAAE,SAAW,EAAE,QAAU,CAAC,gBAAgB,EAAG,CAAC,EACzC,GAGF,KACT,CA4BA,SAAgB,QAAQ,QAAyB,CAC/C,GAAM,CACJ,OACA,WAAa,WACb,OAAS,MACT,QAAU,GACV,kBAAoB,GACpB,MAAQ,IAAI,YACZ,OAAQ,WAAa,CAAC,GACpB,QAEE,eAAiB,CACrB,SAAU,WAAW,UAAY,GACjC,OAAQ,WAAW,QAAU,QAAQ,IAAI,WAAa,aACtD,SAAU,WAAW,UAAa,MAClC,KAAM,WAAW,MAAQ,IACzB,GAAI,WAAW,OAAS,CAAE,OAAQ,WAAW,MAAO,EAAI,CAAC,EACzD,MACF,EAEA,OAAO,MAAO,IAAc,IAAe,OAAuB,CAChE,IAAI,QAAW,IAAY,QAC3B,GAAI,CAAC,QAAS,CACZ,IAAM,OAAS,IAAI,QAAQ,OAC3B,QAAU,OAAS,kBAAkB,MAAM,EAAI,CAAC,CAClD,CACA,IAAM,aAAe,QAAQ,YACzB,IAAsB,GACtB,YAAkC,KAClC,MAAQ,GAGR,eACF,IAAM,OAAO,aAAc,MAAM,EAC7B,MACF,YAAc,MAAM,MAAM,IAAI,GAAG,KAKjC,CAAC,KAAO,CAAC,eACX,IAAM,WAAW,EACjB,YAAc,CAAC,EACf,MAAQ,IAGV,IAAI,WAAa,IACb,YAAc,CAAE,GAAG,WAAY,EAC/B,UAAY,GAEV,WAAsB,CAC1B,IAAI,IAAK,CACP,OAAO,UACT,EACA,KAAM,YAEN,MAAM,YAAa,CACjB,MAAM,MAAM,QAAQ,UAAU,EAC9B,WAAa,WAAW,EACxB,YAAc,CAAC,EACf,WAAW,KAAO,YAClB,MAAM,MAAM,IAAI,WAAY,YAAa,MAAM,EAC/C,IAAI,OAAO,WAAY,KAAK,WAAY,MAAM,EAAG,cAAc,CACjE,EAEA,MAAM,SAAU,CACd,MAAM,MAAM,QAAQ,UAAU,EAC9B,UAAY,GACZ,YAAc,CAAC,EACf,WAAW,KAAO,YAClB,IAAI,YAAY,WAAY,CAAE,KAAM,eAAe,IAAK,CAAC,CAC3D,EAEA,MAAM,MAAO,CACN,WACH,MAAM,MAAM,IAAI,WAAY,YAAa,MAAM,CAEnD,CACF,EAEC,IAAa,QAAU,WAGpB,OACF,IAAI,OAAO,WAAY,KAAK,WAAY,MAAM,EAAG,cAAc,EAIjE,IAAI,GAAG,SAAU,SAAY,CACvB,WACA,OAAS,CAAC,mBAAqB,OAAO,KAAK,WAAW,CAAC,CAAC,SAAW,IAEvE,MAAM,MAAM,IAAI,WAAY,YAAa,MAAM,EAE3C,SAAW,CAAC,OACV,MAAM,OACR,MAAM,MAAM,MAAM,WAAY,MAAM,EAG1C,CAAC,EAGG,SAAW,CAAC,OACd,IAAI,OAAO,WAAY,KAAK,WAAY,MAAM,EAAG,cAAc,EAGjE,KAAK,CACP,CACF"}
1
+ {"version":3,"file":"session.mjs","names":[],"sources":["../../../src/http/middleware/session.ts"],"sourcesContent":["import { parseCookieHeader, setCookie } from './respond'\nimport { randomUUID, createHmac, timingSafeEqual } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nimport { registerDisposable } from '../../core/disposables'\n\nexport interface SessionData {\n [key: string]: unknown\n}\n\nexport interface SessionStore {\n get(sid: string): Promise<SessionData | null>\n set(sid: string, data: SessionData, maxAge: number): Promise<void>\n destroy(sid: string): Promise<void>\n touch?(sid: string, maxAge: number): Promise<void>\n}\n\nexport interface Session {\n id: string\n data: SessionData\n regenerate(): Promise<void>\n destroy(): Promise<void>\n save(): Promise<void>\n}\n\nexport interface SessionOptions {\n /** Secret used to sign the session cookie (required) */\n secret: string\n /** Cookie name (default: 'kick.sid') */\n cookieName?: string\n /** Session max age in milliseconds (default: 86400000 = 24h) */\n maxAge?: number\n /** Reset maxAge on every response (default: false) */\n rolling?: boolean\n /** Save new sessions that have not been modified (default: true) */\n saveUninitialized?: boolean\n /** Cookie options */\n cookie?: {\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n path?: string\n domain?: string\n }\n /** Custom session store (default: in-memory store with TTL cleanup) */\n store?: SessionStore\n}\n\n// ── In-Memory Store ───────────────────────────────────────────────────\n\nclass MemoryStore implements SessionStore {\n private readonly sessions = new Map<string, { data: SessionData; expires: number }>()\n private readonly cleanupInterval: ReturnType<typeof setInterval>\n\n constructor() {\n // Purge expired sessions every 60 seconds\n this.cleanupInterval = setInterval(() => this.purge(), 60_000)\n this.cleanupInterval.unref()\n // Symmetric teardown: Application.shutdown() drains the disposables\n // registry — without this, every in-process app re-create (tests, HMR)\n // leaked one live interval + its session Map.\n registerDisposable(() => this.dispose())\n }\n\n /** Clear the cleanup interval and drop all sessions. */\n dispose(): void {\n clearInterval(this.cleanupInterval)\n this.sessions.clear()\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const entry = this.sessions.get(sid)\n if (!entry) return null\n if (Date.now() > entry.expires) {\n this.sessions.delete(sid)\n return null\n }\n return entry.data\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n this.sessions.set(sid, { data, expires: Date.now() + maxAge })\n }\n\n async destroy(sid: string): Promise<void> {\n this.sessions.delete(sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const entry = this.sessions.get(sid)\n if (entry) {\n entry.expires = Date.now() + maxAge\n }\n }\n\n private purge() {\n const now = Date.now()\n for (const [sid, entry] of this.sessions) {\n if (now > entry.expires) {\n this.sessions.delete(sid)\n }\n }\n }\n}\n\n// ── Cookie Signing ────────────────────────────────────────────────────\n\nfunction sign(value: string, secret: string): string {\n const signature = createHmac('sha256', secret).update(value).digest('base64url')\n return `s:${value}.${signature}`\n}\n\nfunction unsign(signed: string, secret: string): string | false {\n if (!signed.startsWith('s:')) return false\n const raw = signed.slice(2)\n const dotIndex = raw.lastIndexOf('.')\n if (dotIndex === -1) return false\n\n const value = raw.slice(0, dotIndex)\n const providedSig = raw.slice(dotIndex + 1)\n const expectedSig = createHmac('sha256', secret).update(value).digest('base64url')\n\n const a = Buffer.from(providedSig)\n const b = Buffer.from(expectedSig)\n\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n return false\n }\n\n return value\n}\n\n// ── Middleware Factory ────────────────────────────────────────────────\n\n/**\n * Session management middleware.\n *\n * Attaches a `req.session` object with `id`, `data`, `regenerate()`,\n * `destroy()`, and `save()` methods. Session IDs are signed with\n * HMAC-SHA256 to prevent cookie tampering.\n *\n * Reads the session cookie from `req.cookies` when an upstream cookie\n * parser is present; otherwise parses `req.headers.cookie` directly, so\n * no cookie parser is required upstream.\n *\n * @example\n * ```ts\n * import { session } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middlewares: [\n * session({ secret: process.env.SESSION_SECRET! }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function session(options: SessionOptions) {\n const {\n secret,\n cookieName = 'kick.sid',\n maxAge = 86_400_000,\n rolling = false,\n saveUninitialized = true,\n store = new MemoryStore(),\n cookie: cookieOpts = {},\n } = options\n\n const cookieDefaults = {\n httpOnly: cookieOpts.httpOnly ?? true,\n secure: cookieOpts.secure ?? process.env.NODE_ENV === 'production',\n sameSite: cookieOpts.sameSite ?? ('lax' as const),\n path: cookieOpts.path ?? '/',\n ...(cookieOpts.domain ? { domain: cookieOpts.domain } : {}),\n maxAge,\n }\n\n return async (req: Request, res: Response, next: NextFunction) => {\n let cookies = (req as any).cookies as Record<string, string> | undefined\n if (!cookies) {\n const header = req.headers.cookie\n cookies = header ? parseCookieHeader(header) : {}\n }\n const signedCookie = cookies[cookieName]\n let sid: string | false = false\n let sessionData: SessionData | null = null\n let isNew = false\n\n // Attempt to recover existing session\n if (signedCookie) {\n sid = unsign(signedCookie, secret)\n if (sid) {\n sessionData = await store.get(sid)\n }\n }\n\n // Create new session if none found\n if (!sid || !sessionData) {\n sid = randomUUID()\n sessionData = {}\n isNew = true\n }\n\n let currentSid = sid\n let currentData = { ...sessionData }\n let destroyed = false\n\n const sessionObj: Session = {\n get id() {\n return currentSid\n },\n data: currentData,\n\n async regenerate() {\n await store.destroy(currentSid)\n currentSid = randomUUID()\n currentData = {}\n sessionObj.data = currentData\n await store.set(currentSid, currentData, maxAge)\n setCookie(res, cookieName, sign(currentSid, secret), cookieDefaults)\n },\n\n async destroy() {\n await store.destroy(currentSid)\n destroyed = true\n currentData = {}\n sessionObj.data = currentData\n res.clearCookie(cookieName, { path: cookieDefaults.path })\n },\n\n async save() {\n if (!destroyed) {\n await store.set(currentSid, currentData, maxAge)\n }\n },\n }\n\n ;(req as any).session = sessionObj\n\n // Set cookie for new sessions\n if (isNew) {\n setCookie(res, cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n // Auto-save on response finish\n res.on('finish', async () => {\n if (destroyed) return\n if (isNew && !saveUninitialized && Object.keys(currentData).length === 0) return\n\n await store.set(currentSid, currentData, maxAge)\n\n if (rolling && !isNew) {\n if (store.touch) {\n await store.touch(currentSid, maxAge)\n }\n }\n })\n\n // Rolling: refresh cookie on every response\n if (rolling && !isNew) {\n setCookie(res, cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n next()\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as SpaAdapterOptions, r as resolveClientDir, t as SpaAdapter } from "../../spa-1Uc4QIif.mjs";
2
+ import { n as SpaAdapterOptions, r as resolveClientDir, t as SpaAdapter } from "../../spa-DAoE9C2S.mjs";
3
3
  export { SpaAdapter, SpaAdapterOptions, resolveClientDir };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{Logger}from"../../core/logger.mjs";import"../../core-BGsV3RjD.mjs";import{t as defineAdapter}from"../../define-adapter-VXKsMUNb.mjs";import{existsSync,readFileSync,readdirSync}from"node:fs";import{dirname,isAbsolute,join,resolve}from"node:path";const log=Logger.for(`SpaAdapter`);function nearestPackageRoot(from){let dir=from;for(;;){if(existsSync(join(dir,`package.json`)))return dir;let parent=dirname(dir);if(parent===dir)return null;dir=parent}}function resolveClientDir(clientDir,cwd=process.cwd(),entry=process.argv[1]){if(isAbsolute(clientDir))return resolve(clientDir);let fromCwd=resolve(cwd,clientDir);if(existsSync(fromCwd))return fromCwd;if(entry){let root=nearestPackageRoot(dirname(resolve(entry)));if(root){let fromEntry=resolve(root,clientDir);if(existsSync(fromEntry))return fromEntry}}return fromCwd}function pathnameOf(url){if(!url)return`/`;let q=url.indexOf(`?`),h=url.indexOf(`#`),end=Math.min(q===-1?url.length:q,h===-1?url.length:h);return url.slice(0,end)||`/`}function isUnder(pathname,prefix){if(!prefix||prefix===`/`)return!0;let p=prefix.endsWith(`/`)?prefix.slice(0,-1):prefix;return pathname===p||pathname.startsWith(`${p}/`)}const HTML_TYPES=[`text/html`,`application/xhtml+xml`];function qualityOf(params){let q=params.find(param=>param.startsWith(`q=`));if(!q)return 1;let value=Number.parseFloat(q.slice(2));return Number.isNaN(value)?1:value}function acceptsHtml(accept){let raw=Array.isArray(accept)?accept.join(`,`):accept??``;if(!raw)return!1;let ranges=raw.split(`,`).map(part=>part.split(`;`).map(t=>t.trim().toLowerCase())).filter(([type])=>!!type);return HTML_TYPES.some(htmlType=>{let[group]=htmlType.split(`/`),exact=ranges.find(([type])=>type===htmlType),wildcard=ranges.find(([type])=>type===`${group}/*`),match=exact??wildcard;return match?qualityOf(match.slice(1))>0:!1})}const SpaAdapter=defineAdapter({name:`SpaAdapter`,defaults:{clientDir:`dist/client`,apiPrefix:`/api`,exclude:[],cacheControl:`public, max-age=31536000, immutable`,indexCacheControl:`no-cache`,alwaysFallback:!1},build:config=>{let clientDir=resolveClientDir(config.clientDir??`dist/client`),rawPrefix=config.apiPrefix??`/api`,apiPrefixes=Array.isArray(rawPrefix)?rawPrefix:[rawPrefix],excludePaths=config.exclude??[],cacheControl=config.cacheControl??`public, max-age=31536000, immutable`,indexCacheControl=config.indexCacheControl??`no-cache`,alwaysFallback=config.alwaysFallback??!1,indexHtml=null,assetPaths=new Set;function snapshotClientDir(){let paths=new Set,walk=(absDir,prefix)=>{for(let entry of readdirSync(absDir,{withFileTypes:!0})){let child=`${prefix}/${entry.name}`;entry.isDirectory()?walk(join(absDir,entry.name),child):entry.isFile()&&paths.add(child)}};try{walk(clientDir,``)}catch{return new Set}return paths}let classifyPath=pathname=>{let decoded;try{decoded=decodeURIComponent(pathname)}catch{return null}if(decoded.includes(`\0`))return null;let asIndex=decoded.endsWith(`/`)?`${decoded}index.html`:null;return asIndex&&assetPaths.has(asIndex)?`index`:assetPaths.has(decoded)?decoded.endsWith(`.html`)?`index`:`file`:null},isReserved=pathname=>apiPrefixes.some(p=>isUnder(pathname,p))||excludePaths.some(p=>isUnder(pathname,p));return{beforeMount({http}){if(!existsSync(clientDir)){log.warn(`SPA client directory not found: ${clientDir}`),log.warn(`Build your frontend first, or set clientDir to the correct path (relative paths resolve from ${process.cwd()}).`);return}let indexPath=join(clientDir,`index.html`);if(!existsSync(indexPath)){log.warn(`index.html not found in ${clientDir}`);return}indexHtml=readFileSync(indexPath,`utf-8`),assetPaths=snapshotClientDir(),cacheControl!==!1&&http.use(((req,res,next)=>{let pathname=pathnameOf(req.url),kind=isReserved(pathname)?null:classifyPath(pathname);kind&&res.setHeader(`Cache-Control`,kind===`index`?indexCacheControl:cacheControl),next()})),http.serveStatic(`/`,clientDir),log.debug(`Serving SPA from ${clientDir}`)},beforeStart({http}){indexHtml&&http.use(((req,res,next)=>{let method=(req.method??`GET`).toUpperCase();if(method!==`GET`&&method!==`HEAD`)return next();let pathname=pathnameOf(req.url);if(isReserved(pathname))return next();let accept=req.headers?.accept;if(!alwaysFallback&&!acceptsHtml(accept))return next();let body=indexHtml;res.setHeader(`Content-Type`,`text/html; charset=utf-8`),res.setHeader(`Cache-Control`,indexCacheControl),res.setHeader(`Content-Length`,String(Buffer.byteLength(body))),res.statusCode=200,res.end(method===`HEAD`?void 0:body)}))}}}});export{SpaAdapter,resolveClientDir};
11
+ import{Logger}from"../../core/logger.mjs";import"../../core-B1PK7S5p.mjs";import{t as defineAdapter}from"../../define-adapter-DUBru2GV.mjs";import{existsSync,readFileSync,readdirSync}from"node:fs";import{dirname,isAbsolute,join,resolve}from"node:path";const log=Logger.for(`SpaAdapter`);function nearestPackageRoot(from){let dir=from;for(;;){if(existsSync(join(dir,`package.json`)))return dir;let parent=dirname(dir);if(parent===dir)return null;dir=parent}}function resolveClientDir(clientDir,cwd=process.cwd(),entry=process.argv[1]){if(isAbsolute(clientDir))return resolve(clientDir);let fromCwd=resolve(cwd,clientDir);if(existsSync(fromCwd))return fromCwd;if(entry){let root=nearestPackageRoot(dirname(resolve(entry)));if(root){let fromEntry=resolve(root,clientDir);if(existsSync(fromEntry))return fromEntry}}return fromCwd}function pathnameOf(url){if(!url)return`/`;let q=url.indexOf(`?`),h=url.indexOf(`#`),end=Math.min(q===-1?url.length:q,h===-1?url.length:h);return url.slice(0,end)||`/`}function isUnder(pathname,prefix){if(!prefix||prefix===`/`)return!0;let p=prefix.endsWith(`/`)?prefix.slice(0,-1):prefix;return pathname===p||pathname.startsWith(`${p}/`)}const HTML_TYPES=[`text/html`,`application/xhtml+xml`];function qualityOf(params){let q=params.find(param=>param.startsWith(`q=`));if(!q)return 1;let value=Number.parseFloat(q.slice(2));return Number.isNaN(value)?1:value}function acceptsHtml(accept){let raw=Array.isArray(accept)?accept.join(`,`):accept??``;if(!raw)return!1;let ranges=raw.split(`,`).map(part=>part.split(`;`).map(t=>t.trim().toLowerCase())).filter(([type])=>!!type);return HTML_TYPES.some(htmlType=>{let[group]=htmlType.split(`/`),exact=ranges.find(([type])=>type===htmlType),wildcard=ranges.find(([type])=>type===`${group}/*`),match=exact??wildcard;return match?qualityOf(match.slice(1))>0:!1})}const SpaAdapter=defineAdapter({name:`SpaAdapter`,defaults:{clientDir:`dist/client`,apiPrefix:`/api`,exclude:[],cacheControl:`public, max-age=31536000, immutable`,indexCacheControl:`no-cache`,alwaysFallback:!1},build:config=>{let clientDir=resolveClientDir(config.clientDir??`dist/client`),rawPrefix=config.apiPrefix??`/api`,apiPrefixes=Array.isArray(rawPrefix)?rawPrefix:[rawPrefix],excludePaths=config.exclude??[],cacheControl=config.cacheControl??`public, max-age=31536000, immutable`,indexCacheControl=config.indexCacheControl??`no-cache`,alwaysFallback=config.alwaysFallback??!1,indexHtml=null,assetPaths=new Set;function snapshotClientDir(){let paths=new Set,walk=(absDir,prefix)=>{for(let entry of readdirSync(absDir,{withFileTypes:!0})){let child=`${prefix}/${entry.name}`;entry.isDirectory()?walk(join(absDir,entry.name),child):entry.isFile()&&paths.add(child)}};try{walk(clientDir,``)}catch{return new Set}return paths}let classifyPath=pathname=>{let decoded;try{decoded=decodeURIComponent(pathname)}catch{return null}if(decoded.includes(`\0`))return null;let asIndex=decoded.endsWith(`/`)?`${decoded}index.html`:null;return asIndex&&assetPaths.has(asIndex)?`index`:assetPaths.has(decoded)?decoded.endsWith(`.html`)?`index`:`file`:null},isReserved=pathname=>apiPrefixes.some(p=>isUnder(pathname,p))||excludePaths.some(p=>isUnder(pathname,p));return{beforeMount({http}){if(!existsSync(clientDir)){log.warn(`SPA client directory not found: ${clientDir}`),log.warn(`Build your frontend first, or set clientDir to the correct path (relative paths resolve from ${process.cwd()}).`);return}let indexPath=join(clientDir,`index.html`);if(!existsSync(indexPath)){log.warn(`index.html not found in ${clientDir}`);return}indexHtml=readFileSync(indexPath,`utf-8`),assetPaths=snapshotClientDir(),cacheControl!==!1&&http.use(((req,res,next)=>{let pathname=pathnameOf(req.url),kind=isReserved(pathname)?null:classifyPath(pathname);kind&&res.setHeader(`Cache-Control`,kind===`index`?indexCacheControl:cacheControl),next()})),http.serveStatic(`/`,clientDir),log.debug(`Serving SPA from ${clientDir}`)},beforeStart({http}){indexHtml&&http.use(((req,res,next)=>{let method=(req.method??`GET`).toUpperCase();if(method!==`GET`&&method!==`HEAD`)return next();let pathname=pathnameOf(req.url);if(isReserved(pathname))return next();let accept=req.headers?.accept;if(!alwaysFallback&&!acceptsHtml(accept))return next();let body=indexHtml;res.setHeader(`Content-Type`,`text/html; charset=utf-8`),res.setHeader(`Cache-Control`,indexCacheControl),res.setHeader(`Content-Length`,String(Buffer.byteLength(body))),res.statusCode=200,res.end(method===`HEAD`?void 0:body)}))}}}});export{SpaAdapter,resolveClientDir};
12
12
  //# sourceMappingURL=spa.mjs.map
@@ -1,3 +1,3 @@
1
1
 
2
- import { a as RawUploadPart, c as buildFileTypeFilter, i as upload, l as resolveMimeTypes, n as buildUploadMiddleware, o as UploadedFileLike, r as cleanupFiles, s as applyUploadConfig, t as UploadOptions } from "../../upload-aQzE6lT0.mjs";
2
+ import { a as RawUploadPart, c as buildFileTypeFilter, i as upload, l as resolveMimeTypes, n as buildUploadMiddleware, o as UploadedFileLike, r as cleanupFiles, s as applyUploadConfig, t as UploadOptions } from "../../upload-dSsVE0BW.mjs";
3
3
  export { type RawUploadPart, UploadOptions, type UploadedFileLike, applyUploadConfig, buildFileTypeFilter, buildUploadMiddleware, cleanupFiles, resolveMimeTypes, upload };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,6 +8,6 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{n as buildFileTypeFilter,r as resolveMimeTypes,t as applyUploadConfig}from"../../upload-config-BvSLBwom.mjs";import{createRequire}from"node:module";import{unlink}from"node:fs/promises";const requireFromHere=createRequire(import.meta.url);let _multer;function loadMulter(){if(_multer)return _multer;try{return _multer=requireFromHere(`multer`),_multer}catch{throw Error(`@forinda/kickjs: file uploads require the 'multer' package, which is not installed.
12
- Install it as a peer dependency: pnpm add multer (or npm i multer / yarn add multer).`)}}function createMulter(options){let limits={fileSize:options.maxSize??5*1024*1024},fileFilter;if(options.allowedTypes){let typeAllowed=buildFileTypeFilter(options.allowedTypes,options.customMimeMap);fileFilter=(_req,file,cb)=>{typeAllowed(file.mimetype,file.originalname)?cb(null,!0):cb(Error(`File type ${file.mimetype} is not allowed`))}}let multerOptions={limits,...fileFilter?{fileFilter}:{},...options.storage?{storage:options.storage}:{},...options.dest?{dest:options.dest}:{}};return loadMulter()(multerOptions)}function single(fieldName,options={}){return createMulter(options).single(fieldName)}function array(fieldName,maxCount=10,options={}){return createMulter(options).array(fieldName,maxCount)}function none(options={}){return createMulter(options).none()}function cleanupFiles(){return(req,res,next)=>{res.on(`finish`,async()=>{let files=[];if(req.file?.path&&files.push(req.file),Array.isArray(req.files))for(let f of req.files)f?.path&&files.push(f);for(let file of files)try{await unlink(file.path)}catch{}}),next()}}function buildUploadMiddleware(config){let options={};config.maxSize&&(options.maxSize=config.maxSize),config.allowedTypes&&(options.allowedTypes=config.allowedTypes),config.customMimeMap&&(options.customMimeMap=config.customMimeMap);let fieldName=config.fieldName??`file`;switch(config.mode){case`single`:return single(fieldName,options);case`array`:return array(fieldName,config.maxCount??10,options);case`none`:return none(options)}}const upload={single,array,none};export{applyUploadConfig,buildFileTypeFilter,buildUploadMiddleware,cleanupFiles,resolveMimeTypes,upload};
11
+ import{HttpException,HttpStatus}from"../../core/errors.mjs";import{n as buildFileTypeFilter,r as resolveMimeTypes,t as applyUploadConfig}from"../../upload-config-Da_g7Rig.mjs";import{createRequire}from"node:module";import{unlink}from"node:fs/promises";const requireFromHere=createRequire(import.meta.url);let _multer;function loadMulter(){if(_multer)return _multer;try{return _multer=requireFromHere(`multer`),_multer}catch{throw Error(`@forinda/kickjs: file uploads require the 'multer' package, which is not installed.
12
+ Install it as a peer dependency: pnpm add multer (or npm i multer / yarn add multer).`)}}function translateMulterErrors(mw,options){let maxSize=options.maxSize??5*1024*1024;return(req,res,next)=>{mw(req,res,err=>{if(err&&err.code===`LIMIT_FILE_SIZE`){let name=err.field??`file`;return next(new HttpException(HttpStatus.PAYLOAD_TOO_LARGE,`File ${name} exceeds the ${maxSize}-byte limit`))}next(err)})}}function createMulter(options){let limits={fileSize:options.maxSize??5*1024*1024},fileFilter;if(options.allowedTypes){let typeAllowed=buildFileTypeFilter(options.allowedTypes,options.customMimeMap);fileFilter=(_req,file,cb)=>{typeAllowed(file.mimetype,file.originalname)?cb(null,!0):cb(new HttpException(HttpStatus.UNSUPPORTED_MEDIA_TYPE,`File type ${file.mimetype} is not allowed`))}}let multerOptions={limits,...fileFilter?{fileFilter}:{},...options.storage?{storage:options.storage}:{},...options.dest?{dest:options.dest}:{}};return loadMulter()(multerOptions)}function single(fieldName,options={}){return translateMulterErrors(createMulter(options).single(fieldName),options)}function array(fieldName,maxCount=10,options={}){return translateMulterErrors(createMulter(options).array(fieldName,maxCount),options)}function none(options={}){return translateMulterErrors(createMulter(options).none(),options)}function cleanupFiles(){return(req,res,next)=>{res.on(`finish`,async()=>{let files=[];if(req.file?.path&&files.push(req.file),Array.isArray(req.files))for(let f of req.files)f?.path&&files.push(f);for(let file of files)try{await unlink(file.path)}catch{}}),next()}}function buildUploadMiddleware(config){let options={};config.maxSize&&(options.maxSize=config.maxSize),config.allowedTypes&&(options.allowedTypes=config.allowedTypes),config.customMimeMap&&(options.customMimeMap=config.customMimeMap);let fieldName=config.fieldName??`file`;switch(config.mode){case`single`:return single(fieldName,options);case`array`:return array(fieldName,config.maxCount??10,options);case`none`:return none(options)}}const upload={single,array,none};export{applyUploadConfig,buildFileTypeFilter,buildUploadMiddleware,cleanupFiles,resolveMimeTypes,upload};
13
13
  //# sourceMappingURL=upload.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload.mjs","names":[],"sources":["../../../src/http/middleware/upload.ts"],"sourcesContent":["import { unlink } from 'node:fs/promises'\nimport { createRequire } from 'node:module'\nimport type { Request, Response, NextFunction, RequestHandler } from 'express'\nimport type { Options as MulterOptions } from 'multer'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core'\n\n// `multer` is an optional peer dependency. We load it lazily via\n// `createRequire` so importing this module never touches `multer` —\n// only constructing an upload middleware does. Adopters who never\n// call `upload.single/array/none()` or use `@FileUpload` don't need\n// `multer` installed at all.\n//\n// multer uses CJS `export = multer`, so the module is itself the\n// callable factory — no `.default` indirection on either the type\n// or the runtime side.\nconst requireFromHere = createRequire(import.meta.url)\ntype MulterFactory = typeof import('multer')\nlet _multer: MulterFactory | undefined\n\nfunction loadMulter(): MulterFactory {\n if (_multer) return _multer\n try {\n _multer = requireFromHere('multer') as MulterFactory\n return _multer\n } catch {\n throw new Error(\n \"@forinda/kickjs: file uploads require the 'multer' package, which is not installed.\\n\" +\n 'Install it as a peer dependency: pnpm add multer (or npm i multer / yarn add multer).',\n )\n }\n}\n\n// Pure upload core (MIME map, type filter, applyUploadConfig) moved to\n// `./upload-config` so the edge-safe web entry can use it without this\n// module's node-only surface. Re-exported here for compatibility.\nexport {\n buildFileTypeFilter,\n resolveMimeTypes,\n applyUploadConfig,\n type UploadedFileLike,\n type RawUploadPart,\n} from './upload-config'\nimport { buildFileTypeFilter } from './upload-config'\n\n/**\n * Upload options for the middleware.\n * Extends BaseUploadOptions from core (shared with @FileUpload decorator)\n * and adds Multer-specific storage options.\n */\nexport interface UploadOptions extends BaseUploadOptions {\n /** Multer storage config (default: memory storage) */\n storage?: MulterOptions['storage']\n /** Multer dest for disk storage shorthand */\n dest?: string\n}\n\nfunction createMulter(options: UploadOptions) {\n const limits: MulterOptions['limits'] = {\n fileSize: options.maxSize ?? 5 * 1024 * 1024,\n }\n\n let fileFilter: MulterOptions['fileFilter'] | undefined\n if (options.allowedTypes) {\n const typeAllowed = buildFileTypeFilter(options.allowedTypes, options.customMimeMap)\n fileFilter = (_req, file, cb) => {\n if (typeAllowed(file.mimetype, file.originalname)) {\n cb(null, true)\n } else {\n cb(new Error(`File type ${file.mimetype} is not allowed`))\n }\n }\n }\n\n const multerOptions: MulterOptions = {\n limits,\n ...(fileFilter ? { fileFilter } : {}),\n ...(options.storage ? { storage: options.storage } : {}),\n ...(options.dest ? { dest: options.dest } : {}),\n }\n\n return loadMulter()(multerOptions)\n}\n\n/**\n * Single file upload middleware. Attaches the file to `req.file`.\n *\n * @example\n * ```ts\n * @Post('/avatar')\n * @Middleware(upload.single('avatar', { maxSize: 2 * 1024 * 1024, allowedTypes: ['jpg', 'png'] }))\n * async uploadAvatar(ctx: RequestContext) {\n * ctx.json({ filename: ctx.file.originalname })\n * }\n * ```\n */\nfunction single(fieldName: string, options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return m.single(fieldName) as RequestHandler\n}\n\n/**\n * Multiple file upload middleware. Attaches files to `req.files`.\n */\nfunction array(fieldName: string, maxCount = 10, options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return m.array(fieldName, maxCount) as RequestHandler\n}\n\n/**\n * No file upload — just parse multipart form data without file fields.\n */\nfunction none(options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return m.none() as RequestHandler\n}\n\n/**\n * Removes temporary files from disk after the response is sent.\n * Attach this as Express middleware after the upload middleware.\n * Works with both `req.file` (single) and `req.files` (array).\n *\n * @example\n * ```ts\n * @Post('/process')\n * @Middleware(upload.single('document', { dest: '/tmp/uploads' }), cleanupFiles())\n * async processDocument(ctx: RequestContext) {\n * ctx.json({ ok: true })\n * }\n * ```\n */\nexport function cleanupFiles() {\n return (req: Request, res: Response, next: NextFunction) => {\n res.on('finish', async () => {\n const files: any[] = []\n\n if ((req as any).file?.path) {\n files.push((req as any).file)\n }\n if (Array.isArray((req as any).files)) {\n for (const f of (req as any).files) {\n if (f?.path) files.push(f)\n }\n }\n\n for (const file of files) {\n try {\n await unlink(file.path)\n } catch {\n // File may already be moved/deleted by the handler — ignore\n }\n }\n })\n\n next()\n }\n}\n\n/**\n * Build upload middleware from a @FileUpload decorator config.\n * Used internally by the router builder when it detects FILE_UPLOAD metadata.\n * Accepts the same FileUploadConfig interface used by the @FileUpload decorator.\n */\nexport function buildUploadMiddleware(config: FileUploadConfig): RequestHandler {\n const options: UploadOptions = {}\n if (config.maxSize) options.maxSize = config.maxSize\n if (config.allowedTypes) options.allowedTypes = config.allowedTypes\n if (config.customMimeMap) options.customMimeMap = config.customMimeMap\n\n const fieldName = config.fieldName ?? 'file'\n\n switch (config.mode) {\n case 'single':\n return single(fieldName, options)\n case 'array':\n return array(fieldName, config.maxCount ?? 10, options)\n case 'none':\n return none(options)\n }\n}\n\n/** Upload middleware factory with `.single()`, `.array()`, `.none()` methods */\nexport const upload = { single, array, none }\n"],"mappings":";;;;;;;;;;gMAeA,MAAM,gBAAkB,cAAc,OAAO,KAAK,GAAG,EAErD,IAAI,QAEJ,SAAS,YAA4B,CACnC,GAAI,QAAS,OAAO,QACpB,GAAI,CAEF,MADA,SAAU,gBAAgB,QAAQ,EAC3B,OACT,MAAQ,CACN,MAAU,MACR;sFAEF,CACF,CACF,CA0BA,SAAS,aAAa,QAAwB,CAC5C,IAAM,OAAkC,CACtC,SAAU,QAAQ,SAAW,EAAI,KAAO,IAC1C,EAEI,WACJ,GAAI,QAAQ,aAAc,CACxB,IAAM,YAAc,oBAAoB,QAAQ,aAAc,QAAQ,aAAa,EACnF,YAAc,KAAM,KAAM,KAAO,CAC3B,YAAY,KAAK,SAAU,KAAK,YAAY,EAC9C,GAAG,KAAM,EAAI,EAEb,GAAO,MAAM,aAAa,KAAK,SAAS,gBAAgB,CAAC,CAE7D,CACF,CAEA,IAAM,cAA+B,CACnC,OACA,GAAI,WAAa,CAAE,UAAW,EAAI,CAAC,EACnC,GAAI,QAAQ,QAAU,CAAE,QAAS,QAAQ,OAAQ,EAAI,CAAC,EACtD,GAAI,QAAQ,KAAO,CAAE,KAAM,QAAQ,IAAK,EAAI,CAAC,CAC/C,EAEA,OAAO,WAAW,CAAC,CAAC,aAAa,CACnC,CAcA,SAAS,OAAO,UAAmB,QAAyB,CAAC,EAAmB,CAE9E,OADU,aAAa,OAChB,CAAC,CAAC,OAAO,SAAS,CAC3B,CAKA,SAAS,MAAM,UAAmB,SAAW,GAAI,QAAyB,CAAC,EAAmB,CAE5F,OADU,aAAa,OAChB,CAAC,CAAC,MAAM,UAAW,QAAQ,CACpC,CAKA,SAAS,KAAK,QAAyB,CAAC,EAAmB,CAEzD,OADU,aAAa,OAChB,CAAC,CAAC,KAAK,CAChB,CAgBA,SAAgB,cAAe,CAC7B,OAAQ,IAAc,IAAe,OAAuB,CAC1D,IAAI,GAAG,SAAU,SAAY,CAC3B,IAAM,MAAe,CAAC,EAKtB,GAHK,IAAY,MAAM,MACrB,MAAM,KAAM,IAAY,IAAI,EAE1B,MAAM,QAAS,IAAY,KAAK,EAC7B,IAAA,IAAM,KAAM,IAAY,MACvB,GAAG,MAAM,MAAM,KAAK,CAAC,EAI7B,IAAK,IAAM,QAAQ,MACjB,GAAI,CACF,MAAM,OAAO,KAAK,IAAI,CACxB,MAAQ,CAER,CAEJ,CAAC,EAED,KAAK,CACP,CACF,CAOA,SAAgB,sBAAsB,OAA0C,CAC9E,IAAM,QAAyB,CAAC,EAC5B,OAAO,UAAS,QAAQ,QAAU,OAAO,SACzC,OAAO,eAAc,QAAQ,aAAe,OAAO,cACnD,OAAO,gBAAe,QAAQ,cAAgB,OAAO,eAEzD,IAAM,UAAY,OAAO,WAAa,OAEtC,OAAQ,OAAO,KAAf,CACE,IAAK,SACH,OAAO,OAAO,UAAW,OAAO,EAClC,IAAK,QACH,OAAO,MAAM,UAAW,OAAO,UAAY,GAAI,OAAO,EACxD,IAAK,OACH,OAAO,KAAK,OAAO,CACvB,CACF,CAGA,MAAa,OAAS,CAAE,OAAQ,MAAO,IAAK"}
1
+ {"version":3,"file":"upload.mjs","names":[],"sources":["../../../src/http/middleware/upload.ts"],"sourcesContent":["import { HttpException, HttpStatus } from '../../core/errors'\nimport { unlink } from 'node:fs/promises'\nimport { createRequire } from 'node:module'\nimport type { Request, Response, NextFunction, RequestHandler } from 'express'\nimport type { Options as MulterOptions } from 'multer'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core'\n\n// `multer` is an optional peer dependency. We load it lazily via\n// `createRequire` so importing this module never touches `multer` —\n// only constructing an upload middleware does. Adopters who never\n// call `upload.single/array/none()` or use `@FileUpload` don't need\n// `multer` installed at all.\n//\n// multer uses CJS `export = multer`, so the module is itself the\n// callable factory — no `.default` indirection on either the type\n// or the runtime side.\nconst requireFromHere = createRequire(import.meta.url)\ntype MulterFactory = typeof import('multer')\nlet _multer: MulterFactory | undefined\n\nfunction loadMulter(): MulterFactory {\n if (_multer) return _multer\n try {\n _multer = requireFromHere('multer') as MulterFactory\n return _multer\n } catch {\n throw new Error(\n \"@forinda/kickjs: file uploads require the 'multer' package, which is not installed.\\n\" +\n 'Install it as a peer dependency: pnpm add multer (or npm i multer / yarn add multer).',\n )\n }\n}\n\n// Pure upload core (MIME map, type filter, applyUploadConfig) moved to\n// `./upload-config` so the edge-safe web entry can use it without this\n// module's node-only surface. Re-exported here for compatibility.\nexport {\n buildFileTypeFilter,\n resolveMimeTypes,\n applyUploadConfig,\n type UploadedFileLike,\n type RawUploadPart,\n} from './upload-config'\nimport { buildFileTypeFilter } from './upload-config'\n\n/**\n * Upload options for the middleware.\n * Extends BaseUploadOptions from core (shared with @FileUpload decorator)\n * and adds Multer-specific storage options.\n */\nexport interface UploadOptions extends BaseUploadOptions {\n /** Multer storage config (default: memory storage) */\n storage?: MulterOptions['storage']\n /** Multer dest for disk storage shorthand */\n dest?: string\n}\n\n/**\n * Turn multer's own errors into the exceptions the other runtimes raise.\n *\n * multer reports a file over the limit as `MulterError: File too large`, a\n * plain error the handler maps to 500 — so an oversized upload was reported as\n * a server fault on Express while Fastify and h3 answered 413. The message is\n * matched to `applyUploadConfig` so the response does not depend on the engine.\n */\nfunction translateMulterErrors(mw: RequestHandler, options: UploadOptions): RequestHandler {\n const maxSize = options.maxSize ?? 5 * 1024 * 1024\n return (req, res, next) => {\n mw(req, res, (err?: unknown) => {\n if (err && (err as { code?: string }).code === 'LIMIT_FILE_SIZE') {\n const name = (err as { field?: string }).field ?? 'file'\n return next(\n new HttpException(\n HttpStatus.PAYLOAD_TOO_LARGE,\n `File ${name} exceeds the ${maxSize}-byte limit`,\n ),\n )\n }\n next(err as never)\n })\n }\n}\n\nfunction createMulter(options: UploadOptions) {\n const limits: MulterOptions['limits'] = {\n fileSize: options.maxSize ?? 5 * 1024 * 1024,\n }\n\n let fileFilter: MulterOptions['fileFilter'] | undefined\n if (options.allowedTypes) {\n const typeAllowed = buildFileTypeFilter(options.allowedTypes, options.customMimeMap)\n fileFilter = (_req, file, cb) => {\n if (typeAllowed(file.mimetype, file.originalname)) {\n cb(null, true)\n } else {\n // Same exception the Fastify and h3 path raises from\n // `applyUploadConfig`. A plain Error here reached the handler as a 500,\n // so Express blamed itself for the client's file — and leaked a multer\n // stack doing it.\n cb(\n new HttpException(\n HttpStatus.UNSUPPORTED_MEDIA_TYPE,\n `File type ${file.mimetype} is not allowed`,\n ),\n )\n }\n }\n }\n\n const multerOptions: MulterOptions = {\n limits,\n ...(fileFilter ? { fileFilter } : {}),\n ...(options.storage ? { storage: options.storage } : {}),\n ...(options.dest ? { dest: options.dest } : {}),\n }\n\n return loadMulter()(multerOptions)\n}\n\n/**\n * Single file upload middleware. Attaches the file to `req.file`.\n *\n * @example\n * ```ts\n * @Post('/avatar')\n * @Middleware(upload.single('avatar', { maxSize: 2 * 1024 * 1024, allowedTypes: ['jpg', 'png'] }))\n * async uploadAvatar(ctx: RequestContext) {\n * ctx.json({ filename: ctx.file.originalname })\n * }\n * ```\n */\nfunction single(fieldName: string, options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return translateMulterErrors(m.single(fieldName) as RequestHandler, options)\n}\n\n/**\n * Multiple file upload middleware. Attaches files to `req.files`.\n */\nfunction array(fieldName: string, maxCount = 10, options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return translateMulterErrors(m.array(fieldName, maxCount) as RequestHandler, options)\n}\n\n/**\n * No file upload — just parse multipart form data without file fields.\n */\nfunction none(options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return translateMulterErrors(m.none() as RequestHandler, options)\n}\n\n/**\n * Removes temporary files from disk after the response is sent.\n * Attach this as Express middleware after the upload middleware.\n * Works with both `req.file` (single) and `req.files` (array).\n *\n * @example\n * ```ts\n * @Post('/process')\n * @Middleware(upload.single('document', { dest: '/tmp/uploads' }), cleanupFiles())\n * async processDocument(ctx: RequestContext) {\n * ctx.json({ ok: true })\n * }\n * ```\n */\nexport function cleanupFiles() {\n return (req: Request, res: Response, next: NextFunction) => {\n res.on('finish', async () => {\n const files: any[] = []\n\n if ((req as any).file?.path) {\n files.push((req as any).file)\n }\n if (Array.isArray((req as any).files)) {\n for (const f of (req as any).files) {\n if (f?.path) files.push(f)\n }\n }\n\n for (const file of files) {\n try {\n await unlink(file.path)\n } catch {\n // File may already be moved/deleted by the handler — ignore\n }\n }\n })\n\n next()\n }\n}\n\n/**\n * Build upload middleware from a @FileUpload decorator config.\n * Used internally by the router builder when it detects FILE_UPLOAD metadata.\n * Accepts the same FileUploadConfig interface used by the @FileUpload decorator.\n */\nexport function buildUploadMiddleware(config: FileUploadConfig): RequestHandler {\n const options: UploadOptions = {}\n if (config.maxSize) options.maxSize = config.maxSize\n if (config.allowedTypes) options.allowedTypes = config.allowedTypes\n if (config.customMimeMap) options.customMimeMap = config.customMimeMap\n\n const fieldName = config.fieldName ?? 'file'\n\n switch (config.mode) {\n case 'single':\n return single(fieldName, options)\n case 'array':\n return array(fieldName, config.maxCount ?? 10, options)\n case 'none':\n return none(options)\n }\n}\n\n/** Upload middleware factory with `.single()`, `.array()`, `.none()` methods */\nexport const upload = { single, array, none }\n"],"mappings":";;;;;;;;;;4PAgBA,MAAM,gBAAkB,cAAc,OAAO,KAAK,GAAG,EAErD,IAAI,QAEJ,SAAS,YAA4B,CACnC,GAAI,QAAS,OAAO,QACpB,GAAI,CAEF,MADA,SAAU,gBAAgB,QAAQ,EAC3B,OACT,MAAQ,CACN,MAAU,MACR;sFAEF,CACF,CACF,CAkCA,SAAS,sBAAsB,GAAoB,QAAwC,CACzF,IAAM,QAAU,QAAQ,SAAW,EAAI,KAAO,KAC9C,OAAQ,IAAK,IAAK,OAAS,CACzB,GAAG,IAAK,IAAM,KAAkB,CAC9B,GAAI,KAAQ,IAA0B,OAAS,kBAAmB,CAChE,IAAM,KAAQ,IAA2B,OAAS,OAClD,OAAO,KACL,IAAI,cACF,WAAW,kBACX,QAAQ,KAAK,eAAe,QAAQ,YACtC,CACF,CACF,CACA,KAAK,GAAY,CACnB,CAAC,CACH,CACF,CAEA,SAAS,aAAa,QAAwB,CAC5C,IAAM,OAAkC,CACtC,SAAU,QAAQ,SAAW,EAAI,KAAO,IAC1C,EAEI,WACJ,GAAI,QAAQ,aAAc,CACxB,IAAM,YAAc,oBAAoB,QAAQ,aAAc,QAAQ,aAAa,EACnF,YAAc,KAAM,KAAM,KAAO,CAC3B,YAAY,KAAK,SAAU,KAAK,YAAY,EAC9C,GAAG,KAAM,EAAI,EAMb,GACE,IAAI,cACF,WAAW,uBACX,aAAa,KAAK,SAAS,gBAC7B,CACF,CAEJ,CACF,CAEA,IAAM,cAA+B,CACnC,OACA,GAAI,WAAa,CAAE,UAAW,EAAI,CAAC,EACnC,GAAI,QAAQ,QAAU,CAAE,QAAS,QAAQ,OAAQ,EAAI,CAAC,EACtD,GAAI,QAAQ,KAAO,CAAE,KAAM,QAAQ,IAAK,EAAI,CAAC,CAC/C,EAEA,OAAO,WAAW,CAAC,CAAC,aAAa,CACnC,CAcA,SAAS,OAAO,UAAmB,QAAyB,CAAC,EAAmB,CAE9E,OAAO,sBADG,aAAa,OACM,CAAC,CAAC,OAAO,SAAS,EAAqB,OAAO,CAC7E,CAKA,SAAS,MAAM,UAAmB,SAAW,GAAI,QAAyB,CAAC,EAAmB,CAE5F,OAAO,sBADG,aAAa,OACM,CAAC,CAAC,MAAM,UAAW,QAAQ,EAAqB,OAAO,CACtF,CAKA,SAAS,KAAK,QAAyB,CAAC,EAAmB,CAEzD,OAAO,sBADG,aAAa,OACM,CAAC,CAAC,KAAK,EAAqB,OAAO,CAClE,CAgBA,SAAgB,cAAe,CAC7B,OAAQ,IAAc,IAAe,OAAuB,CAC1D,IAAI,GAAG,SAAU,SAAY,CAC3B,IAAM,MAAe,CAAC,EAKtB,GAHK,IAAY,MAAM,MACrB,MAAM,KAAM,IAAY,IAAI,EAE1B,MAAM,QAAS,IAAY,KAAK,EAC7B,IAAA,IAAM,KAAM,IAAY,MACvB,GAAG,MAAM,MAAM,KAAK,CAAC,EAI7B,IAAK,IAAM,QAAQ,MACjB,GAAI,CACF,MAAM,OAAO,KAAK,IAAI,CACxB,MAAQ,CAER,CAEJ,CAAC,EAED,KAAK,CACP,CACF,CAOA,SAAgB,sBAAsB,OAA0C,CAC9E,IAAM,QAAyB,CAAC,EAC5B,OAAO,UAAS,QAAQ,QAAU,OAAO,SACzC,OAAO,eAAc,QAAQ,aAAe,OAAO,cACnD,OAAO,gBAAe,QAAQ,cAAgB,OAAO,eAEzD,IAAM,UAAY,OAAO,WAAa,OAEtC,OAAQ,OAAO,KAAf,CACE,IAAK,SACH,OAAO,OAAO,UAAW,OAAO,EAClC,IAAK,QACH,OAAO,MAAM,UAAW,OAAO,UAAY,GAAI,OAAO,EACxD,IAAK,OACH,OAAO,KAAK,OAAO,CACvB,CACF,CAGA,MAAa,OAAS,CAAE,OAAQ,MAAO,IAAK"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1,3 +1,3 @@
1
1
 
2
- import { a as ViewAdapterOptions, i as ViewAdapter } from "../../spa-1Uc4QIif.mjs";
2
+ import { a as ViewAdapterOptions, i as ViewAdapter } from "../../spa-DAoE9C2S.mjs";
3
3
  export { ViewAdapter, ViewAdapterOptions };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{Logger}from"../../core/logger.mjs";import"../../core-BGsV3RjD.mjs";import{t as defineAdapter}from"../../define-adapter-VXKsMUNb.mjs";import{resolve}from"node:path";const log=Logger.for(`ViewEngine`),ViewAdapter=defineAdapter({name:`ViewAdapter`,build:options=>({beforeMount({app}){let{engine,ext,viewsDir=`src/views`}=options;engine.__express?app.engine(ext,engine.__express):typeof engine.renderFile==`function`?app.engine(ext,(path,opts,callback)=>{engine.renderFile(path,opts,callback)}):(typeof engine==`function`||log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`),app.engine(ext,engine)),app.set(`view engine`,ext),app.set(`views`,resolve(viewsDir)),log.debug(`View engine: ${ext} (${resolve(viewsDir)})`)}})});export{ViewAdapter};
11
+ import{Logger}from"../../core/logger.mjs";import"../../core-B1PK7S5p.mjs";import{t as defineAdapter}from"../../define-adapter-DUBru2GV.mjs";import{resolve}from"node:path";const log=Logger.for(`ViewEngine`),ViewAdapter=defineAdapter({name:`ViewAdapter`,build:options=>({beforeMount({app}){let{engine,ext,viewsDir=`src/views`}=options;engine.__express?app.engine(ext,engine.__express):typeof engine.renderFile==`function`?app.engine(ext,(path,opts,callback)=>{engine.renderFile(path,opts,callback)}):(typeof engine==`function`||log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`),app.engine(ext,engine)),app.set(`view engine`,ext),app.set(`views`,resolve(viewsDir)),log.debug(`View engine: ${ext} (${resolve(viewsDir)})`)}})});export{ViewAdapter};
12
12
  //# sourceMappingURL=views.mjs.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,4 +8,4 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- 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-CrSAtLqV.mjs";export{FILTER_OPERATORS,buildQueryParams,getQueryParsingDefaults,parseFilters,parsePagination,parseQuery,parseSearchQuery,parseSort,resetQueryParsingDefaults,setQueryParsingDefaults};
11
+ 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-u5LBf5yW.mjs";export{FILTER_OPERATORS,buildQueryParams,getQueryParsingDefaults,parseFilters,parsePagination,parseQuery,parseSearchQuery,parseSort,resetQueryParsingDefaults,setQueryParsingDefaults};
@@ -1,3 +1,3 @@
1
1
 
2
- import { i as buildRouteTable, n as _setExternalContributorSources, r as assertRouteUnique, t as BuildRoutesOptions } from "../router-builder-BlReYbNA.mjs";
2
+ import { i as buildRouteTable, n as _setExternalContributorSources, r as assertRouteUnique, t as BuildRoutesOptions } from "../router-builder-DUi2RHBv.mjs";
3
3
  export { BuildRoutesOptions, _setExternalContributorSources, assertRouteUnique, buildRouteTable };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,4 +8,4 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"../router-builder-A4jcCl4J.mjs";export{_setExternalContributorSources,assertRouteUnique,buildRouteTable};
11
+ import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"../router-builder-Dqs8ITU7.mjs";export{_setExternalContributorSources,assertRouteUnique,buildRouteTable};
@@ -1,5 +1,5 @@
1
1
 
2
- import { f as HttpRuntime } from "../../adapter-DB1jE7nd.mjs";
2
+ import { f as HttpRuntime } from "../../adapter-OjEKLePS.mjs";
3
3
  import { IncomingMessage, ServerResponse } from "node:http";
4
4
  //#region src/http/runtimes/fastify.d.ts
5
5
  interface FastifyReplyLike {
@@ -16,6 +16,8 @@ interface FastifyRequestLike {
16
16
  body?: unknown;
17
17
  params?: unknown;
18
18
  query?: unknown;
19
+ /** Request headers, as Fastify normalises them (lowercased keys). */
20
+ headers?: Record<string, string | string[] | undefined>;
19
21
  /** Present once @fastify/multipart is registered — async iterator of parts. */
20
22
  parts?: () => AsyncIterableIterator<MultipartPart>;
21
23
  isMultipart?: () => boolean;
@@ -33,7 +35,17 @@ interface FastifyAppLike {
33
35
  setErrorHandler(handler: (err: unknown, req: FastifyRequestLike, reply: FastifyReplyLike) => void): unknown;
34
36
  ready(): Promise<void>;
35
37
  routing(req: IncomingMessage, res: ServerResponse): void;
38
+ /**
39
+ * Fastify's content-type parser registry. Typed here rather than reached for
40
+ * with a cast so the callback shape is checked at the call site — these
41
+ * parsers decide what `ctx.body` is on this runtime.
42
+ */
43
+ addContentTypeParser?(matcher: string | RegExp | string[], opts: {
44
+ parseAs: 'string' | 'buffer';
45
+ }, parser: FastifyBodyParser): unknown;
36
46
  }
47
+ /** `done`-style callback Fastify hands a content-type parser. */
48
+ type FastifyBodyParser = (req: FastifyRequestLike, body: string | Buffer, done: (err: Error | null, body?: unknown) => void) => void;
37
49
  interface MultipartPart {
38
50
  type: 'file' | 'field';
39
51
  fieldname: string;
@@ -1 +1 @@
1
- {"version":3,"file":"fastify.d.mts","names":[],"sources":["../../../src/http/runtimes/fastify.ts"],"mappings":";;;;UA6CU;EACR,KAAK,qBAAqB;EAC1B,KAAK,oBAAoB;EACzB,OAAO,cAAc,iBAAiB;EACtC,KAAK,sBAAsB;EAC3B;WACS;WACA,KAAK;;UAEN;EACR,KAAK;EACL;EACA;EACA;;EAEA,cAAc,sBAAsB;EACpC;;KAEG,kBAAkB,SAAS,oBAAoB,OAAO,4BAA4B;UAC7E;EACR,SAAS,iBAAiB,iBAAiB,gBAAgB;EAC3D,IAAI,eAAe;EACnB,MAAM;IAAQ;IAAgB;IAAa,SAAS;;EACpD,mBAAmB,SAAS;EAC5B,gBACE,UAAU,cAAc,KAAK,oBAAoB,OAAO;EAE1D,SAAS;EACT,QAAQ,KAAK,iBAAiB,KAAK;;UAY3B;EACR;EACA;EACA;EACA;EACA;EACA,iBAAiB,QAAQ;;;;;;;iBAsLX,kBAAkB,YAAY;;;;;;;;UA0I7B;EACf,2BAA2B;EAC3B,4BAA4B;EAC5B,uBAAuB"}
1
+ {"version":3,"file":"fastify.d.mts","names":[],"sources":["../../../src/http/runtimes/fastify.ts"],"mappings":";;;;UA+CU;EACR,KAAK,qBAAqB;EAC1B,KAAK,oBAAoB;EACzB,OAAO,cAAc,iBAAiB;EACtC,KAAK,sBAAsB;EAC3B;WACS;WACA,KAAK;;UAEN;EACR,KAAK;EACL;EACA;EACA;;EAEA,UAAU;;EAEV,cAAc,sBAAsB;EACpC;;KAEG,kBAAkB,SAAS,oBAAoB,OAAO,4BAA4B;UAC7E;EACR,SAAS,iBAAiB,iBAAiB,gBAAgB;EAC3D,IAAI,eAAe;EACnB,MAAM;IAAQ;IAAgB;IAAa,SAAS;;EACpD,mBAAmB,SAAS;EAC5B,gBACE,UAAU,cAAc,KAAK,oBAAoB,OAAO;EAE1D,SAAS;EACT,QAAQ,KAAK,iBAAiB,KAAK;;;;;;EAMnC,sBACE,kBAAkB,mBAClB;IAAQ;KACR,QAAQ;;;KAQP,qBACH,KAAK,oBACL,eAAe,QACf,OAAO,KAAK,cAAc;UAYlB;EACR;EACA;EACA;EACA;EACA;EACA,iBAAiB,QAAQ;;;;;;;iBAsLX,kBAAkB,YAAY;;;;;;;;UA6M7B;EACf,2BAA2B;EAC3B,4BAA4B;EAC5B,uBAAuB"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v7.4.0
2
+ * @forinda/kickjs v8.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,6 +8,6 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{i as requestStore,t as RequestContext}from"../../context-Dh01xr3X.mjs";import{a as disposeRequestStore,i as createRequestStore,t as applyHandlerResult}from"../../reply-DVH0keKZ.mjs";import{validate}from"../middleware/validate.mjs";import{t as applyUploadConfig}from"../../upload-config-BvSLBwom.mjs";import"../middleware/upload.mjs";import{createRequire}from"node:module";const peerRequire=createRequire(import.meta.url),loadPeer=name=>{let mod=peerRequire(name);return mod&&mod.__esModule&&mod.default?mod.default:mod.default??mod},QUEUE=Symbol(`kickjs.fastify.connectQueue`),READY=Symbol(`kickjs.fastify.ready`),NEXT=Symbol(`kickjs.fastify.next`),NEEDS_MULTIPART=Symbol(`kickjs.fastify.needsMultipart`),NOOP_NEXT=()=>{};var FastifyReplyDriver=class{reply;constructor(reply){this.reply=reply}status(code){return this.reply.code(code),this}json(data){return this.reply.send(data),this}send(data){return this.reply.send(data),this}type(contentType){return this.reply.type(contentType),this}setHeader(name,value){return this.reply.header(name,value),this}render(){throw Error(`ctx.render() is not supported on the Fastify runtime (no view engine).`)}writeHead(statusCode,headers){return this.reply.hijack(),this.reply.raw.writeHead(statusCode,headers),this}flushHeaders(){return this.reply.raw.flushHeaders?.(),this}write(chunk){return this.reply.raw.write(chunk)}end(data){return this.reply.raw.end(data),this}once(event,listener){return this.reply.raw.once(event,listener),this}get headersSent(){return this.reply.sent||this.reply.raw.headersSent}};function replyDriver(reply){return new FastifyReplyDriver(reply)}function routeHandler(entry){let validator=entry.meta.validation?validate(entry.meta.validation):void 0;return async(request,reply)=>{let raw=request.raw;if(raw.body=request.body,raw.params=request.params,raw.query=request.query,entry.meta.upload&&entry.meta.upload.mode!==`none`){if(typeof request.parts!=`function`)throw Error(`@forinda/kickjs: @FileUpload on the Fastify runtime requires '@fastify/multipart'.
12
- Install it as a peer dependency: pnpm add @fastify/multipart.`);let rawParts=[],fields={};for await(let part of request.parts())part.type===`file`&&part.toBuffer?rawParts.push({fieldname:part.fieldname,filename:part.filename??``,mimetype:part.mimetype??`application/octet-stream`,buffer:await part.toBuffer()}):fields[part.fieldname]=part.value;let{file,files}=applyUploadConfig(rawParts,entry.meta.upload);raw.file=file,raw.files=files,raw.body={...raw.body,...fields}}let headerId=raw.headers[`x-request-id`],store=createRequestStore((Array.isArray(headerId)?headerId[0]:headerId)||raw.requestId);raw.requestId=store.requestId,reply.raw.once(`close`,()=>disposeRequestStore(store)),await requestStore.run(store,async()=>{let ctx=new RequestContext(raw,reply,NOOP_NEXT,replyDriver(reply));validator&&await new Promise((resolve,reject)=>{validator(raw,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||reply.sent)return}if(entry.contributorRunner&&await entry.contributorRunner(ctx),reply.sent)return;let result=await entry.handler(ctx);reply.sent||applyHandlerResult(ctx,result)})}}function fastifyRuntime(){return{name:`fastify`,createApp(options={}){let app=loadPeer(`fastify`)({logger:!1,routerOptions:{ignoreTrailingSlash:!0},trustProxy:typeof options.trustProxy!=`function`&&options.trustProxy});return app[QUEUE]=[],app},nodeHandler(app){let state=app;state[READY]||(state[READY]=(async()=>{if(state[NEEDS_MULTIPART]){let multipart=loadPeer(`@fastify/multipart`);await app.register(multipart,{limits:{fileSize:2**53-1}})}let middie=loadPeer(`@fastify/middie`);await app.register(middie);for(let{mw,opts}of state[QUEUE]??[])opts?.path===void 0?app.use(mw):app.use(opts.path,mw);await app.ready()})());let readyP=state[READY];return(req,res,next)=>{next&&(req[NEXT]=next),readyP.then(()=>app.routing(req,res)).catch(err=>{next&&next(err)})}},mountRoutes(app,table){for(let{mountPath,routes}of table)for(let entry of routes){entry.meta.upload&&(app[NEEDS_MULTIPART]=!0);let url=joinPath(mountPath,entry.path);app.route({method:entry.method,url,handler:routeHandler(entry)})}},useConnect(app,mw,opts){app[QUEUE]?.push({mw,opts})},serveStatic(app,prefix,dir){let serveStatic=loadPeer(`serve-static`);this.useConnect(app,serveStatic(dir),{path:prefix})},setNotFound(app,mw){app.setNotFoundHandler((request,reply)=>{let next=request.raw[NEXT];if(next){reply.hijack(),next();return}mw(request.raw,replyDriver(reply),NOOP_NEXT)})},setErrorHandler(app,mw){app.setErrorHandler((err,request,reply)=>{mw(err,request.raw,replyDriver(reply),NOOP_NEXT)})},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{fastifyRuntime};
11
+ import{HttpException}from"../../core/errors.mjs";import{a as disposeRequestStore,d as unsupportedMediaTypeError,i as createRequestStore,t as applyHandlerResult}from"../../reply-Ca-H1Ozr.mjs";import{i as requestStore,t as RequestContext}from"../../context-caMuzOpA.mjs";import{validate}from"../middleware/validate.mjs";import{t as applyUploadConfig}from"../../upload-config-Da_g7Rig.mjs";import"../middleware/upload.mjs";import{createRequire}from"node:module";const peerRequire=createRequire(import.meta.url),loadPeer=name=>{let mod=peerRequire(name);return mod&&mod.__esModule&&mod.default?mod.default:mod.default??mod},QUEUE=Symbol(`kickjs.fastify.connectQueue`),READY=Symbol(`kickjs.fastify.ready`),NEXT=Symbol(`kickjs.fastify.next`),NEEDS_MULTIPART=Symbol(`kickjs.fastify.needsMultipart`),NOOP_NEXT=()=>{};var FastifyReplyDriver=class{reply;constructor(reply){this.reply=reply}status(code){return this.reply.code(code),this}json(data){return this.reply.send(data),this}send(data){return this.reply.send(data),this}type(contentType){return this.reply.type(contentType),this}setHeader(name,value){return this.reply.header(name,value),this}render(){throw Error(`ctx.render() is not supported on the Fastify runtime (no view engine).`)}writeHead(statusCode,headers){return this.reply.hijack(),this.reply.raw.writeHead(statusCode,headers),this}flushHeaders(){return this.reply.raw.flushHeaders?.(),this}write(chunk){return this.reply.raw.write(chunk)}end(data){return this.reply.raw.end(data),this}once(event,listener){return this.reply.raw.once(event,listener),this}get headersSent(){return this.reply.sent||this.reply.raw.headersSent}};function replyDriver(reply){return new FastifyReplyDriver(reply)}function routeHandler(entry){let validator=entry.meta.validation?validate(entry.meta.validation):void 0;return async(request,reply)=>{let raw=request.raw;if(raw.body=request.body,raw.params=request.params,raw.query=request.query,entry.meta.upload&&entry.meta.upload.mode!==`none`){if(typeof request.parts!=`function`)throw Error(`@forinda/kickjs: @FileUpload on the Fastify runtime requires '@fastify/multipart'.
12
+ Install it as a peer dependency: pnpm add @fastify/multipart.`);let rawParts=[],fields={};for await(let part of request.parts())part.type===`file`&&part.toBuffer?rawParts.push({fieldname:part.fieldname,filename:part.filename??``,mimetype:part.mimetype??`application/octet-stream`,buffer:await part.toBuffer()}):fields[part.fieldname]=part.value;let{file,files}=applyUploadConfig(rawParts,entry.meta.upload);raw.file=file,raw.files=files,raw.body={...raw.body,...fields}}let headerId=raw.headers[`x-request-id`],store=createRequestStore((Array.isArray(headerId)?headerId[0]:headerId)||raw.requestId);raw.requestId=store.requestId,reply.raw.once(`close`,()=>disposeRequestStore(store)),await requestStore.run(store,async()=>{let ctx=new RequestContext(raw,reply,NOOP_NEXT,replyDriver(reply));validator&&await new Promise((resolve,reject)=>{validator(raw,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||reply.sent)return}if(entry.contributorRunner&&await entry.contributorRunner(ctx),reply.sent)return;let result=await entry.handler(ctx);reply.sent||applyHandlerResult(ctx,result)})}}function fastifyRuntime(){return{name:`fastify`,createApp(options={}){let app=loadPeer(`fastify`)({logger:!1,routerOptions:{ignoreTrailingSlash:!0},trustProxy:typeof options.trustProxy!=`function`&&options.trustProxy});app[QUEUE]=[];let addParser=app.addContentTypeParser?.bind(app);return addParser&&(addParser(/^application\/[\w.+-]+\+json$/,{parseAs:`string`},(_req,body,done)=>{if(body===``)return done(null,void 0);try{done(null,JSON.parse(body))}catch(err){done(HttpException.badRequest(err instanceof Error?err.message:`Request body could not be parsed`),void 0)}}),addParser(`application/x-www-form-urlencoded`,{parseAs:`string`},(_req,body,done)=>{done(null,Object.fromEntries(new URLSearchParams(body)))}),addParser(/^text\//,{parseAs:`string`},(_req,body,done)=>{done(null,body)}),addParser(`*`,{parseAs:`string`},(req,body,done)=>{if(!body)return done(null,void 0);done(unsupportedMediaTypeError(req.headers?.[`content-type`]),void 0)})),app},nodeHandler(app){let state=app;state[READY]||(state[READY]=(async()=>{if(state[NEEDS_MULTIPART]){let multipart=loadPeer(`@fastify/multipart`);await app.register(multipart,{limits:{fileSize:2**53-1}})}let middie=loadPeer(`@fastify/middie`);await app.register(middie);for(let{mw,opts}of state[QUEUE]??[])opts?.path===void 0?app.use(mw):app.use(opts.path,mw);await app.ready()})());let readyP=state[READY];return(req,res,next)=>{next&&(req[NEXT]=next),readyP.then(()=>app.routing(req,res)).catch(err=>{next&&next(err)})}},mountRoutes(app,table){for(let{mountPath,routes}of table)for(let entry of routes){entry.meta.upload&&(app[NEEDS_MULTIPART]=!0);let url=joinPath(mountPath,entry.path);app.route({method:entry.method,url,handler:routeHandler(entry)})}},useConnect(app,mw,opts){app[QUEUE]?.push({mw,opts})},serveStatic(app,prefix,dir){let serveStatic=loadPeer(`serve-static`);this.useConnect(app,serveStatic(dir),{path:prefix})},setNotFound(app,mw){app.setNotFoundHandler((request,reply)=>{let next=request.raw[NEXT];if(next){reply.hijack(),next();return}mw(request.raw,replyDriver(reply),NOOP_NEXT)})},setErrorHandler(app,mw){app.setErrorHandler((err,request,reply)=>{mw(err,request.raw,replyDriver(reply),NOOP_NEXT)})},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{fastifyRuntime};
13
13
  //# sourceMappingURL=fastify.mjs.map