@forinda/kickjs 5.7.1 → 5.9.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 (134) hide show
  1. package/dist/{adapter-CoJp35rz.d.mts → adapter-BRvrS9xR.d.mts} +119 -16
  2. package/dist/adapter-BRvrS9xR.d.mts.map +1 -0
  3. package/dist/app-module-BE7hAASB.d.mts.map +1 -1
  4. package/dist/application-BQN6JUFA.mjs +12 -0
  5. package/dist/application-BQN6JUFA.mjs.map +1 -0
  6. package/dist/{application-DmekGKp7.d.mts → application-BpRV6_s8.d.mts} +29 -9
  7. package/dist/application-BpRV6_s8.d.mts.map +1 -0
  8. package/dist/{bootstrap-Dqyit14A.mjs → bootstrap-B9xvalW_.mjs} +3 -3
  9. package/dist/{bootstrap-Dqyit14A.mjs.map → bootstrap-B9xvalW_.mjs.map} +1 -1
  10. package/dist/{bootstrap-CwP2GGu0.d.mts → bootstrap-DJ3Qutd_.d.mts} +2 -2
  11. package/dist/bootstrap-DJ3Qutd_.d.mts.map +1 -0
  12. package/dist/cache-XrNWj1KQ.d.mts.map +1 -1
  13. package/dist/config/index.mjs +2 -2
  14. package/dist/{config-DsD1NCSn.mjs → config-BfLro7F5.mjs} +3 -3
  15. package/dist/{config-DsD1NCSn.mjs.map → config-BfLro7F5.mjs.map} +1 -1
  16. package/dist/{container-jPCvIZH2.mjs → container-B1X8WRwl.mjs} +3 -3
  17. package/dist/{container-jPCvIZH2.mjs.map → container-B1X8WRwl.mjs.map} +1 -1
  18. package/dist/container-BZKeHc5X.d.mts.map +1 -1
  19. package/dist/context-CtiMB_tb.mjs +12 -0
  20. package/dist/context-CtiMB_tb.mjs.map +1 -0
  21. package/dist/context-aocg9gO-.d.mts.map +1 -1
  22. package/dist/context-decorator-BYf8HdMl.d.mts.map +1 -1
  23. package/dist/core/adapter.d.mts +2 -2
  24. package/dist/core/adapter.mjs +1 -1
  25. package/dist/core/app-module.mjs +1 -1
  26. package/dist/core/cache.mjs +1 -1
  27. package/dist/core/cache.mjs.map +1 -1
  28. package/dist/core/container.mjs +2 -2
  29. package/dist/core/cron.mjs +2 -2
  30. package/dist/core/cron.mjs.map +1 -1
  31. package/dist/core/decorators.mjs +2 -2
  32. package/dist/core/decorators.mjs.map +1 -1
  33. package/dist/core/errors.mjs +2 -2
  34. package/dist/core/errors.mjs.map +1 -1
  35. package/dist/core/interfaces.mjs +1 -1
  36. package/dist/core/interfaces.mjs.map +1 -1
  37. package/dist/core/logger.mjs +1 -1
  38. package/dist/core/logger.mjs.map +1 -1
  39. package/dist/core/path.mjs +1 -1
  40. package/dist/core/path.mjs.map +1 -1
  41. package/dist/core/plugin.d.mts +1 -1
  42. package/dist/core/plugin.mjs +1 -1
  43. package/dist/core/reactivity.d.mts +1 -1
  44. package/dist/core/reactivity.mjs +2 -2
  45. package/dist/core/reactivity.mjs.map +1 -1
  46. package/dist/{core-DR36jGxB.mjs → core-Cd5c8LhS.mjs} +3 -3
  47. package/dist/{core-DR36jGxB.mjs.map → core-Cd5c8LhS.mjs.map} +1 -1
  48. package/dist/cors-BfgqfLPd.d.mts.map +1 -1
  49. package/dist/cron-GNoYydfg.d.mts.map +1 -1
  50. package/dist/csrf-DiK9zept.d.mts.map +1 -1
  51. package/dist/decorators-BYi8lB37.d.mts.map +1 -1
  52. package/dist/{env-CDmi8uj3.mjs → env-BgiV8KKj.mjs} +3 -3
  53. package/dist/{env-CDmi8uj3.mjs.map → env-BgiV8KKj.mjs.map} +1 -1
  54. package/dist/errors-uaHCeXRz.d.mts.map +1 -1
  55. package/dist/execution-context-DKnt61pY.d.mts.map +1 -1
  56. package/dist/helmet-CaMbjzBG.d.mts.map +1 -1
  57. package/dist/http/application.d.mts +1 -1
  58. package/dist/http/application.mjs +2 -2
  59. package/dist/http/bootstrap.d.mts +1 -1
  60. package/dist/http/bootstrap.mjs +2 -2
  61. package/dist/http/context.mjs +2 -2
  62. package/dist/http/middleware/cors.mjs +1 -1
  63. package/dist/http/middleware/cors.mjs.map +1 -1
  64. package/dist/http/middleware/csrf.mjs +1 -1
  65. package/dist/http/middleware/csrf.mjs.map +1 -1
  66. package/dist/http/middleware/error-handler.mjs +2 -2
  67. package/dist/http/middleware/error-handler.mjs.map +1 -1
  68. package/dist/http/middleware/helmet.mjs +1 -1
  69. package/dist/http/middleware/helmet.mjs.map +1 -1
  70. package/dist/http/middleware/rate-limit.mjs +2 -2
  71. package/dist/http/middleware/rate-limit.mjs.map +1 -1
  72. package/dist/http/middleware/request-id.mjs +1 -1
  73. package/dist/http/middleware/request-id.mjs.map +1 -1
  74. package/dist/http/middleware/request-logger.mjs +2 -2
  75. package/dist/http/middleware/request-logger.mjs.map +1 -1
  76. package/dist/http/middleware/session.mjs +1 -1
  77. package/dist/http/middleware/session.mjs.map +1 -1
  78. package/dist/http/middleware/spa.d.mts +1 -1
  79. package/dist/http/middleware/spa.mjs +2 -2
  80. package/dist/http/middleware/spa.mjs.map +1 -1
  81. package/dist/http/middleware/upload.mjs +1 -1
  82. package/dist/http/middleware/upload.mjs.map +1 -1
  83. package/dist/http/middleware/validate.mjs +2 -2
  84. package/dist/http/middleware/validate.mjs.map +1 -1
  85. package/dist/http/middleware/views.d.mts +1 -1
  86. package/dist/http/middleware/views.mjs +2 -2
  87. package/dist/http/middleware/views.mjs.map +1 -1
  88. package/dist/http/query/index.mjs +2 -2
  89. package/dist/http/router-builder.d.mts +1 -1
  90. package/dist/http/router-builder.mjs +2 -2
  91. package/dist/http/router-builder.mjs.map +1 -1
  92. package/dist/index-B9cyOQAe.d.mts.map +1 -1
  93. package/dist/index-DM1PteZT.d.mts.map +1 -1
  94. package/dist/{index-CkKLXZHg.d.mts → index-qZqR_Mxr.d.mts} +3 -3
  95. package/dist/index-qZqR_Mxr.d.mts.map +1 -0
  96. package/dist/index.d.mts +10 -10
  97. package/dist/index.mjs +2 -2
  98. package/dist/index.mjs.map +1 -1
  99. package/dist/interfaces-B50n7dwl.d.mts.map +1 -1
  100. package/dist/logger-CLVRZf2N.d.mts.map +1 -1
  101. package/dist/{metadata-Chvfoj23.mjs → metadata-BNNMsb9P.mjs} +2 -2
  102. package/dist/{metadata-Chvfoj23.mjs.map → metadata-BNNMsb9P.mjs.map} +1 -1
  103. package/dist/path-Diviu6hv.d.mts.map +1 -1
  104. package/dist/{plugin-NMHVmDRF.d.mts → plugin-CyoGnRbq.d.mts} +8 -7
  105. package/dist/plugin-CyoGnRbq.d.mts.map +1 -0
  106. package/dist/{query-CtctnbW6.mjs → query-Du-Evuhc.mjs} +2 -2
  107. package/dist/{query-CtctnbW6.mjs.map → query-Du-Evuhc.mjs.map} +1 -1
  108. package/dist/{reactivity-CfJceo4H.d.mts → reactivity-BRvj7bdG.d.mts} +14 -1
  109. package/dist/reactivity-BRvj7bdG.d.mts.map +1 -0
  110. package/dist/request-id-BgglvZHN.d.mts.map +1 -1
  111. package/dist/request-logger-CWHbAzTE.d.mts.map +1 -1
  112. package/dist/{router-builder-BfCCeQB-.d.mts → router-builder-R6dyZ-A3.d.mts} +2 -2
  113. package/dist/router-builder-R6dyZ-A3.d.mts.map +1 -0
  114. package/dist/session-N0hActOO.d.mts.map +1 -1
  115. package/dist/{spa-DIXtRBj-.d.mts → spa-DFxiK7jQ.d.mts} +2 -2
  116. package/dist/spa-DFxiK7jQ.d.mts.map +1 -0
  117. package/dist/upload-5goxbO9A.d.mts.map +1 -1
  118. package/dist/validate-HwAyWTwH.d.mts.map +1 -1
  119. package/dist/{views-DDT9vghc.d.mts → views-DGbyY_oV.d.mts} +2 -2
  120. package/dist/views-DGbyY_oV.d.mts.map +1 -0
  121. package/package.json +4 -4
  122. package/dist/adapter-CoJp35rz.d.mts.map +0 -1
  123. package/dist/application-DbUe_4St.mjs +0 -12
  124. package/dist/application-DbUe_4St.mjs.map +0 -1
  125. package/dist/application-DmekGKp7.d.mts.map +0 -1
  126. package/dist/bootstrap-CwP2GGu0.d.mts.map +0 -1
  127. package/dist/context-CPW8cxav.mjs +0 -12
  128. package/dist/context-CPW8cxav.mjs.map +0 -1
  129. package/dist/index-CkKLXZHg.d.mts.map +0 -1
  130. package/dist/plugin-NMHVmDRF.d.mts.map +0 -1
  131. package/dist/reactivity-CfJceo4H.d.mts.map +0 -1
  132. package/dist/router-builder-BfCCeQB-.d.mts.map +0 -1
  133. package/dist/spa-DIXtRBj-.d.mts.map +0 -1
  134. package/dist/views-DDT9vghc.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"execution-context-DKnt61pY.d.mts","names":[],"sources":["../src/core/execution-context.ts"],"mappings":";;;AAmCA;;;;;AAGA;;;;;;;;;;;;;;;;;;;AAYA;;;;;;;;UAfiB,WAAA;;KAGL,SAAA,yCAAkD,CAAA,eAAgB,WAAA,GAC1E,WAAA,CAAY,CAAA,IACZ,QAAA;;;;;;;;;UAUa,gBAAA;EAIsB;EAFrC,GAAA,mBAAsB,GAAA,EAAK,CAAA,GAAI,SAAA,CAAU,CAAA;EAEX;EAA9B,GAAA,mBAAsB,GAAA,EAAK,CAAA,EAAG,KAAA,EAAO,SAAA,CAAU,CAAA;EAE7B;EAAA,SAAT,SAAA;AAAA"}
1
+ {"version":3,"file":"execution-context-DKnt61pY.d.mts","names":[],"sources":["../src/core/execution-context.ts"],"mappings":";;;AAmCA;;;;AAA4B;AAG5B;;;;;;;;;;;;;;;;;;AAEY;AAUZ;;;;;;;;UAfiB,WAAA;;KAGL,SAAA,yCAAkD,CAAA,eAAgB,WAAA,GAC1E,WAAA,CAAY,CAAA,IACZ,QAAA;;;;;;;;;UAUa,gBAAA;EAIsB;EAFrC,GAAA,mBAAsB,GAAA,EAAK,CAAA,GAAI,SAAA,CAAU,CAAA;EAEX;EAA9B,GAAA,mBAAsB,GAAA,EAAK,CAAA,EAAG,KAAA,EAAO,SAAA,CAAU,CAAA;EAE7B;EAAA,SAAT,SAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"helmet-CaMbjzBG.d.mts","names":[],"sources":["../src/http/middleware/helmet.ts"],"mappings":";;;;UAEiB,aAAA;;EAEf,qBAAA,aAAkC,MAAA;EAFN;EAI5B,OAAA;EAFwC;EAIxC,UAAA;EAJkC;EAMlC,IAAA;IAAiB,MAAA;IAAiB,iBAAA;IAA6B,OAAA;EAAA;EAAA;EAE/D,WAAA;EAEA;EAAA,aAAA;EAIA;EAFA,cAAA;EAES;EAAT,SAAA;AAAA;;;;;;;;;;;;iBAcc,MAAA,CAAO,OAAA,GAAS,aAAA,IAYtB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA"}
1
+ {"version":3,"file":"helmet-CaMbjzBG.d.mts","names":[],"sources":["../src/http/middleware/helmet.ts"],"mappings":";;;;UAEiB,aAAA;;EAEf,qBAAA,aAAkC,MAAM;EAFZ;EAI5B,OAAA;EAFwC;EAIxC,UAAA;EAJkC;EAMlC,IAAA;IAAiB,MAAA;IAAiB,iBAAA;IAA6B,OAAA;EAAA;EAAA;EAE/D,WAAA;EAEA;EAAA,aAAA;EAIA;EAFA,cAAA;EAES;EAAT,SAAA;AAAA;;;;;;;;;;;;iBAcc,MAAA,CAAO,OAAA,GAAS,aAAA,IAYtB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-DmekGKp7.mjs";
2
+ import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-BpRV6_s8.mjs";
3
3
  export { Application, ApplicationOptions, MiddlewareEntry };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.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{t as Application}from"../application-DbUe_4St.mjs";export{Application};
11
+ import{t as Application}from"../application-BQN6JUFA.mjs";export{Application};
@@ -1,3 +1,3 @@
1
1
 
2
- import { t as bootstrap } from "../bootstrap-CwP2GGu0.mjs";
2
+ import { t as bootstrap } from "../bootstrap-DJ3Qutd_.mjs";
3
3
  export { bootstrap };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.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{t as bootstrap}from"../bootstrap-Dqyit14A.mjs";export{bootstrap};
11
+ import{t as bootstrap}from"../bootstrap-B9xvalW_.mjs";export{bootstrap};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.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{t as RequestContext}from"../context-CPW8cxav.mjs";export{RequestContext};
11
+ import{t as RequestContext}from"../context-CtiMB_tb.mjs";export{RequestContext};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"cors.mjs","names":[],"sources":["../../../src/http/middleware/cors.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface CorsOptions {\n /** Allowed origin(s). `true` reflects request origin, `'*'` allows all, `false` rejects all. Default: `false` (restrictive) */\n origin?: boolean | string | RegExp | (string | RegExp)[]\n /** Allowed HTTP methods. Default: `['GET','HEAD','PUT','PATCH','POST','DELETE']` */\n methods?: string[]\n /** Allowed request headers. Default: reflects `Access-Control-Request-Headers` */\n allowedHeaders?: string[]\n /** Headers exposed to the browser. Default: none */\n exposedHeaders?: string[]\n /** Allow credentials (cookies, authorization). Default: false */\n credentials?: boolean\n /** Preflight cache duration in seconds. Default: 86400 (24h) */\n maxAge?: number\n /** Handle preflight OPTIONS requests. Default: true */\n preflight?: boolean\n}\n\nconst DEFAULT_METHODS = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']\n\nfunction isOriginAllowed(requestOrigin: string, allowed: CorsOptions['origin']): boolean {\n if (allowed === true || allowed === '*') return true\n if (typeof allowed === 'string') return requestOrigin === allowed\n if (allowed instanceof RegExp) return allowed.test(requestOrigin)\n if (Array.isArray(allowed)) {\n return allowed.some((pattern) =>\n typeof pattern === 'string' ? requestOrigin === pattern : pattern.test(requestOrigin),\n )\n }\n return false\n}\n\n/**\n * CORS middleware with correct spec behavior.\n *\n * - Reflects origin from allowlist (sets `Vary: Origin`)\n * - Handles preflight `OPTIONS` with `204 No Content`\n * - Supports string, regex, and array origin matching\n *\n * @example\n * ```ts\n * // Allow all origins\n * bootstrap({ middleware: [cors(), express.json()] })\n *\n * // Allowlist specific origins\n * bootstrap({\n * middleware: [\n * cors({\n * origin: ['https://app.example.com', /\\.example\\.com$/],\n * credentials: true,\n * }),\n * express.json(),\n * ],\n * })\n * ```\n */\nexport function cors(options: CorsOptions = {}) {\n const {\n origin = false,\n methods = DEFAULT_METHODS,\n allowedHeaders,\n exposedHeaders,\n credentials = false,\n maxAge = 86400,\n preflight = true,\n } = options\n\n return (req: Request, res: Response, next: NextFunction) => {\n const requestOrigin = req.headers.origin\n\n // Determine the Access-Control-Allow-Origin value\n if (origin === '*' && !credentials) {\n res.setHeader('Access-Control-Allow-Origin', '*')\n } else if (requestOrigin) {\n if (isOriginAllowed(requestOrigin, origin)) {\n res.setHeader('Access-Control-Allow-Origin', requestOrigin)\n // Must vary by Origin when reflecting — CDN/proxy correctness\n res.setHeader('Vary', 'Origin')\n }\n }\n\n if (credentials) {\n res.setHeader('Access-Control-Allow-Credentials', 'true')\n }\n\n if (exposedHeaders?.length) {\n res.setHeader('Access-Control-Expose-Headers', exposedHeaders.join(', '))\n }\n\n // Preflight handling\n if (preflight && req.method === 'OPTIONS') {\n res.setHeader('Access-Control-Allow-Methods', methods.join(', '))\n\n const reqHeaders = allowedHeaders ?? req.headers['access-control-request-headers']\n if (reqHeaders) {\n const value = Array.isArray(reqHeaders) ? reqHeaders.join(', ') : reqHeaders\n res.setHeader('Access-Control-Allow-Headers', value)\n }\n\n if (maxAge) {\n res.setHeader('Access-Control-Max-Age', String(maxAge))\n }\n\n res.statusCode = 204\n res.setHeader('Content-Length', '0')\n res.end()\n return\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAM,gBAAkB,CAAC,MAAO,OAAQ,MAAO,QAAS,OAAQ,SAAS,CAEzE,SAAS,gBAAgB,cAAuB,QAAyC,CASvF,OARI,UAAY,IAAQ,UAAY,IAAY,GAC5C,OAAO,SAAY,SAAiB,gBAAkB,QACtD,mBAAmB,OAAe,QAAQ,KAAK,cAAc,CAC7D,MAAM,QAAQ,QAAQ,CACjB,QAAQ,KAAM,SACnB,OAAO,SAAY,SAAW,gBAAkB,QAAU,QAAQ,KAAK,cAAc,CACtF,CAEI,GA2BT,SAAgB,KAAK,QAAuB,EAAE,CAAE,CAC9C,GAAM,CACJ,OAAS,GACT,QAAU,gBACV,eACA,eACA,YAAc,GACd,OAAS,MACT,UAAY,IACV,QAEJ,OAAQ,IAAc,IAAe,OAAuB,CAC1D,IAAM,cAAgB,IAAI,QAAQ,OAsBlC,GAnBI,SAAW,KAAO,CAAC,YACrB,IAAI,UAAU,8BAA+B,IAAI,CACxC,eACL,gBAAgB,cAAe,OAAO,GACxC,IAAI,UAAU,8BAA+B,cAAc,CAE3D,IAAI,UAAU,OAAQ,SAAS,EAI/B,aACF,IAAI,UAAU,mCAAoC,OAAO,CAGvD,gBAAgB,QAClB,IAAI,UAAU,gCAAiC,eAAe,KAAK,KAAK,CAAC,CAIvE,WAAa,IAAI,SAAW,UAAW,CACzC,IAAI,UAAU,+BAAgC,QAAQ,KAAK,KAAK,CAAC,CAEjE,IAAM,WAAa,gBAAkB,IAAI,QAAQ,kCACjD,GAAI,WAAY,CACd,IAAM,MAAQ,MAAM,QAAQ,WAAW,CAAG,WAAW,KAAK,KAAK,CAAG,WAClE,IAAI,UAAU,+BAAgC,MAAM,CAGlD,QACF,IAAI,UAAU,yBAA0B,OAAO,OAAO,CAAC,CAGzD,IAAI,WAAa,IACjB,IAAI,UAAU,iBAAkB,IAAI,CACpC,IAAI,KAAK,CACT,OAGF,MAAM"}
1
+ {"version":3,"file":"cors.mjs","names":[],"sources":["../../../src/http/middleware/cors.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface CorsOptions {\n /** Allowed origin(s). `true` reflects request origin, `'*'` allows all, `false` rejects all. Default: `false` (restrictive) */\n origin?: boolean | string | RegExp | (string | RegExp)[]\n /** Allowed HTTP methods. Default: `['GET','HEAD','PUT','PATCH','POST','DELETE']` */\n methods?: string[]\n /** Allowed request headers. Default: reflects `Access-Control-Request-Headers` */\n allowedHeaders?: string[]\n /** Headers exposed to the browser. Default: none */\n exposedHeaders?: string[]\n /** Allow credentials (cookies, authorization). Default: false */\n credentials?: boolean\n /** Preflight cache duration in seconds. Default: 86400 (24h) */\n maxAge?: number\n /** Handle preflight OPTIONS requests. Default: true */\n preflight?: boolean\n}\n\nconst DEFAULT_METHODS = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']\n\nfunction isOriginAllowed(requestOrigin: string, allowed: CorsOptions['origin']): boolean {\n if (allowed === true || allowed === '*') return true\n if (typeof allowed === 'string') return requestOrigin === allowed\n if (allowed instanceof RegExp) return allowed.test(requestOrigin)\n if (Array.isArray(allowed)) {\n return allowed.some((pattern) =>\n typeof pattern === 'string' ? requestOrigin === pattern : pattern.test(requestOrigin),\n )\n }\n return false\n}\n\n/**\n * CORS middleware with correct spec behavior.\n *\n * - Reflects origin from allowlist (sets `Vary: Origin`)\n * - Handles preflight `OPTIONS` with `204 No Content`\n * - Supports string, regex, and array origin matching\n *\n * @example\n * ```ts\n * // Allow all origins\n * bootstrap({ middleware: [cors(), express.json()] })\n *\n * // Allowlist specific origins\n * bootstrap({\n * middleware: [\n * cors({\n * origin: ['https://app.example.com', /\\.example\\.com$/],\n * credentials: true,\n * }),\n * express.json(),\n * ],\n * })\n * ```\n */\nexport function cors(options: CorsOptions = {}) {\n const {\n origin = false,\n methods = DEFAULT_METHODS,\n allowedHeaders,\n exposedHeaders,\n credentials = false,\n maxAge = 86400,\n preflight = true,\n } = options\n\n return (req: Request, res: Response, next: NextFunction) => {\n const requestOrigin = req.headers.origin\n\n // Determine the Access-Control-Allow-Origin value\n if (origin === '*' && !credentials) {\n res.setHeader('Access-Control-Allow-Origin', '*')\n } else if (requestOrigin) {\n if (isOriginAllowed(requestOrigin, origin)) {\n res.setHeader('Access-Control-Allow-Origin', requestOrigin)\n // Must vary by Origin when reflecting — CDN/proxy correctness\n res.setHeader('Vary', 'Origin')\n }\n }\n\n if (credentials) {\n res.setHeader('Access-Control-Allow-Credentials', 'true')\n }\n\n if (exposedHeaders?.length) {\n res.setHeader('Access-Control-Expose-Headers', exposedHeaders.join(', '))\n }\n\n // Preflight handling\n if (preflight && req.method === 'OPTIONS') {\n res.setHeader('Access-Control-Allow-Methods', methods.join(', '))\n\n const reqHeaders = allowedHeaders ?? req.headers['access-control-request-headers']\n if (reqHeaders) {\n const value = Array.isArray(reqHeaders) ? reqHeaders.join(', ') : reqHeaders\n res.setHeader('Access-Control-Allow-Headers', value)\n }\n\n if (maxAge) {\n res.setHeader('Access-Control-Max-Age', String(maxAge))\n }\n\n res.statusCode = 204\n res.setHeader('Content-Length', '0')\n res.end()\n return\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAM,gBAAkB,CAAC,MAAO,OAAQ,MAAO,QAAS,OAAQ,QAAQ,EAExE,SAAS,gBAAgB,cAAuB,QAAyC,CASvF,OARI,UAAY,IAAQ,UAAY,IAAY,GAC5C,OAAO,SAAY,SAAiB,gBAAkB,QACtD,mBAAmB,OAAe,QAAQ,KAAK,aAAa,EAC5D,MAAM,QAAQ,OAAO,EAChB,QAAQ,KAAM,SACnB,OAAO,SAAY,SAAW,gBAAkB,QAAU,QAAQ,KAAK,aAAa,CACtF,EAEK,EACT,CA0BA,SAAgB,KAAK,QAAuB,CAAC,EAAG,CAC9C,GAAM,CACJ,OAAS,GACT,QAAU,gBACV,eACA,eACA,YAAc,GACd,OAAS,MACT,UAAY,IACV,QAEJ,OAAQ,IAAc,IAAe,OAAuB,CAC1D,IAAM,cAAgB,IAAI,QAAQ,OAsBlC,GAnBI,SAAW,KAAO,CAAC,YACrB,IAAI,UAAU,8BAA+B,GAAG,EACvC,eACL,gBAAgB,cAAe,MAAM,IACvC,IAAI,UAAU,8BAA+B,aAAa,EAE1D,IAAI,UAAU,OAAQ,QAAQ,GAI9B,aACF,IAAI,UAAU,mCAAoC,MAAM,EAGtD,gBAAgB,QAClB,IAAI,UAAU,gCAAiC,eAAe,KAAK,IAAI,CAAC,EAItE,WAAa,IAAI,SAAW,UAAW,CACzC,IAAI,UAAU,+BAAgC,QAAQ,KAAK,IAAI,CAAC,EAEhE,IAAM,WAAa,gBAAkB,IAAI,QAAQ,kCACjD,GAAI,WAAY,CACd,IAAM,MAAQ,MAAM,QAAQ,UAAU,EAAI,WAAW,KAAK,IAAI,EAAI,WAClE,IAAI,UAAU,+BAAgC,KAAK,CACrD,CAEI,QACF,IAAI,UAAU,yBAA0B,OAAO,MAAM,CAAC,EAGxD,IAAI,WAAa,IACjB,IAAI,UAAU,iBAAkB,GAAG,EACnC,IAAI,IAAI,EACR,MACF,CAEA,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"csrf.mjs","names":[],"sources":["../../../src/http/middleware/csrf.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport interface CsrfOptions {\n /** Cookie name for the CSRF token (default: '_csrf') */\n cookie?: string\n /** Header name to check for the token (default: 'x-csrf-token') */\n header?: string\n /** HTTP methods that require CSRF validation (default: POST, PUT, PATCH, DELETE) */\n methods?: string[]\n /** Paths to exclude from CSRF checks (e.g. webhooks) */\n ignorePaths?: string[]\n /** Token byte length before hex encoding (default: 32 = 64 hex chars) */\n tokenLength?: number\n /** Cookie options */\n cookieOptions?: {\n httpOnly?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n secure?: boolean\n path?: string\n }\n}\n\n/**\n * Double-submit cookie CSRF protection middleware.\n *\n * On every request, sets a CSRF token cookie. For state-changing methods\n * (POST, PUT, PATCH, DELETE), validates that the request header matches\n * the cookie value.\n *\n * @example\n * ```ts\n * import { csrf } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * cookieParser(),\n * csrf(),\n * // ... other middleware\n * ],\n * })\n * ```\n *\n * Client usage:\n * 1. Read the `_csrf` cookie value\n * 2. Send it in the `x-csrf-token` header on every mutating request\n */\nexport function csrf(options: CsrfOptions = {}) {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const cookieOpts = {\n httpOnly: true,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Generate or reuse CSRF token\n const cookies = (req as any).cookies || {}\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomBytes(tokenLength).toString('hex')\n res.cookie(cookieName, token, cookieOpts)\n }\n\n // Skip validation for safe methods and ignored paths\n if (!protectedMethods.has(req.method.toUpperCase())) {\n return next()\n }\n\n if (ignorePaths.has(req.path)) {\n return next()\n }\n\n // Validate: header token must match cookie token\n const headerToken = req.headers[headerName] as string | undefined\n\n if (!headerToken || headerToken !== token) {\n return res.status(403).json({\n message: 'CSRF token mismatch',\n })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;qCAgDA,SAAgB,KAAK,QAAuB,EAAE,CAAE,CAC9C,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,SAAS,EAAE,IAAK,GAAM,EAAE,aAAa,CAAC,CACpF,CACK,YAAc,IAAI,IAAI,QAAQ,aAAe,EAAE,CAAC,CAChD,YAAc,QAAQ,aAAe,GACrC,WAAa,CACjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,cACZ,CAED,OAAQ,IAAc,IAAe,OAAuB,CAG1D,IAAI,OADa,IAAY,SAAW,EAAE,EACtB,YAYpB,GAVK,QACH,MAAQ,YAAY,YAAY,CAAC,SAAS,MAAM,CAChD,IAAI,OAAO,WAAY,MAAO,WAAW,EAIvC,CAAC,iBAAiB,IAAI,IAAI,OAAO,aAAa,CAAC,EAI/C,YAAY,IAAI,IAAI,KAAK,CAC3B,OAAO,MAAM,CAIf,IAAM,YAAc,IAAI,QAAQ,YAEhC,GAAI,CAAC,aAAe,cAAgB,MAClC,OAAO,IAAI,OAAO,IAAI,CAAC,KAAK,CAC1B,QAAS,sBACV,CAAC,CAGJ,MAAM"}
1
+ {"version":3,"file":"csrf.mjs","names":[],"sources":["../../../src/http/middleware/csrf.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport interface CsrfOptions {\n /** Cookie name for the CSRF token (default: '_csrf') */\n cookie?: string\n /** Header name to check for the token (default: 'x-csrf-token') */\n header?: string\n /** HTTP methods that require CSRF validation (default: POST, PUT, PATCH, DELETE) */\n methods?: string[]\n /** Paths to exclude from CSRF checks (e.g. webhooks) */\n ignorePaths?: string[]\n /** Token byte length before hex encoding (default: 32 = 64 hex chars) */\n tokenLength?: number\n /** Cookie options */\n cookieOptions?: {\n httpOnly?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n secure?: boolean\n path?: string\n }\n}\n\n/**\n * Double-submit cookie CSRF protection middleware.\n *\n * On every request, sets a CSRF token cookie. For state-changing methods\n * (POST, PUT, PATCH, DELETE), validates that the request header matches\n * the cookie value.\n *\n * @example\n * ```ts\n * import { csrf } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * cookieParser(),\n * csrf(),\n * // ... other middleware\n * ],\n * })\n * ```\n *\n * Client usage:\n * 1. Read the `_csrf` cookie value\n * 2. Send it in the `x-csrf-token` header on every mutating request\n */\nexport function csrf(options: CsrfOptions = {}) {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const cookieOpts = {\n httpOnly: true,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Generate or reuse CSRF token\n const cookies = (req as any).cookies || {}\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomBytes(tokenLength).toString('hex')\n res.cookie(cookieName, token, cookieOpts)\n }\n\n // Skip validation for safe methods and ignored paths\n if (!protectedMethods.has(req.method.toUpperCase())) {\n return next()\n }\n\n if (ignorePaths.has(req.path)) {\n return next()\n }\n\n // Validate: header token must match cookie token\n const headerToken = req.headers[headerName] as string | undefined\n\n if (!headerToken || headerToken !== token) {\n return res.status(403).json({\n message: 'CSRF token mismatch',\n })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;qCAgDA,SAAgB,KAAK,QAAuB,CAAC,EAAG,CAC9C,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,QAAQ,GAAG,IAAK,GAAM,EAAE,YAAY,CAAC,CACpF,EACM,YAAc,IAAI,IAAI,QAAQ,aAAe,CAAC,CAAC,EAC/C,YAAc,QAAQ,aAAe,GACrC,WAAa,CACjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,aACb,EAEA,OAAQ,IAAc,IAAe,OAAuB,CAG1D,IAAI,OADa,IAAY,SAAW,CAAC,GACrB,YAYpB,GAVK,QACH,MAAQ,YAAY,WAAW,EAAE,SAAS,KAAK,EAC/C,IAAI,OAAO,WAAY,MAAO,UAAU,GAItC,CAAC,iBAAiB,IAAI,IAAI,OAAO,YAAY,CAAC,GAI9C,YAAY,IAAI,IAAI,IAAI,EAC1B,OAAO,KAAK,EAId,IAAM,YAAc,IAAI,QAAQ,YAEhC,GAAI,CAAC,aAAe,cAAgB,MAClC,OAAO,IAAI,OAAO,GAAG,EAAE,KAAK,CAC1B,QAAS,qBACX,CAAC,EAGH,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.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-DR36jGxB.mjs";import{HttpException}from"../../core/errors.mjs";const log=createLogger(`ErrorHandler`);function notFoundHandler(){return(_req,res,_next)=>{res.status(404).json({message:`Not Found`})}}function errorHandler(){let isProduction=process.env.NODE_ENV===`production`;return(err,req,res,_next)=>{if(res.headersSent){log.warn(`Error after headers sent: ${err?.message||`Unknown`}`);return}if(err?.name===`ZodError`){let firstIssue=err.issues?.[0];return res.status(422).json({message:firstIssue?.message||`Validation failed`,...isProduction?{}:{errors:err.issues}})}if(err instanceof HttpException){if(err.status>=500&&log.error(err,err.message),err.headers)for(let[k,v]of Object.entries(err.headers))res.setHeader(k,v);let exposeDetails=!isProduction&&err.details!==void 0;return res.status(err.status).json({message:err.message,...exposeDetails?{errors:err.details}:{}})}let status=err.status||err.statusCode||500;log.error(err,`${req.method} ${req.originalUrl} — ${err.message||`Unhandled error`}`);let message=status===500?`Internal Server Error`:err.message||`Error`;res.status(status).json({message})}}export{errorHandler,notFoundHandler};
11
+ import{createLogger}from"../../core/logger.mjs";import"../../core-Cd5c8LhS.mjs";import{HttpException}from"../../core/errors.mjs";const log=createLogger(`ErrorHandler`);function notFoundHandler(){return(_req,res,_next)=>{res.status(404).json({message:`Not Found`})}}function errorHandler(){let isProduction=process.env.NODE_ENV===`production`;return(err,req,res,_next)=>{if(res.headersSent){log.warn(`Error after headers sent: ${err?.message||`Unknown`}`);return}if(err?.name===`ZodError`){let firstIssue=err.issues?.[0];return res.status(422).json({message:firstIssue?.message||`Validation failed`,...isProduction?{}:{errors:err.issues}})}if(err instanceof HttpException){if(err.status>=500&&log.error(err,err.message),err.headers)for(let[k,v]of Object.entries(err.headers))res.setHeader(k,v);let exposeDetails=!isProduction&&err.details!==void 0;return res.status(err.status).json({message:err.message,...exposeDetails?{errors:err.details}:{}})}let status=err.status||err.statusCode||500;log.error(err,`${req.method} ${req.originalUrl} — ${err.message||`Unhandled error`}`);let message=status===500?`Internal Server Error`:err.message||`Error`;res.status(status).json({message})}}export{errorHandler,notFoundHandler};
12
12
  //# sourceMappingURL=error-handler.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-handler.mjs","names":[],"sources":["../../../src/http/middleware/error-handler.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { HttpException, createLogger } from '../../core'\n\nconst log = createLogger('ErrorHandler')\n\n/** Catch-all for unmatched routes */\nexport function notFoundHandler() {\n return (_req: Request, res: Response, _next: NextFunction) => {\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 */\nexport function errorHandler() {\n const isProduction = process.env.NODE_ENV === 'production'\n return (err: any, req: Request, res: Response, _next: NextFunction) => {\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 // 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 log.error(err, `${req.method} ${req.originalUrl} — ${err.message || 'Unhandled error'}`)\n const message = status === 500 ? 'Internal Server Error' : err.message || 'Error'\n res.status(status).json({ message })\n }\n}\n"],"mappings":";;;;;;;;;;iIAGA,MAAM,IAAM,aAAa,eAAe,CAGxC,SAAgB,iBAAkB,CAChC,OAAQ,KAAe,IAAe,QAAwB,CAC5D,IAAI,OAAO,IAAI,CAAC,KAAK,CAAE,QAAS,YAAa,CAAC,EAclD,SAAgB,cAAe,CAC7B,IAAM,aAAe,QAAQ,IAAI,WAAa,aAC9C,OAAQ,IAAU,IAAc,IAAe,QAAwB,CAErE,GAAI,IAAI,YAAa,CACnB,IAAI,KAAK,6BAA6B,KAAK,SAAW,YAAY,CAClE,OAIF,GAAI,KAAK,OAAS,WAAY,CAC5B,IAAM,WAAa,IAAI,SAAS,GAChC,OAAO,IAAI,OAAO,IAAI,CAAC,KAAK,CAC1B,QAAS,YAAY,SAAW,oBAChC,GAAI,aAAe,EAAE,CAAG,CAAE,OAAQ,IAAI,OAAQ,CAC/C,CAAC,CAIJ,GAAI,eAAe,cAAe,CAIhC,GAHI,IAAI,QAAU,KAChB,IAAI,MAAM,IAAK,IAAI,QAAQ,CAEzB,IAAI,QACN,IAAK,GAAM,CAAC,EAAG,KAAM,OAAO,QAAQ,IAAI,QAAQ,CAC9C,IAAI,UAAU,EAAG,EAAE,CAGvB,IAAM,cAAgB,CAAC,cAAgB,IAAI,UAAY,IAAA,GACvD,OAAO,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK,CACjC,QAAS,IAAI,QACb,GAAI,cAAgB,CAAE,OAAQ,IAAI,QAAS,CAAG,EAAE,CACjD,CAAC,CAIJ,IAAM,OAAS,IAAI,QAAU,IAAI,YAAc,IAC/C,IAAI,MAAM,IAAK,GAAG,IAAI,OAAO,GAAG,IAAI,YAAY,KAAK,IAAI,SAAW,oBAAoB,CACxF,IAAM,QAAU,SAAW,IAAM,wBAA0B,IAAI,SAAW,QAC1E,IAAI,OAAO,OAAO,CAAC,KAAK,CAAE,QAAS,CAAC"}
1
+ {"version":3,"file":"error-handler.mjs","names":[],"sources":["../../../src/http/middleware/error-handler.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { HttpException, createLogger } from '../../core'\n\nconst log = createLogger('ErrorHandler')\n\n/** Catch-all for unmatched routes */\nexport function notFoundHandler() {\n return (_req: Request, res: Response, _next: NextFunction) => {\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 */\nexport function errorHandler() {\n const isProduction = process.env.NODE_ENV === 'production'\n return (err: any, req: Request, res: Response, _next: NextFunction) => {\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 // 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 log.error(err, `${req.method} ${req.originalUrl} — ${err.message || 'Unhandled error'}`)\n const message = status === 500 ? 'Internal Server Error' : err.message || 'Error'\n res.status(status).json({ message })\n }\n}\n"],"mappings":";;;;;;;;;;iIAGA,MAAM,IAAM,aAAa,cAAc,EAGvC,SAAgB,iBAAkB,CAChC,OAAQ,KAAe,IAAe,QAAwB,CAC5D,IAAI,OAAO,GAAG,EAAE,KAAK,CAAE,QAAS,WAAY,CAAC,CAC/C,CACF,CAYA,SAAgB,cAAe,CAC7B,IAAM,aAAe,QAAQ,IAAI,WAAa,aAC9C,OAAQ,IAAU,IAAc,IAAe,QAAwB,CAErE,GAAI,IAAI,YAAa,CACnB,IAAI,KAAK,6BAA6B,KAAK,SAAW,WAAW,EACjE,MACF,CAGA,GAAI,KAAK,OAAS,WAAY,CAC5B,IAAM,WAAa,IAAI,SAAS,GAChC,OAAO,IAAI,OAAO,GAAG,EAAE,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,EAAE,KAAK,CACjC,QAAS,IAAI,QACb,GAAI,cAAgB,CAAE,OAAQ,IAAI,OAAQ,EAAI,CAAC,CACjD,CAAC,CACH,CAGA,IAAM,OAAS,IAAI,QAAU,IAAI,YAAc,IAC/C,IAAI,MAAM,IAAK,GAAG,IAAI,OAAO,GAAG,IAAI,YAAY,KAAK,IAAI,SAAW,mBAAmB,EACvF,IAAM,QAAU,SAAW,IAAM,wBAA0B,IAAI,SAAW,QAC1E,IAAI,OAAO,MAAM,EAAE,KAAK,CAAE,OAAQ,CAAC,CACrC,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -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,EAAE,CAAE,CAClD,GAAM,CACJ,QAAU,GACV,WAAa,OACb,KAAO,CAAE,OAAQ,QAAU,kBAAmB,GAAM,CACpD,YAAc,GACd,cAAgB,GAChB,eAAiB,cACjB,UAAY,GACZ,sBAAwB,IACtB,QAEJ,OAAQ,IAAc,IAAe,OAAuB,CAQ1D,GAPI,eAAe,IAAI,aAAa,eAAe,CAC/C,SAAS,IAAI,UAAU,yBAA0B,UAAU,CAC3D,YAAY,IAAI,UAAU,kBAAmB,WAAW,CACxD,WAAW,IAAI,UAAU,mBAAoB,IAAI,CACjD,gBAAgB,IAAI,UAAU,kBAAmB,eAAe,CAC/D,aAAa,IAAI,UAAU,yBAA0B,MAAM,CAE5D,KAAM,CAER,IAAI,MAAQ,WADG,KAAK,QAAU,UAE1B,KAAK,oBAAmB,OAAS,uBACjC,KAAK,UAAS,OAAS,aAC3B,IAAI,UAAU,4BAA6B,MAAM,CAGnD,GAAI,sBAAuB,CACzB,IAAM,OACJ,OAAO,uBAA0B,SAC7B,OAAO,QAAQ,sBAAsB,CAClC,KAAK,CAAC,IAAK,UAAY,GAAG,IAAI,GAAG,OAAO,KAAK,IAAI,GAAG,CACpD,KAAK,KAAK,CACb,qBACN,IAAI,UAAU,0BAA2B,OAAO,CAGlD,MAAM"}
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,EACjC,KAAK,CAAC,IAAK,UAAY,GAAG,IAAI,GAAG,OAAO,KAAK,GAAG,GAAG,EACnD,KAAK,IAAI,EACZ,qBACN,IAAI,UAAU,0BAA2B,MAAM,CACjD,CAEA,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- var MemoryStore=class{hits=new Map;cleanupTimer;constructor(windowMs){this.windowMs=windowMs,this.cleanupTimer=setInterval(()=>this.cleanup(),windowMs),this.cleanupTimer.unref&&this.cleanupTimer.unref()}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=>req.ip??`127.0.0.1`),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(req.path)||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
+ 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()}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=>req.ip??`127.0.0.1`),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(req.path)||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};
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'\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 }\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 const keyGenerator = options.keyGenerator ?? ((req: Request) => req.ip ?? '127.0.0.1')\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 if (skipPaths.has(req.path)) {\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":";;;;;;;;;;AAkCA,IAAM,YAAN,KAA4C,CAC1C,KAAe,IAAI,IACnB,aAEA,YAAY,SAA0B,CAAlB,KAAA,SAAA,SAClB,KAAK,aAAe,gBAAkB,KAAK,SAAS,CAAE,SAAS,CAE3D,KAAK,aAAa,OACpB,KAAK,aAAa,OAAO,CAI7B,MAAM,UAAU,IAA8D,CAC5E,IAAM,IAAM,KAAK,KAAK,CAChB,MAAQ,KAAK,KAAK,IAAI,IAAI,CAEhC,GAAI,OAAS,MAAM,UAAU,SAAS,CAAG,IAEvC,MADA,OAAM,YACC,CAAE,UAAW,MAAM,UAAW,UAAW,MAAM,UAAW,CAGnE,IAAM,UAAY,IAAI,KAAK,IAAM,KAAK,SAAS,CACzC,SAA6B,CAAE,UAAW,EAAG,UAAW,CAE9D,OADA,KAAK,KAAK,IAAI,IAAK,SAAS,CACrB,CAAE,UAAW,EAAG,UAAW,CAGpC,MAAM,UAAU,IAA4B,CAC1C,IAAM,MAAQ,KAAK,KAAK,IAAI,IAAI,CAC5B,OAAS,MAAM,UAAY,GAC7B,MAAM,YAIV,MAAM,MAAM,IAA4B,CACtC,KAAK,KAAK,OAAO,IAAI,CAGvB,SAAwB,CACtB,IAAM,IAAM,KAAK,KAAK,CACtB,IAAK,GAAM,CAAC,IAAK,SAAU,KAAK,KAC1B,MAAM,UAAU,SAAS,EAAI,KAC/B,KAAK,KAAK,OAAO,IAAI,GA0B7B,SAAgB,UAAU,QAA4B,EAAE,CAAE,CACxD,IAAM,IAAM,QAAQ,KAAO,IACrB,SAAW,QAAQ,UAAY,IAC/B,QAAU,QAAQ,SAAW,oBAC7B,WAAa,QAAQ,YAAc,IACnC,aAAe,QAAQ,eAAkB,KAAiB,IAAI,IAAM,aACpE,YAAc,QAAQ,SAAW,GACjC,MAAQ,QAAQ,OAAS,IAAI,YAAY,SAAS,CAClD,KAAO,QAAQ,KACf,UAAY,IAAI,IAAI,QAAQ,WAAa,EAAE,CAAC,CAElD,OAAO,MAAO,IAAc,IAAe,OAAuB,CAOhE,GALI,UAAU,IAAI,IAAI,KAAK,EAKvB,MAAQ,KAAK,IAAI,CACnB,OAAO,MAAM,CAGf,IAAM,IAAM,aAAa,IAAI,CACvB,CAAE,UAAW,WAAc,MAAM,MAAM,UAAU,IAAI,CACrD,UAAY,KAAK,IAAI,EAAG,IAAM,UAAU,CAQ9C,GANI,cACF,IAAI,UAAU,kBAAmB,IAAI,CACrC,IAAI,UAAU,sBAAuB,UAAU,CAC/C,IAAI,UAAU,kBAAmB,KAAK,KAAK,UAAU,SAAS,CAAG,IAAK,CAAC,EAGrE,UAAY,IACd,OAAO,IAAI,OAAO,WAAW,CAAC,KAAK,CAAE,QAAS,CAAC,CAGjD,MAAM"}
1
+ {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../../src/http/middleware/rate-limit.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\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 }\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 const keyGenerator = options.keyGenerator ?? ((req: Request) => req.ip ?? '127.0.0.1')\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 if (skipPaths.has(req.path)) {\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":";;;;;;;;;;AAkCA,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,CAE5B,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,IACnC,aAAe,QAAQ,eAAkB,KAAiB,IAAI,IAAM,aACpE,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,CAOhE,GALI,UAAU,IAAI,IAAI,IAAI,GAKtB,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,EAAE,KAAK,CAAE,OAAQ,CAAC,EAGhD,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"request-id.mjs","names":[],"sources":["../../../src/http/middleware/request-id.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport const REQUEST_ID_HEADER = 'x-request-id'\n\n/**\n * Middleware that generates or propagates a unique request ID.\n *\n * Idempotent: if `req.requestId` is already set (e.g. by\n * `requestScopeMiddleware` earlier in the pipeline), this middleware reuses\n * it instead of generating a divergent second value. Keeps the\n * X-Request-Id response header, the ALS store's `requestId`, and\n * `RequestContext.requestId` in agreement for every request, regardless of\n * mounting order.\n */\nexport function requestId() {\n return (req: Request, res: Response, next: NextFunction) => {\n const existing = (req as Request & { requestId?: string }).requestId\n const id = existing || (req.headers[REQUEST_ID_HEADER] as string) || randomUUID()\n ;(req as any).requestId = id\n res.setHeader(REQUEST_ID_HEADER, id)\n next()\n }\n}\n"],"mappings":";;;;;;;;;;oCAGA,MAAa,kBAAoB,eAYjC,SAAgB,WAAY,CAC1B,OAAQ,IAAc,IAAe,OAAuB,CAE1D,IAAM,GADY,IAAyC,WACnC,IAAI,QAAA,iBAAyC,YAAY,CAC/E,IAAY,UAAY,GAC1B,IAAI,UAAU,kBAAmB,GAAG,CACpC,MAAM"}
1
+ {"version":3,"file":"request-id.mjs","names":[],"sources":["../../../src/http/middleware/request-id.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport const REQUEST_ID_HEADER = 'x-request-id'\n\n/**\n * Middleware that generates or propagates a unique request ID.\n *\n * Idempotent: if `req.requestId` is already set (e.g. by\n * `requestScopeMiddleware` earlier in the pipeline), this middleware reuses\n * it instead of generating a divergent second value. Keeps the\n * X-Request-Id response header, the ALS store's `requestId`, and\n * `RequestContext.requestId` in agreement for every request, regardless of\n * mounting order.\n */\nexport function requestId() {\n return (req: Request, res: Response, next: NextFunction) => {\n const existing = (req as Request & { requestId?: string }).requestId\n const id = existing || (req.headers[REQUEST_ID_HEADER] as string) || randomUUID()\n ;(req as any).requestId = id\n res.setHeader(REQUEST_ID_HEADER, id)\n next()\n }\n}\n"],"mappings":";;;;;;;;;;oCAGA,MAAa,kBAAoB,eAYjC,SAAgB,WAAY,CAC1B,OAAQ,IAAc,IAAe,OAAuB,CAE1D,IAAM,GADY,IAAyC,WACnC,IAAI,QAAA,iBAAyC,WAAW,EAC/E,IAAa,UAAY,GAC1B,IAAI,UAAU,kBAAmB,EAAE,EACnC,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.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-DR36jGxB.mjs";function requestLogger(options={}){let log=createLogger(options.name??`HTTP`),level=options.level??`info`,skip=options.skip??[];return(req,res,next)=>{if(skip.some(prefix=>req.path.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;log[level](`${req.method} ${req.originalUrl} ${status} ${duration}ms ${requestId}`)}),next()}}export{requestLogger};
11
+ import{createLogger}from"../../core/logger.mjs";import"../../core-Cd5c8LhS.mjs";function requestLogger(options={}){let log=createLogger(options.name??`HTTP`),level=options.level??`info`,skip=options.skip??[];return(req,res,next)=>{if(skip.some(prefix=>req.path.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;log[level](`${req.method} ${req.originalUrl} ${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":["import type { Request, Response, NextFunction } from 'express'\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: Request, res: Response, next: NextFunction) => {\n // Skip logging for excluded paths\n if (skip.some((prefix) => req.path.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 log[level](`${req.method} ${req.originalUrl} ${status} ${duration}ms ${requestId}`)\n })\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;gFA4BA,SAAgB,cAAc,QAAgC,EAAE,CAAE,CAChE,IAAM,IAAM,aAAa,QAAQ,MAAQ,OAAO,CAC1C,MAAQ,QAAQ,OAAS,OACzB,KAAO,QAAQ,MAAQ,EAAE,CAE/B,OAAQ,IAAc,IAAe,OAAuB,CAE1D,GAAI,KAAK,KAAM,QAAW,IAAI,KAAK,WAAW,OAAO,CAAC,CACpD,OAAO,MAAM,CAGf,IAAM,MAAQ,KAAK,KAAK,CAExB,IAAI,GAAG,aAAgB,CACrB,IAAM,SAAW,KAAK,KAAK,CAAG,MACxB,UAAa,IAAY,WAAa,IAAI,QAAQ,iBAAmB,IACrE,OAAS,IAAI,WAEnB,IAAI,OAAO,GAAG,IAAI,OAAO,GAAG,IAAI,YAAY,GAAG,OAAO,GAAG,SAAS,KAAK,YAAY,EACnF,CAEF,MAAM"}
1
+ {"version":3,"file":"request-logger.mjs","names":[],"sources":["../../../src/http/middleware/request-logger.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\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: Request, res: Response, next: NextFunction) => {\n // Skip logging for excluded paths\n if (skip.some((prefix) => req.path.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 log[level](`${req.method} ${req.originalUrl} ${status} ${duration}ms ${requestId}`)\n })\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;gFA4BA,SAAgB,cAAc,QAAgC,CAAC,EAAG,CAChE,IAAM,IAAM,aAAa,QAAQ,MAAQ,MAAM,EACzC,MAAQ,QAAQ,OAAS,OACzB,KAAO,QAAQ,MAAQ,CAAC,EAE9B,OAAQ,IAAc,IAAe,OAAuB,CAE1D,GAAI,KAAK,KAAM,QAAW,IAAI,KAAK,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,WAEnB,IAAI,OAAO,GAAG,IAAI,OAAO,GAAG,IAAI,YAAY,GAAG,OAAO,GAAG,SAAS,KAAK,WAAW,CACpF,CAAC,EAED,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -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\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 }\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":"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"}
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\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 }\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":"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"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as SpaAdapterOptions, t as SpaAdapter } from "../../spa-DIXtRBj-.mjs";
2
+ import { n as SpaAdapterOptions, t as SpaAdapter } from "../../spa-DFxiK7jQ.mjs";
3
3
  export { SpaAdapter, SpaAdapterOptions };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.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{h as __require}from"../../container-jPCvIZH2.mjs";import{Logger}from"../../core/logger.mjs";import"../../core-DR36jGxB.mjs";import{existsSync,readFileSync}from"node:fs";import{join,resolve}from"node:path";const log=Logger.for(`SpaAdapter`);var SpaAdapter=class{name=`SpaAdapter`;clientDir;apiPrefixes;excludePaths;cacheControl;indexCacheControl;indexHtml=null;constructor(options={}){this.options=options,this.clientDir=resolve(options.clientDir??`dist/client`),this.excludePaths=options.exclude??[],this.cacheControl=options.cacheControl??`public, max-age=31536000, immutable`,this.indexCacheControl=options.indexCacheControl??`no-cache`;let prefix=options.apiPrefix??`/api`;this.apiPrefixes=Array.isArray(prefix)?prefix:[prefix]}beforeMount({app}){if(!existsSync(this.clientDir)){log.warn(`SPA client directory not found: ${this.clientDir}`),log.warn(`Build your frontend first, or set clientDir to the correct path.`);return}let indexPath=join(this.clientDir,`index.html`);if(existsSync(indexPath))this.indexHtml=readFileSync(indexPath,`utf-8`);else{log.warn(`index.html not found in ${this.clientDir}`);return}try{let express=__require(`express`),staticOpts={};this.cacheControl&&(staticOpts.setHeaders=(res,filePath)=>{filePath.endsWith(`.html`)?res.setHeader(`Cache-Control`,this.indexCacheControl):res.setHeader(`Cache-Control`,this.cacheControl)}),app.use(express.static(this.clientDir,staticOpts))}catch{log.error(`express.static not available — SPA static file serving disabled`);return}log.debug(`Serving SPA from ${this.clientDir}`)}beforeStart({app}){this.indexHtml&&app.use((req,res,next)=>{for(let prefix of this.apiPrefixes)if(req.path.startsWith(prefix))return next();for(let path of this.excludePaths)if(req.path.startsWith(path))return next();if(req.path.includes(`.`)||req.method!==`GET`)return next();res.setHeader(`Content-Type`,`text/html`),res.setHeader(`Cache-Control`,this.indexCacheControl),res.send(this.indexHtml)})}};export{SpaAdapter};
11
+ import{h as __require}from"../../container-B1X8WRwl.mjs";import{Logger}from"../../core/logger.mjs";import"../../core-Cd5c8LhS.mjs";import{existsSync,readFileSync}from"node:fs";import{join,resolve}from"node:path";const log=Logger.for(`SpaAdapter`);var SpaAdapter=class{options;name=`SpaAdapter`;clientDir;apiPrefixes;excludePaths;cacheControl;indexCacheControl;indexHtml=null;constructor(options={}){this.options=options,this.clientDir=resolve(options.clientDir??`dist/client`),this.excludePaths=options.exclude??[],this.cacheControl=options.cacheControl??`public, max-age=31536000, immutable`,this.indexCacheControl=options.indexCacheControl??`no-cache`;let prefix=options.apiPrefix??`/api`;this.apiPrefixes=Array.isArray(prefix)?prefix:[prefix]}beforeMount({app}){if(!existsSync(this.clientDir)){log.warn(`SPA client directory not found: ${this.clientDir}`),log.warn(`Build your frontend first, or set clientDir to the correct path.`);return}let indexPath=join(this.clientDir,`index.html`);if(existsSync(indexPath))this.indexHtml=readFileSync(indexPath,`utf-8`);else{log.warn(`index.html not found in ${this.clientDir}`);return}try{let express=__require(`express`),staticOpts={};this.cacheControl&&(staticOpts.setHeaders=(res,filePath)=>{filePath.endsWith(`.html`)?res.setHeader(`Cache-Control`,this.indexCacheControl):res.setHeader(`Cache-Control`,this.cacheControl)}),app.use(express.static(this.clientDir,staticOpts))}catch{log.error(`express.static not available — SPA static file serving disabled`);return}log.debug(`Serving SPA from ${this.clientDir}`)}beforeStart({app}){this.indexHtml&&app.use((req,res,next)=>{for(let prefix of this.apiPrefixes)if(req.path.startsWith(prefix))return next();for(let path of this.excludePaths)if(req.path.startsWith(path))return next();if(req.path.includes(`.`)||req.method!==`GET`)return next();res.setHeader(`Content-Type`,`text/html`),res.setHeader(`Cache-Control`,this.indexCacheControl),res.send(this.indexHtml)})}};export{SpaAdapter};
12
12
  //# sourceMappingURL=spa.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"spa.mjs","names":[],"sources":["../../../src/http/middleware/spa.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { Logger, type AppAdapter, type AdapterContext } from '../../core'\n\nconst log = Logger.for('SpaAdapter')\n\nexport interface SpaAdapterOptions {\n /**\n * Directory containing the built SPA files (index.html, assets, etc.)\n * Default: 'dist/client'\n *\n * @example\n * ```ts\n * // Vue: 'dist'\n * // React (Vite): 'dist'\n * // React (CRA): 'build'\n * // Svelte: 'build'\n * // Angular: 'dist/my-app/browser'\n * ```\n */\n clientDir?: string\n\n /**\n * URL prefix for API routes. SPA fallback only applies to\n * non-API routes (routes NOT starting with this prefix).\n * Default: '/api'\n *\n * Set to an array for multiple prefixes:\n * ```ts\n * apiPrefix: ['/api', '/graphql', '/_debug']\n * ```\n */\n apiPrefix?: string | string[]\n\n /**\n * Additional paths to exclude from SPA fallback.\n * These paths will NOT serve index.html.\n *\n * @example\n * ```ts\n * exclude: ['/health', '/metrics', '/ws']\n * ```\n */\n exclude?: string[]\n\n /**\n * Cache-Control header for static assets (default: 'public, max-age=31536000, immutable')\n * Set to false to disable caching headers.\n */\n cacheControl?: string | false\n\n /**\n * Cache-Control for index.html (default: 'no-cache')\n * index.html should not be cached to ensure clients get fresh builds.\n */\n indexCacheControl?: string\n}\n\n/**\n * SPA adapter — serve a Vue, React, Svelte, or Angular build alongside\n * your KickJS API. API routes are handled by controllers; everything else\n * falls back to index.html for client-side routing.\n *\n * @example\n * ```ts\n * import { SpaAdapter } from '@forinda/kickjs/spa'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new SpaAdapter({\n * clientDir: 'dist/client', // or 'build', 'dist', etc.\n * apiPrefix: '/api',\n * }),\n * ],\n * })\n * ```\n *\n * File structure:\n * ```\n * dist/\n * client/ ← SPA build output\n * index.html\n * assets/\n * app.js\n * style.css\n * server/ ← KickJS server\n * ```\n */\nexport class SpaAdapter implements AppAdapter {\n name = 'SpaAdapter'\n private clientDir: string\n private apiPrefixes: string[]\n private excludePaths: string[]\n private cacheControl: string | false\n private indexCacheControl: string\n private indexHtml: string | null = null\n\n constructor(private options: SpaAdapterOptions = {}) {\n this.clientDir = resolve(options.clientDir ?? 'dist/client')\n this.excludePaths = options.exclude ?? []\n this.cacheControl = options.cacheControl ?? 'public, max-age=31536000, immutable'\n this.indexCacheControl = options.indexCacheControl ?? 'no-cache'\n\n const prefix = options.apiPrefix ?? '/api'\n this.apiPrefixes = Array.isArray(prefix) ? prefix : [prefix]\n }\n\n beforeMount({ app }: AdapterContext): void {\n if (!existsSync(this.clientDir)) {\n log.warn(`SPA client directory not found: ${this.clientDir}`)\n log.warn('Build your frontend first, or set clientDir to the correct path.')\n return\n }\n\n // Read index.html into memory\n const indexPath = join(this.clientDir, 'index.html')\n if (existsSync(indexPath)) {\n this.indexHtml = readFileSync(indexPath, 'utf-8')\n } else {\n log.warn(`index.html not found in ${this.clientDir}`)\n return\n }\n\n // Serve static files with cache headers\n try {\n // Dynamic import to avoid hard dependency on express.static\n const express = require('express')\n const staticOpts: any = {}\n\n if (this.cacheControl) {\n staticOpts.setHeaders = (res: any, filePath: string) => {\n if (filePath.endsWith('.html')) {\n res.setHeader('Cache-Control', this.indexCacheControl)\n } else {\n res.setHeader('Cache-Control', this.cacheControl as string)\n }\n }\n }\n\n app.use(express.static(this.clientDir, staticOpts))\n } catch {\n log.error('express.static not available — SPA static file serving disabled')\n return\n }\n\n log.debug(`Serving SPA from ${this.clientDir}`)\n }\n\n beforeStart({ app }: AdapterContext): void {\n if (!this.indexHtml) return\n\n // SPA fallback: serve index.html for all non-API, non-file routes\n app.use((req: any, res: any, next: any) => {\n // Skip API routes\n for (const prefix of this.apiPrefixes) {\n if (req.path.startsWith(prefix)) return next()\n }\n\n // Skip excluded paths\n for (const path of this.excludePaths) {\n if (req.path.startsWith(path)) return next()\n }\n\n // Skip requests for files (have an extension)\n if (req.path.includes('.')) return next()\n\n // Skip non-GET requests\n if (req.method !== 'GET') return next()\n\n // Serve index.html\n res.setHeader('Content-Type', 'text/html')\n res.setHeader('Cache-Control', this.indexCacheControl)\n res.send(this.indexHtml)\n })\n }\n}\n"],"mappings":";;;;;;;;;;oNAIA,MAAM,IAAM,OAAO,IAAI,aAAa,CAqFpC,IAAa,WAAb,KAA8C,CAC5C,KAAO,aACP,UACA,YACA,aACA,aACA,kBACA,UAAmC,KAEnC,YAAY,QAAqC,EAAE,CAAE,CAAjC,KAAA,QAAA,QAClB,KAAK,UAAY,QAAQ,QAAQ,WAAa,cAAc,CAC5D,KAAK,aAAe,QAAQ,SAAW,EAAE,CACzC,KAAK,aAAe,QAAQ,cAAgB,sCAC5C,KAAK,kBAAoB,QAAQ,mBAAqB,WAEtD,IAAM,OAAS,QAAQ,WAAa,OACpC,KAAK,YAAc,MAAM,QAAQ,OAAO,CAAG,OAAS,CAAC,OAAO,CAG9D,YAAY,CAAE,KAA6B,CACzC,GAAI,CAAC,WAAW,KAAK,UAAU,CAAE,CAC/B,IAAI,KAAK,mCAAmC,KAAK,YAAY,CAC7D,IAAI,KAAK,mEAAmE,CAC5E,OAIF,IAAM,UAAY,KAAK,KAAK,UAAW,aAAa,CACpD,GAAI,WAAW,UAAU,CACvB,KAAK,UAAY,aAAa,UAAW,QAAQ,KAC5C,CACL,IAAI,KAAK,2BAA2B,KAAK,YAAY,CACrD,OAIF,GAAI,CAEF,IAAM,QAAA,UAAkB,UAAU,CAC5B,WAAkB,EAAE,CAEtB,KAAK,eACP,WAAW,YAAc,IAAU,WAAqB,CAClD,SAAS,SAAS,QAAQ,CAC5B,IAAI,UAAU,gBAAiB,KAAK,kBAAkB,CAEtD,IAAI,UAAU,gBAAiB,KAAK,aAAuB,GAKjE,IAAI,IAAI,QAAQ,OAAO,KAAK,UAAW,WAAW,CAAC,MAC7C,CACN,IAAI,MAAM,kEAAkE,CAC5E,OAGF,IAAI,MAAM,oBAAoB,KAAK,YAAY,CAGjD,YAAY,CAAE,KAA6B,CACpC,KAAK,WAGV,IAAI,KAAK,IAAU,IAAU,OAAc,CAEzC,IAAK,IAAM,UAAU,KAAK,YACxB,GAAI,IAAI,KAAK,WAAW,OAAO,CAAE,OAAO,MAAM,CAIhD,IAAK,IAAM,QAAQ,KAAK,aACtB,GAAI,IAAI,KAAK,WAAW,KAAK,CAAE,OAAO,MAAM,CAO9C,GAHI,IAAI,KAAK,SAAS,IAAI,EAGtB,IAAI,SAAW,MAAO,OAAO,MAAM,CAGvC,IAAI,UAAU,eAAgB,YAAY,CAC1C,IAAI,UAAU,gBAAiB,KAAK,kBAAkB,CACtD,IAAI,KAAK,KAAK,UAAU,EACxB"}
1
+ {"version":3,"file":"spa.mjs","names":[],"sources":["../../../src/http/middleware/spa.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { Logger, type AppAdapter, type AdapterContext } from '../../core'\n\nconst log = Logger.for('SpaAdapter')\n\nexport interface SpaAdapterOptions {\n /**\n * Directory containing the built SPA files (index.html, assets, etc.)\n * Default: 'dist/client'\n *\n * @example\n * ```ts\n * // Vue: 'dist'\n * // React (Vite): 'dist'\n * // React (CRA): 'build'\n * // Svelte: 'build'\n * // Angular: 'dist/my-app/browser'\n * ```\n */\n clientDir?: string\n\n /**\n * URL prefix for API routes. SPA fallback only applies to\n * non-API routes (routes NOT starting with this prefix).\n * Default: '/api'\n *\n * Set to an array for multiple prefixes:\n * ```ts\n * apiPrefix: ['/api', '/graphql', '/_debug']\n * ```\n */\n apiPrefix?: string | string[]\n\n /**\n * Additional paths to exclude from SPA fallback.\n * These paths will NOT serve index.html.\n *\n * @example\n * ```ts\n * exclude: ['/health', '/metrics', '/ws']\n * ```\n */\n exclude?: string[]\n\n /**\n * Cache-Control header for static assets (default: 'public, max-age=31536000, immutable')\n * Set to false to disable caching headers.\n */\n cacheControl?: string | false\n\n /**\n * Cache-Control for index.html (default: 'no-cache')\n * index.html should not be cached to ensure clients get fresh builds.\n */\n indexCacheControl?: string\n}\n\n/**\n * SPA adapter — serve a Vue, React, Svelte, or Angular build alongside\n * your KickJS API. API routes are handled by controllers; everything else\n * falls back to index.html for client-side routing.\n *\n * @example\n * ```ts\n * import { SpaAdapter } from '@forinda/kickjs/spa'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new SpaAdapter({\n * clientDir: 'dist/client', // or 'build', 'dist', etc.\n * apiPrefix: '/api',\n * }),\n * ],\n * })\n * ```\n *\n * File structure:\n * ```\n * dist/\n * client/ ← SPA build output\n * index.html\n * assets/\n * app.js\n * style.css\n * server/ ← KickJS server\n * ```\n */\nexport class SpaAdapter implements AppAdapter {\n name = 'SpaAdapter'\n private clientDir: string\n private apiPrefixes: string[]\n private excludePaths: string[]\n private cacheControl: string | false\n private indexCacheControl: string\n private indexHtml: string | null = null\n\n constructor(private options: SpaAdapterOptions = {}) {\n this.clientDir = resolve(options.clientDir ?? 'dist/client')\n this.excludePaths = options.exclude ?? []\n this.cacheControl = options.cacheControl ?? 'public, max-age=31536000, immutable'\n this.indexCacheControl = options.indexCacheControl ?? 'no-cache'\n\n const prefix = options.apiPrefix ?? '/api'\n this.apiPrefixes = Array.isArray(prefix) ? prefix : [prefix]\n }\n\n beforeMount({ app }: AdapterContext): void {\n if (!existsSync(this.clientDir)) {\n log.warn(`SPA client directory not found: ${this.clientDir}`)\n log.warn('Build your frontend first, or set clientDir to the correct path.')\n return\n }\n\n // Read index.html into memory\n const indexPath = join(this.clientDir, 'index.html')\n if (existsSync(indexPath)) {\n this.indexHtml = readFileSync(indexPath, 'utf-8')\n } else {\n log.warn(`index.html not found in ${this.clientDir}`)\n return\n }\n\n // Serve static files with cache headers\n try {\n // Dynamic import to avoid hard dependency on express.static\n const express = require('express')\n const staticOpts: any = {}\n\n if (this.cacheControl) {\n staticOpts.setHeaders = (res: any, filePath: string) => {\n if (filePath.endsWith('.html')) {\n res.setHeader('Cache-Control', this.indexCacheControl)\n } else {\n res.setHeader('Cache-Control', this.cacheControl as string)\n }\n }\n }\n\n app.use(express.static(this.clientDir, staticOpts))\n } catch {\n log.error('express.static not available — SPA static file serving disabled')\n return\n }\n\n log.debug(`Serving SPA from ${this.clientDir}`)\n }\n\n beforeStart({ app }: AdapterContext): void {\n if (!this.indexHtml) return\n\n // SPA fallback: serve index.html for all non-API, non-file routes\n app.use((req: any, res: any, next: any) => {\n // Skip API routes\n for (const prefix of this.apiPrefixes) {\n if (req.path.startsWith(prefix)) return next()\n }\n\n // Skip excluded paths\n for (const path of this.excludePaths) {\n if (req.path.startsWith(path)) return next()\n }\n\n // Skip requests for files (have an extension)\n if (req.path.includes('.')) return next()\n\n // Skip non-GET requests\n if (req.method !== 'GET') return next()\n\n // Serve index.html\n res.setHeader('Content-Type', 'text/html')\n res.setHeader('Cache-Control', this.indexCacheControl)\n res.send(this.indexHtml)\n })\n }\n}\n"],"mappings":";;;;;;;;;;oNAIA,MAAM,IAAM,OAAO,IAAI,YAAY,EAqFnC,IAAa,WAAb,KAA8C,CASxB,QARpB,KAAO,aACP,UACA,YACA,aACA,aACA,kBACA,UAAmC,KAEnC,YAAY,QAAqC,CAAC,EAAG,CAAjC,KAAA,QAAA,QAClB,KAAK,UAAY,QAAQ,QAAQ,WAAa,aAAa,EAC3D,KAAK,aAAe,QAAQ,SAAW,CAAC,EACxC,KAAK,aAAe,QAAQ,cAAgB,sCAC5C,KAAK,kBAAoB,QAAQ,mBAAqB,WAEtD,IAAM,OAAS,QAAQ,WAAa,OACpC,KAAK,YAAc,MAAM,QAAQ,MAAM,EAAI,OAAS,CAAC,MAAM,CAC7D,CAEA,YAAY,CAAE,KAA6B,CACzC,GAAI,CAAC,WAAW,KAAK,SAAS,EAAG,CAC/B,IAAI,KAAK,mCAAmC,KAAK,WAAW,EAC5D,IAAI,KAAK,kEAAkE,EAC3E,MACF,CAGA,IAAM,UAAY,KAAK,KAAK,UAAW,YAAY,EACnD,GAAI,WAAW,SAAS,EACtB,KAAK,UAAY,aAAa,UAAW,OAAO,MAC3C,CACL,IAAI,KAAK,2BAA2B,KAAK,WAAW,EACpD,MACF,CAGA,GAAI,CAEF,IAAM,QAAA,UAAkB,SAAS,EAC3B,WAAkB,CAAC,EAErB,KAAK,eACP,WAAW,YAAc,IAAU,WAAqB,CAClD,SAAS,SAAS,OAAO,EAC3B,IAAI,UAAU,gBAAiB,KAAK,iBAAiB,EAErD,IAAI,UAAU,gBAAiB,KAAK,YAAsB,CAE9D,GAGF,IAAI,IAAI,QAAQ,OAAO,KAAK,UAAW,UAAU,CAAC,CACpD,MAAQ,CACN,IAAI,MAAM,iEAAiE,EAC3E,MACF,CAEA,IAAI,MAAM,oBAAoB,KAAK,WAAW,CAChD,CAEA,YAAY,CAAE,KAA6B,CACpC,KAAK,WAGV,IAAI,KAAK,IAAU,IAAU,OAAc,CAEzC,IAAK,IAAM,UAAU,KAAK,YACxB,GAAI,IAAI,KAAK,WAAW,MAAM,EAAG,OAAO,KAAK,EAI/C,IAAK,IAAM,QAAQ,KAAK,aACtB,GAAI,IAAI,KAAK,WAAW,IAAI,EAAG,OAAO,KAAK,EAO7C,GAHI,IAAI,KAAK,SAAS,GAAG,GAGrB,IAAI,SAAW,MAAO,OAAO,KAAK,EAGtC,IAAI,UAAU,eAAgB,WAAW,EACzC,IAAI,UAAU,gBAAiB,KAAK,iBAAiB,EACrD,IAAI,KAAK,KAAK,SAAS,CACzB,CAAC,CACH,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"upload.mjs","names":[],"sources":["../../../src/http/middleware/upload.ts"],"sourcesContent":["import { unlink } from 'node:fs/promises'\nimport type { Request, Response, NextFunction, RequestHandler } from 'express'\nimport multer, { type Options as MulterOptions } from 'multer'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core'\n\n/**\n * Maps short file extensions to their MIME types.\n * Users can pass `['jpg', 'png', 'pdf']` instead of full MIME strings.\n */\nconst MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * 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 mimeMap = options.customMimeMap ? { ...MIME_MAP, ...options.customMimeMap } : MIME_MAP\n\n const limits: MulterOptions['limits'] = {\n fileSize: options.maxSize ?? 5 * 1024 * 1024,\n }\n\n let fileFilter: MulterOptions['fileFilter'] | undefined\n\n if (typeof options.allowedTypes === 'function') {\n // Custom filter function\n const filterFn = options.allowedTypes\n fileFilter = (_req, file, cb) => {\n if (filterFn(file.mimetype, file.originalname)) {\n cb(null, true)\n } else {\n cb(new Error(`File type ${file.mimetype} is not allowed`))\n }\n }\n } else if (Array.isArray(options.allowedTypes)) {\n // String array — resolve short extensions using the (possibly extended) MIME map\n const resolvedTypes = options.allowedTypes.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return mimeMap[lower] ?? t\n })\n fileFilter = (_req, file, cb) => {\n const allowed = resolvedTypes.some((type) => {\n if (type.endsWith('/*')) {\n return file.mimetype.startsWith(type.replace('/*', '/'))\n }\n return file.mimetype === type\n })\n if (allowed) {\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 multer(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":";;;;;;;;;;gEASA,MAAM,SAAmC,CAEvC,IAAK,aACL,KAAM,aACN,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,gBACL,IAAK,YACL,IAAK,eACL,KAAM,aACN,IAAK,aACL,KAAM,aAEN,IAAK,kBACL,IAAK,qBACL,KAAM,0EACN,IAAK,2BACL,KAAM,oEACN,IAAK,gCACL,KAAM,4EACN,IAAK,0CACL,IAAK,iDACL,IAAK,kDACL,IAAK,kBACL,IAAK,aACL,IAAK,WAEL,IAAK,kBACL,GAAI,mBACJ,IAAK,oBACL,IAAK,sBACL,KAAM,8BAEN,IAAK,aACL,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,YAEL,IAAK,YACL,KAAM,aACN,IAAK,kBACL,IAAK,kBACL,IAAK,mBAEL,KAAM,mBACN,IAAK,kBACL,KAAM,YACP,CAYD,SAAgB,iBAAiB,MAA2B,CAC1D,OAAO,MAAM,IAAK,GAET,SADO,EAAE,aAAa,CAAC,QAAQ,MAAO,GACxB,GAAK,EAC1B,CAeJ,SAAS,aAAa,QAAwB,CAC5C,IAAM,QAAU,QAAQ,cAAgB,CAAE,GAAG,SAAU,GAAG,QAAQ,cAAe,CAAG,SAE9E,OAAkC,CACtC,SAAU,QAAQ,SAAW,EAAI,KAAO,KACzC,CAEG,WAEJ,GAAI,OAAO,QAAQ,cAAiB,WAAY,CAE9C,IAAM,SAAW,QAAQ,aACzB,YAAc,KAAM,KAAM,KAAO,CAC3B,SAAS,KAAK,SAAU,KAAK,aAAa,CAC5C,GAAG,KAAM,GAAK,CAEd,GAAO,MAAM,aAAa,KAAK,SAAS,iBAAiB,CAAC,UAGrD,MAAM,QAAQ,QAAQ,aAAa,CAAE,CAE9C,IAAM,cAAgB,QAAQ,aAAa,IAAK,GAEvC,QADO,EAAE,aAAa,CAAC,QAAQ,MAAO,GACzB,GAAK,EACzB,CACF,YAAc,KAAM,KAAM,KAAO,CACf,cAAc,KAAM,MAC9B,KAAK,SAAS,KAAK,CACd,KAAK,SAAS,WAAW,KAAK,QAAQ,KAAM,IAAI,CAAC,CAEnD,KAAK,WAAa,KAEhB,CACT,GAAG,KAAM,GAAK,CAEd,GAAO,MAAM,aAAa,KAAK,SAAS,iBAAiB,CAAC,EAYhE,OAAO,OAAO,CANZ,OACA,GAAI,WAAa,CAAE,WAAY,CAAG,EAAE,CACpC,GAAI,QAAQ,QAAU,CAAE,QAAS,QAAQ,QAAS,CAAG,EAAE,CACvD,GAAI,QAAQ,KAAO,CAAE,KAAM,QAAQ,KAAM,CAAG,EAAE,CAGrB,CAAC,CAe9B,SAAS,OAAO,UAAmB,QAAyB,EAAE,CAAkB,CAE9E,OADU,aAAa,QACf,CAAC,OAAO,UAAU,CAM5B,SAAS,MAAM,UAAmB,SAAW,GAAI,QAAyB,EAAE,CAAkB,CAE5F,OADU,aAAa,QACf,CAAC,MAAM,UAAW,SAAS,CAMrC,SAAS,KAAK,QAAyB,EAAE,CAAkB,CAEzD,OADU,aAAa,QACf,CAAC,MAAM,CAiBjB,SAAgB,cAAe,CAC7B,OAAQ,IAAc,IAAe,OAAuB,CAC1D,IAAI,GAAG,SAAU,SAAY,CAC3B,IAAM,MAAe,EAAE,CAKvB,GAHK,IAAY,MAAM,MACrB,MAAM,KAAM,IAAY,KAAK,CAE3B,MAAM,QAAS,IAAY,MAAM,KAC9B,IAAM,KAAM,IAAY,MACvB,GAAG,MAAM,MAAM,KAAK,EAAE,CAI9B,IAAK,IAAM,QAAQ,MACjB,GAAI,CACF,MAAM,OAAO,KAAK,KAAK,MACjB,IAIV,CAEF,MAAM,EASV,SAAgB,sBAAsB,OAA0C,CAC9E,IAAM,QAAyB,EAAE,CAC7B,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,QAAQ,CACnC,IAAK,QACH,OAAO,MAAM,UAAW,OAAO,UAAY,GAAI,QAAQ,CACzD,IAAK,OACH,OAAO,KAAK,QAAQ,EAK1B,MAAa,OAAS,CAAE,OAAQ,MAAO,KAAM"}
1
+ {"version":3,"file":"upload.mjs","names":[],"sources":["../../../src/http/middleware/upload.ts"],"sourcesContent":["import { unlink } from 'node:fs/promises'\nimport type { Request, Response, NextFunction, RequestHandler } from 'express'\nimport multer, { type Options as MulterOptions } from 'multer'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core'\n\n/**\n * Maps short file extensions to their MIME types.\n * Users can pass `['jpg', 'png', 'pdf']` instead of full MIME strings.\n */\nconst MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * 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 mimeMap = options.customMimeMap ? { ...MIME_MAP, ...options.customMimeMap } : MIME_MAP\n\n const limits: MulterOptions['limits'] = {\n fileSize: options.maxSize ?? 5 * 1024 * 1024,\n }\n\n let fileFilter: MulterOptions['fileFilter'] | undefined\n\n if (typeof options.allowedTypes === 'function') {\n // Custom filter function\n const filterFn = options.allowedTypes\n fileFilter = (_req, file, cb) => {\n if (filterFn(file.mimetype, file.originalname)) {\n cb(null, true)\n } else {\n cb(new Error(`File type ${file.mimetype} is not allowed`))\n }\n }\n } else if (Array.isArray(options.allowedTypes)) {\n // String array — resolve short extensions using the (possibly extended) MIME map\n const resolvedTypes = options.allowedTypes.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return mimeMap[lower] ?? t\n })\n fileFilter = (_req, file, cb) => {\n const allowed = resolvedTypes.some((type) => {\n if (type.endsWith('/*')) {\n return file.mimetype.startsWith(type.replace('/*', '/'))\n }\n return file.mimetype === type\n })\n if (allowed) {\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 multer(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":"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"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.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"../../core-DR36jGxB.mjs";import{HttpException,HttpStatus}from"../../core/errors.mjs";function toValidationException(error,message,useFirstIssueMessage){let issues=error.issues||[],details=issues.map(i=>({field:i.path?.join(`.`)??``,message:i.message})),finalMessage=useFirstIssueMessage?issues[0]?.message??message:message;return new HttpException(HttpStatus.UNPROCESSABLE_ENTITY,finalMessage,details)}function assignReqProperty(req,key,value){Object.defineProperty(req,key,{value,writable:!0,configurable:!0,enumerable:!0})}function validate(schema){return(req,_res,next)=>{try{if(schema.body){let result=schema.body.safeParse(req.body);if(!result.success)return next(toValidationException(result.error,`Validation failed`,!0));req.body=result.data}if(schema.query){let result=schema.query.safeParse(req.query);if(!result.success)return next(toValidationException(result.error,`Invalid query parameters`,!1));assignReqProperty(req,`query`,result.data)}if(schema.params){let result=schema.params.safeParse(req.params);if(!result.success)return next(toValidationException(result.error,`Invalid path parameters`,!1));assignReqProperty(req,`params`,result.data)}next()}catch(err){next(err)}}}export{validate};
11
+ import"../../core-Cd5c8LhS.mjs";import{HttpException,HttpStatus}from"../../core/errors.mjs";function toValidationException(error,message,useFirstIssueMessage){let issues=error.issues||[],details=issues.map(i=>({field:i.path?.join(`.`)??``,message:i.message})),finalMessage=useFirstIssueMessage?issues[0]?.message??message:message;return new HttpException(HttpStatus.UNPROCESSABLE_ENTITY,finalMessage,details)}function assignReqProperty(req,key,value){Object.defineProperty(req,key,{value,writable:!0,configurable:!0,enumerable:!0})}function validate(schema){return(req,_res,next)=>{try{if(schema.body){let result=schema.body.safeParse(req.body);if(!result.success)return next(toValidationException(result.error,`Validation failed`,!0));req.body=result.data}if(schema.query){let result=schema.query.safeParse(req.query);if(!result.success)return next(toValidationException(result.error,`Invalid query parameters`,!1));assignReqProperty(req,`query`,result.data)}if(schema.params){let result=schema.params.safeParse(req.params);if(!result.success)return next(toValidationException(result.error,`Invalid path parameters`,!1));assignReqProperty(req,`params`,result.data)}next()}catch(err){next(err)}}}export{validate};
12
12
  //# sourceMappingURL=validate.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.mjs","names":[],"sources":["../../../src/http/middleware/validate.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { HttpException, HttpStatus } from '../../core'\n\nexport interface ValidationSchema {\n body?: any\n query?: any\n params?: any\n}\n\nfunction toValidationException(\n error: any,\n message: string,\n useFirstIssueMessage: boolean,\n): HttpException {\n const issues = error.issues || []\n const details = issues.map((i: any) => ({\n field: i.path?.join('.') ?? '',\n message: i.message,\n }))\n const finalMessage = useFirstIssueMessage ? (issues[0]?.message ?? message) : message\n return new HttpException(HttpStatus.UNPROCESSABLE_ENTITY, finalMessage, details)\n}\n\n/**\n * Express 5 installs `req.query` as a getter-only property (so pluggable\n * `query parser` functions can run lazily). Direct assignment throws in\n * strict mode: `Cannot set property query of #<IncomingMessage> which\n * has only a getter`. `Object.defineProperty` replaces the getter with\n * a plain value slot and sidesteps the throw. Used for `query` and\n * defensively for `params` too — Express 5 currently leaves `params`\n * writable, but swapping the descriptor costs the same and future-proofs\n * against upstream changes.\n */\nfunction assignReqProperty(req: Request, key: 'query' | 'params', value: unknown): void {\n Object.defineProperty(req, key, {\n value,\n writable: true,\n configurable: true,\n enumerable: true,\n })\n}\n\n/**\n * Express middleware that validates request body/query/params against schemas.\n * Works with any validation library that exposes `.safeParse(data)` returning\n * `{ success: true, data }` or `{ success: false, error: { issues } }`.\n *\n * Validation failures are forwarded via `next(err)` as an `HttpException`, so\n * they flow through the application's global error handler (`onError`) and\n * produce a uniform response envelope. Apps that keep the built-in handler\n * continue to see the same `{ message, errors: [{ field, message }] }` body.\n */\nexport function validate(schema: ValidationSchema) {\n return (req: Request, _res: Response, next: NextFunction) => {\n try {\n if (schema.body) {\n const result = schema.body.safeParse(req.body)\n if (!result.success) {\n return next(toValidationException(result.error, 'Validation failed', true))\n }\n req.body = result.data\n }\n\n if (schema.query) {\n const result = schema.query.safeParse(req.query)\n if (!result.success) {\n return next(toValidationException(result.error, 'Invalid query parameters', false))\n }\n assignReqProperty(req, 'query', result.data)\n }\n\n if (schema.params) {\n const result = schema.params.safeParse(req.params)\n if (!result.success) {\n return next(toValidationException(result.error, 'Invalid path parameters', false))\n }\n assignReqProperty(req, 'params', result.data)\n }\n\n next()\n } catch (err) {\n next(err)\n }\n }\n}\n"],"mappings":";;;;;;;;;;4FASA,SAAS,sBACP,MACA,QACA,qBACe,CACf,IAAM,OAAS,MAAM,QAAU,EAAE,CAC3B,QAAU,OAAO,IAAK,IAAY,CACtC,MAAO,EAAE,MAAM,KAAK,IAAI,EAAI,GAC5B,QAAS,EAAE,QACZ,EAAE,CACG,aAAe,qBAAwB,OAAO,IAAI,SAAW,QAAW,QAC9E,OAAO,IAAI,cAAc,WAAW,qBAAsB,aAAc,QAAQ,CAalF,SAAS,kBAAkB,IAAc,IAAyB,MAAsB,CACtF,OAAO,eAAe,IAAK,IAAK,CAC9B,MACA,SAAU,GACV,aAAc,GACd,WAAY,GACb,CAAC,CAaJ,SAAgB,SAAS,OAA0B,CACjD,OAAQ,IAAc,KAAgB,OAAuB,CAC3D,GAAI,CACF,GAAI,OAAO,KAAM,CACf,IAAM,OAAS,OAAO,KAAK,UAAU,IAAI,KAAK,CAC9C,GAAI,CAAC,OAAO,QACV,OAAO,KAAK,sBAAsB,OAAO,MAAO,oBAAqB,GAAK,CAAC,CAE7E,IAAI,KAAO,OAAO,KAGpB,GAAI,OAAO,MAAO,CAChB,IAAM,OAAS,OAAO,MAAM,UAAU,IAAI,MAAM,CAChD,GAAI,CAAC,OAAO,QACV,OAAO,KAAK,sBAAsB,OAAO,MAAO,2BAA4B,GAAM,CAAC,CAErF,kBAAkB,IAAK,QAAS,OAAO,KAAK,CAG9C,GAAI,OAAO,OAAQ,CACjB,IAAM,OAAS,OAAO,OAAO,UAAU,IAAI,OAAO,CAClD,GAAI,CAAC,OAAO,QACV,OAAO,KAAK,sBAAsB,OAAO,MAAO,0BAA2B,GAAM,CAAC,CAEpF,kBAAkB,IAAK,SAAU,OAAO,KAAK,CAG/C,MAAM,OACC,IAAK,CACZ,KAAK,IAAI"}
1
+ {"version":3,"file":"validate.mjs","names":[],"sources":["../../../src/http/middleware/validate.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { HttpException, HttpStatus } from '../../core'\n\nexport interface ValidationSchema {\n body?: any\n query?: any\n params?: any\n}\n\nfunction toValidationException(\n error: any,\n message: string,\n useFirstIssueMessage: boolean,\n): HttpException {\n const issues = error.issues || []\n const details = issues.map((i: any) => ({\n field: i.path?.join('.') ?? '',\n message: i.message,\n }))\n const finalMessage = useFirstIssueMessage ? (issues[0]?.message ?? message) : message\n return new HttpException(HttpStatus.UNPROCESSABLE_ENTITY, finalMessage, details)\n}\n\n/**\n * Express 5 installs `req.query` as a getter-only property (so pluggable\n * `query parser` functions can run lazily). Direct assignment throws in\n * strict mode: `Cannot set property query of #<IncomingMessage> which\n * has only a getter`. `Object.defineProperty` replaces the getter with\n * a plain value slot and sidesteps the throw. Used for `query` and\n * defensively for `params` too — Express 5 currently leaves `params`\n * writable, but swapping the descriptor costs the same and future-proofs\n * against upstream changes.\n */\nfunction assignReqProperty(req: Request, key: 'query' | 'params', value: unknown): void {\n Object.defineProperty(req, key, {\n value,\n writable: true,\n configurable: true,\n enumerable: true,\n })\n}\n\n/**\n * Express middleware that validates request body/query/params against schemas.\n * Works with any validation library that exposes `.safeParse(data)` returning\n * `{ success: true, data }` or `{ success: false, error: { issues } }`.\n *\n * Validation failures are forwarded via `next(err)` as an `HttpException`, so\n * they flow through the application's global error handler (`onError`) and\n * produce a uniform response envelope. Apps that keep the built-in handler\n * continue to see the same `{ message, errors: [{ field, message }] }` body.\n */\nexport function validate(schema: ValidationSchema) {\n return (req: Request, _res: Response, next: NextFunction) => {\n try {\n if (schema.body) {\n const result = schema.body.safeParse(req.body)\n if (!result.success) {\n return next(toValidationException(result.error, 'Validation failed', true))\n }\n req.body = result.data\n }\n\n if (schema.query) {\n const result = schema.query.safeParse(req.query)\n if (!result.success) {\n return next(toValidationException(result.error, 'Invalid query parameters', false))\n }\n assignReqProperty(req, 'query', result.data)\n }\n\n if (schema.params) {\n const result = schema.params.safeParse(req.params)\n if (!result.success) {\n return next(toValidationException(result.error, 'Invalid path parameters', false))\n }\n assignReqProperty(req, 'params', result.data)\n }\n\n next()\n } catch (err) {\n next(err)\n }\n }\n}\n"],"mappings":";;;;;;;;;;4FASA,SAAS,sBACP,MACA,QACA,qBACe,CACf,IAAM,OAAS,MAAM,QAAU,CAAC,EAC1B,QAAU,OAAO,IAAK,IAAY,CACtC,MAAO,EAAE,MAAM,KAAK,GAAG,GAAK,GAC5B,QAAS,EAAE,OACb,EAAE,EACI,aAAe,qBAAwB,OAAO,IAAI,SAAW,QAAW,QAC9E,OAAO,IAAI,cAAc,WAAW,qBAAsB,aAAc,OAAO,CACjF,CAYA,SAAS,kBAAkB,IAAc,IAAyB,MAAsB,CACtF,OAAO,eAAe,IAAK,IAAK,CAC9B,MACA,SAAU,GACV,aAAc,GACd,WAAY,EACd,CAAC,CACH,CAYA,SAAgB,SAAS,OAA0B,CACjD,OAAQ,IAAc,KAAgB,OAAuB,CAC3D,GAAI,CACF,GAAI,OAAO,KAAM,CACf,IAAM,OAAS,OAAO,KAAK,UAAU,IAAI,IAAI,EAC7C,GAAI,CAAC,OAAO,QACV,OAAO,KAAK,sBAAsB,OAAO,MAAO,oBAAqB,EAAI,CAAC,EAE5E,IAAI,KAAO,OAAO,IACpB,CAEA,GAAI,OAAO,MAAO,CAChB,IAAM,OAAS,OAAO,MAAM,UAAU,IAAI,KAAK,EAC/C,GAAI,CAAC,OAAO,QACV,OAAO,KAAK,sBAAsB,OAAO,MAAO,2BAA4B,EAAK,CAAC,EAEpF,kBAAkB,IAAK,QAAS,OAAO,IAAI,CAC7C,CAEA,GAAI,OAAO,OAAQ,CACjB,IAAM,OAAS,OAAO,OAAO,UAAU,IAAI,MAAM,EACjD,GAAI,CAAC,OAAO,QACV,OAAO,KAAK,sBAAsB,OAAO,MAAO,0BAA2B,EAAK,CAAC,EAEnF,kBAAkB,IAAK,SAAU,OAAO,IAAI,CAC9C,CAEA,KAAK,CACP,OAAS,IAAK,CACZ,KAAK,GAAG,CACV,CACF,CACF"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as ViewAdapterOptions, t as ViewAdapter } from "../../views-DDT9vghc.mjs";
2
+ import { n as ViewAdapterOptions, t as ViewAdapter } from "../../views-DGbyY_oV.mjs";
3
3
  export { ViewAdapter, ViewAdapterOptions };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.1
2
+ * @forinda/kickjs v5.9.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{n as defineAdapter}from"../../core-DR36jGxB.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{n as defineAdapter}from"../../core-Cd5c8LhS.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