@forinda/kickjs 6.1.1 → 6.3.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 (167) hide show
  1. package/README.md +15 -0
  2. package/dist/{adapter-D_uzU-QA.d.mts → adapter-stSpsUHs.d.mts} +63 -17
  3. package/dist/adapter-stSpsUHs.d.mts.map +1 -0
  4. package/dist/{app-module-CuAS6hJC.d.mts → app-module-BwYtACD6.d.mts} +3 -3
  5. package/dist/{app-module-CuAS6hJC.d.mts.map → app-module-BwYtACD6.d.mts.map} +1 -1
  6. package/dist/application-CHYTI1Q_.mjs +12 -0
  7. package/dist/application-CHYTI1Q_.mjs.map +1 -0
  8. package/dist/{application-CFenLjbY.d.mts → application-C_KlrNoH.d.mts} +4 -4
  9. package/dist/{application-CFenLjbY.d.mts.map → application-C_KlrNoH.d.mts.map} +1 -1
  10. package/dist/{bootstrap-DodgsiDG.d.mts → bootstrap-BgffgFNX.d.mts} +2 -2
  11. package/dist/{bootstrap-DodgsiDG.d.mts.map → bootstrap-BgffgFNX.d.mts.map} +1 -1
  12. package/dist/{bootstrap-Dj96_O6j.mjs → bootstrap-sB0dlLu7.mjs} +3 -3
  13. package/dist/{bootstrap-Dj96_O6j.mjs.map → bootstrap-sB0dlLu7.mjs.map} +1 -1
  14. package/dist/{cache-BSFDtsEG.d.mts → cache-CwZzIH0D.d.mts} +9 -11
  15. package/dist/cache-CwZzIH0D.d.mts.map +1 -0
  16. package/dist/config/index.d.mts +1 -1
  17. package/dist/config/index.mjs +2 -2
  18. package/dist/{config-sXLOhrp4.mjs → config-DqO6gB-n.mjs} +3 -3
  19. package/dist/{config-sXLOhrp4.mjs.map → config-DqO6gB-n.mjs.map} +1 -1
  20. package/dist/container-C522k8AX.mjs +108 -0
  21. package/dist/container-C522k8AX.mjs.map +1 -0
  22. package/dist/{container-TWOUyR_0.d.mts → container-iED1swkd.d.mts} +19 -3
  23. package/dist/container-iED1swkd.d.mts.map +1 -0
  24. package/dist/context-Cmnmn68a.mjs +12 -0
  25. package/dist/context-Cmnmn68a.mjs.map +1 -0
  26. package/dist/contributor-runner-C4qFEVIz.mjs +13 -0
  27. package/dist/contributor-runner-C4qFEVIz.mjs.map +1 -0
  28. package/dist/core/adapter.d.mts +1 -1
  29. package/dist/core/adapter.mjs +1 -1
  30. package/dist/core/app-module.d.mts +1 -1
  31. package/dist/core/app-module.mjs +1 -1
  32. package/dist/core/cache.d.mts +1 -1
  33. package/dist/core/cache.mjs +2 -2
  34. package/dist/core/cache.mjs.map +1 -1
  35. package/dist/core/container.d.mts +1 -1
  36. package/dist/core/container.mjs +2 -2
  37. package/dist/core/cron.mjs +2 -2
  38. package/dist/core/decorators.d.mts +2 -2
  39. package/dist/core/decorators.mjs +2 -2
  40. package/dist/core/decorators.mjs.map +1 -1
  41. package/dist/core/errors.mjs +1 -1
  42. package/dist/core/interfaces.d.mts +1 -1
  43. package/dist/core/interfaces.mjs +2 -2
  44. package/dist/core/interfaces.mjs.map +1 -1
  45. package/dist/core/logger.mjs +2 -2
  46. package/dist/core/logger.mjs.map +1 -1
  47. package/dist/core/path.mjs +1 -1
  48. package/dist/core/plugin.d.mts +1 -1
  49. package/dist/core/plugin.mjs +1 -1
  50. package/dist/core/reactivity.d.mts +1 -1
  51. package/dist/core/reactivity.mjs +2 -2
  52. package/dist/core/reactivity.mjs.map +1 -1
  53. package/dist/core-3S12kJ7Q.mjs +12 -0
  54. package/dist/core-3S12kJ7Q.mjs.map +1 -0
  55. package/dist/csrf-7nUXGTuz.d.mts.map +1 -1
  56. package/dist/csrf-CbB5oAPn.mjs +12 -0
  57. package/dist/csrf-CbB5oAPn.mjs.map +1 -0
  58. package/dist/{decorators-ejABLo9H.d.mts → decorators-wEJBqXHg.d.mts} +21 -5
  59. package/dist/decorators-wEJBqXHg.d.mts.map +1 -0
  60. package/dist/disposables-BBTIbZkX.mjs +12 -0
  61. package/dist/disposables-BBTIbZkX.mjs.map +1 -0
  62. package/dist/env-Cj5QLqnZ.mjs +13 -0
  63. package/dist/{env-RP8lbK-Z.mjs.map → env-Cj5QLqnZ.mjs.map} +1 -1
  64. package/dist/handler-db1-nMd6.mjs +12 -0
  65. package/dist/handler-db1-nMd6.mjs.map +1 -0
  66. package/dist/http/application.d.mts +1 -1
  67. package/dist/http/application.mjs +2 -2
  68. package/dist/http/bootstrap.d.mts +1 -1
  69. package/dist/http/bootstrap.mjs +2 -2
  70. package/dist/http/context.d.mts +3 -3
  71. package/dist/http/context.mjs +2 -2
  72. package/dist/http/middleware/cors.mjs +1 -1
  73. package/dist/http/middleware/csrf.mjs +2 -3
  74. package/dist/http/middleware/error-handler.mjs +2 -2
  75. package/dist/http/middleware/helmet.mjs +1 -1
  76. package/dist/http/middleware/rate-limit.mjs +2 -2
  77. package/dist/http/middleware/rate-limit.mjs.map +1 -1
  78. package/dist/http/middleware/request-id.mjs +2 -2
  79. package/dist/http/middleware/request-id.mjs.map +1 -1
  80. package/dist/http/middleware/request-logger.mjs +2 -2
  81. package/dist/http/middleware/session.mjs +2 -2
  82. package/dist/http/middleware/session.mjs.map +1 -1
  83. package/dist/http/middleware/spa.d.mts +1 -1
  84. package/dist/http/middleware/spa.mjs +2 -2
  85. package/dist/http/middleware/spa.mjs.map +1 -1
  86. package/dist/http/middleware/upload.d.mts +2 -2
  87. package/dist/http/middleware/upload.mjs +3 -3
  88. package/dist/http/middleware/upload.mjs.map +1 -1
  89. package/dist/http/middleware/validate.mjs +2 -2
  90. package/dist/http/middleware/validate.mjs.map +1 -1
  91. package/dist/http/middleware/views.d.mts +1 -1
  92. package/dist/http/middleware/views.mjs +2 -2
  93. package/dist/http/query/index.mjs +2 -2
  94. package/dist/http/router-builder.d.mts +1 -1
  95. package/dist/http/router-builder.mjs +2 -2
  96. package/dist/http/router-builder.mjs.map +1 -1
  97. package/dist/http/runtimes/fastify.d.mts +1 -1
  98. package/dist/http/runtimes/fastify.d.mts.map +1 -1
  99. package/dist/http/runtimes/fastify.mjs +3 -3
  100. package/dist/http/runtimes/fastify.mjs.map +1 -1
  101. package/dist/http/runtimes/h3-web.d.mts +52 -0
  102. package/dist/http/runtimes/h3-web.d.mts.map +1 -0
  103. package/dist/http/runtimes/h3-web.mjs +12 -0
  104. package/dist/http/runtimes/h3-web.mjs.map +1 -0
  105. package/dist/http/runtimes/h3.d.mts +10 -4
  106. package/dist/http/runtimes/h3.d.mts.map +1 -1
  107. package/dist/http/runtimes/h3.mjs +2 -2
  108. package/dist/http/runtimes/h3.mjs.map +1 -1
  109. package/dist/{index-zIcb4wjg.d.mts → index-D98NLfS5.d.mts} +2 -2
  110. package/dist/{index-zIcb4wjg.d.mts.map → index-D98NLfS5.d.mts.map} +1 -1
  111. package/dist/index.d.mts +16 -15
  112. package/dist/index.mjs +2 -2
  113. package/dist/index.mjs.map +1 -1
  114. package/dist/{interfaces-DbTyKGdS.d.mts → interfaces-CayXPOAf.d.mts} +2 -1
  115. package/dist/{interfaces-DbTyKGdS.d.mts.map → interfaces-CayXPOAf.d.mts.map} +1 -1
  116. package/dist/{metadata-C-yduM8v.mjs → metadata-W54KhZoU.mjs} +2 -2
  117. package/dist/{metadata-C-yduM8v.mjs.map → metadata-W54KhZoU.mjs.map} +1 -1
  118. package/dist/module-registry-BbYVBwD9.mjs +12 -0
  119. package/dist/module-registry-BbYVBwD9.mjs.map +1 -0
  120. package/dist/{query-C0vJlGKm.mjs → query-B0ucxuU0.mjs} +2 -2
  121. package/dist/{query-C0vJlGKm.mjs.map → query-B0ucxuU0.mjs.map} +1 -1
  122. package/dist/rate-limit-oLL5EPUf.d.mts.map +1 -1
  123. package/dist/{reactivity-DWmLcN0A.d.mts → reactivity-XL0fFDvG.d.mts} +10 -16
  124. package/dist/reactivity-XL0fFDvG.d.mts.map +1 -0
  125. package/dist/reply-2CvBP_Sl.d.mts +63 -0
  126. package/dist/reply-2CvBP_Sl.d.mts.map +1 -0
  127. package/dist/reply-DI4EqF0R.mjs +12 -0
  128. package/dist/reply-DI4EqF0R.mjs.map +1 -0
  129. package/dist/request-id-IDobbxU-.d.mts.map +1 -1
  130. package/dist/rolldown-runtime-Dv-xQPN8.mjs +11 -0
  131. package/dist/{router-builder-Bh3qSiDz.d.mts → router-builder-BK7yPm89.d.mts} +2 -2
  132. package/dist/router-builder-BK7yPm89.d.mts.map +1 -0
  133. package/dist/session-DSRA-PUX.d.mts.map +1 -1
  134. package/dist/{spa-BdvZFXJz.d.mts → spa-BwO1qOuA.d.mts} +2 -2
  135. package/dist/{spa-BdvZFXJz.d.mts.map → spa-BwO1qOuA.d.mts.map} +1 -1
  136. package/dist/{upload-Bc_rBRtE.d.mts → upload-CIZc803m.d.mts} +9 -7
  137. package/dist/upload-CIZc803m.d.mts.map +1 -0
  138. package/dist/upload-config-D8gsl_ox.mjs +12 -0
  139. package/dist/upload-config-D8gsl_ox.mjs.map +1 -0
  140. package/dist/validate-DoZDOhCi.d.mts.map +1 -1
  141. package/dist/{views-DcTVDiOh.d.mts → views-w0EPrhui.d.mts} +7 -5
  142. package/dist/views-w0EPrhui.d.mts.map +1 -0
  143. package/dist/web.d.mts +150 -0
  144. package/dist/web.d.mts.map +1 -0
  145. package/dist/web.mjs +12 -0
  146. package/dist/web.mjs.map +1 -0
  147. package/package.json +22 -13
  148. package/dist/adapter-D_uzU-QA.d.mts.map +0 -1
  149. package/dist/application-KaKi6jgi.mjs +0 -12
  150. package/dist/application-KaKi6jgi.mjs.map +0 -1
  151. package/dist/cache-BSFDtsEG.d.mts.map +0 -1
  152. package/dist/container-Oy24XRnS.mjs +0 -108
  153. package/dist/container-Oy24XRnS.mjs.map +0 -1
  154. package/dist/container-TWOUyR_0.d.mts.map +0 -1
  155. package/dist/context-Co72dNA_.mjs +0 -12
  156. package/dist/context-Co72dNA_.mjs.map +0 -1
  157. package/dist/core-kmJ4y4fJ.mjs +0 -13
  158. package/dist/core-kmJ4y4fJ.mjs.map +0 -1
  159. package/dist/decorators-ejABLo9H.d.mts.map +0 -1
  160. package/dist/env-RP8lbK-Z.mjs +0 -13
  161. package/dist/http/middleware/csrf.mjs.map +0 -1
  162. package/dist/reactivity-DWmLcN0A.d.mts.map +0 -1
  163. package/dist/request-scope-C_taFScZ.mjs +0 -12
  164. package/dist/request-scope-C_taFScZ.mjs.map +0 -1
  165. package/dist/router-builder-Bh3qSiDz.d.mts.map +0 -1
  166. package/dist/upload-Bc_rBRtE.d.mts.map +0 -1
  167. package/dist/views-DcTVDiOh.d.mts.map +0 -1
@@ -1,6 +1,6 @@
1
1
 
2
- import { l as ServiceOptions, n as BuilderOf } from "./interfaces-DbTyKGdS.mjs";
3
- import { i as KickJsRegistry, s as InjectionToken } from "./container-TWOUyR_0.mjs";
2
+ import { l as ServiceOptions, n as BuilderOf } from "./interfaces-CayXPOAf.mjs";
3
+ import { i as KickJsRegistry, s as InjectionToken } from "./container-iED1swkd.mjs";
4
4
  //#region src/core/assets.d.ts
5
5
  declare const ASSET_MANIFEST_VERSION: 1;
6
6
  /**
@@ -76,6 +76,15 @@ declare function Repository(options?: ServiceOptions): ClassDecorator;
76
76
  declare function Controller(): ClassDecorator;
77
77
  /** Mark a method as a lifecycle hook called after instantiation */
78
78
  declare function PostConstruct(): MethodDecorator;
79
+ /**
80
+ * Mark a method as a teardown hook — the counterpart to {@link PostConstruct}.
81
+ *
82
+ * For REQUEST-scoped services it runs when the request's scope closes
83
+ * (response finished or aborted): release per-request resources there
84
+ * (transactions, handles, subscriptions). May be async; errors are logged
85
+ * and swallowed so one failing hook can't break request completion.
86
+ */
87
+ declare function PreDestroy(): MethodDecorator;
79
88
  /**
80
89
  * A decorator that's valid as either a property decorator OR a
81
90
  * constructor-parameter decorator. forinda/kick-js#235 §2 — `@Autowired`
@@ -264,7 +273,14 @@ interface RouteDefinition {
264
273
  validation?: {
265
274
  /** JSON Schema object for validating the request body */body?: any; /** JSON Schema object for validating query parameters */
266
275
  query?: any; /** JSON Schema object for validating URL params */
267
- params?: any; /** Schema name in OpenAPI components/schemas for the request body. Auto-generated from handler name if omitted. */
276
+ params?: any;
277
+ /**
278
+ * DECLARED response schema — never validated at runtime. Feeds two
279
+ * consumers from one source of truth: the Swagger adapter's success
280
+ * response schema, and `kick typegen`'s `KickRoutes[...].response`
281
+ * (winning over return-type inference when present).
282
+ */
283
+ response?: any; /** Schema name in OpenAPI components/schemas for the request body. Auto-generated from handler name if omitted. */
268
284
  name?: string;
269
285
  };
270
286
  }
@@ -408,5 +424,5 @@ declare function withBuilder<C extends new (...args: any[]) => any>(ctor: C): C
408
424
  builder(): BuilderOf<InstanceType<C>>;
409
425
  };
410
426
  //#endregion
411
- export { Value as A, useAssets as B, Post as C, Repository as D, Put as E, KickAssets as F, UnknownAssetError as I, assets as L, withBuilder as M, ASSETS as N, RouteDefinition as O, ASSET_MANIFEST_VERSION as P, clearAssetCache as R, Patch as S, PropertyOrParameterDecorator as T, Get as _, Autowired as a, Middleware as b, ColumnApiQueryParamsConfig as c, Delete as d, Env as f, FileUploadConfig as g, FileUpload as h, AssetKey as i, normalizeApiQueryParamsConfig as j, Service as k, Component as l, FileTypeFilter as m, ApiQueryParamsConfig as n, BaseUploadOptions as o, EnvKey as p, Asset as r, Builder as s, ApiQueryParams as t, Controller as u, Inject as v, PostConstruct as w, MiddlewareHandler as x, Injectable as y, resolveAsset as z };
412
- //# sourceMappingURL=decorators-ejABLo9H.d.mts.map
427
+ export { Service as A, resolveAsset as B, Post as C, Put as D, PropertyOrParameterDecorator as E, ASSET_MANIFEST_VERSION as F, KickAssets as I, UnknownAssetError as L, normalizeApiQueryParamsConfig as M, withBuilder as N, Repository as O, ASSETS as P, assets as R, Patch as S, PreDestroy as T, useAssets as V, Get as _, Autowired as a, Middleware as b, ColumnApiQueryParamsConfig as c, Delete as d, Env as f, FileUploadConfig as g, FileUpload as h, AssetKey as i, Value as j, RouteDefinition as k, Component as l, FileTypeFilter as m, ApiQueryParamsConfig as n, BaseUploadOptions as o, EnvKey as p, Asset as r, Builder as s, ApiQueryParams as t, Controller as u, Inject as v, PostConstruct as w, MiddlewareHandler as x, Injectable as y, clearAssetCache as z };
428
+ //# sourceMappingURL=decorators-wEJBqXHg.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators-wEJBqXHg.d.mts","names":[],"sources":["../src/core/assets.ts","../src/core/decorators.ts"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @forinda/kickjs v6.3.0
3
+ *
4
+ * Copyright (c) Felix Orinda
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ const disposables=new Set;function registerDisposable(fn){return disposables.add(fn),()=>disposables.delete(fn)}function drainDisposables(){let pending=[...disposables];return disposables.clear(),pending}async function runDisposables(list){await Promise.allSettled(list.map(async fn=>fn()))}async function disposeAll(){await runDisposables(drainDisposables())}export{runDisposables as i,drainDisposables as n,registerDisposable as r,disposeAll as t};
12
+ //# sourceMappingURL=disposables-BBTIbZkX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disposables-BBTIbZkX.mjs","names":[],"sources":["../src/core/disposables.ts"],"sourcesContent":["/**\n * Registry for framework-owned resources that hold timers or other handles\n * outside the adapter/plugin lifecycle — e.g. the in-memory session and\n * rate-limit stores' cleanup intervals. `Application.shutdown()` drains it so\n * startup and shutdown stay symmetric even for resources created lazily by\n * middleware factories.\n *\n * Intervals are `.unref()`'d so they never wedge process exit — this registry\n * exists for IN-PROCESS teardown (test suites, HMR, multi-app hosts) where\n * each un-disposed store would otherwise leak a live interval + backing Map\n * per app instance.\n */\n\nexport type Disposable = () => void | Promise<void>\n\nconst disposables = new Set<Disposable>()\n\n/** Register a cleanup callback. Returns an unregister function. */\nexport function registerDisposable(fn: Disposable): () => void {\n disposables.add(fn)\n return () => disposables.delete(fn)\n}\n\n/**\n * Remove and return all currently registered disposables WITHOUT running\n * them. Used by HMR rebuild to snapshot the outgoing app's resources before\n * `setup()` registers the incoming app's — so the stale set can be disposed\n * on success, or restored on failure, without touching the new one.\n */\nexport function drainDisposables(): Disposable[] {\n const pending = [...disposables]\n disposables.clear()\n return pending\n}\n\n/**\n * Run a list of disposables. Errors — including SYNCHRONOUS throws — are\n * swallowed per-entry: the `async` callback converts a sync throw into a\n * rejection that `allSettled` absorbs, so one bad disposable can't abort\n * the rest (a bare `Promise.resolve(fn())` would let the throw escape the\n * `.map()` before `allSettled` ever ran).\n */\nexport async function runDisposables(list: Disposable[]): Promise<void> {\n await Promise.allSettled(list.map(async (fn) => fn()))\n}\n\n/** Run and clear all registered disposables. Errors are swallowed per-entry. */\nexport async function disposeAll(): Promise<void> {\n await runDisposables(drainDisposables())\n}\n"],"mappings":";;;;;;;;;;AAeA,MAAM,YAAc,IAAI,IAGxB,SAAgB,mBAAmB,GAA4B,CAE7D,OADA,YAAY,IAAI,EAAE,MACL,YAAY,OAAO,EAAE,CACpC,CAQA,SAAgB,kBAAiC,CAC/C,IAAM,QAAU,CAAC,GAAG,WAAW,EAE/B,OADA,YAAY,MAAM,EACX,OACT,CASA,eAAsB,eAAe,KAAmC,CACtE,MAAM,QAAQ,WAAW,KAAK,IAAI,KAAO,KAAO,GAAG,CAAC,CAAC,CACvD,CAGA,eAAsB,YAA4B,CAChD,MAAM,eAAe,iBAAiB,CAAC,CACzC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @forinda/kickjs v6.3.0
3
+ *
4
+ * Copyright (c) Felix Orinda
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ import{t as __require}from"./rolldown-runtime-Dv-xQPN8.mjs";import{t as Container}from"./container-C522k8AX.mjs";import{detectSchema}from"@forinda/kickjs-schema";let _zod;function getZod(){if(_zod)return _zod;try{_zod=__require(`zod`)}catch{throw Error("@forinda/kickjs: the 'zod' peer dependency is required for baseEnvSchema / defineEnv / loadEnv but is not installed. Install it (`pnpm add zod`), or define your env with a non-Zod schema and call loadEnvFromSchema() instead — @forinda/kickjs-schema supports Valibot, Yup, and any Standard Schema validator without zod.")}return _zod}function tryLoadDotenv(){try{__require(`dotenv`).config({override:!1,quiet:!0})}catch{}}tryLoadDotenv();function buildBaseEnvSchema(){let z=getZod();return z.object({PORT:z.coerce.number().default(3e3),NODE_ENV:z.enum([`development`,`production`,`test`]).default(`development`),LOG_LEVEL:z.string().default(`info`)})}let _baseEnvSchema;function getBaseEnvSchema(){return _baseEnvSchema||=buildBaseEnvSchema(),_baseEnvSchema}const baseEnvSchema=new Proxy({},{get(_t,prop){let real=getBaseEnvSchema(),value=real[prop];return typeof value==`function`?value.bind(real):value},has(_t,prop){return prop in getBaseEnvSchema()}});let cachedEnv=null,cachedSchema=null;function defineEnv(extend){let base=getBaseEnvSchema(),userSchema=extend(base);return base.extend(userSchema.shape)}function loadEnv(schema){let s=schema??cachedSchema??getBaseEnvSchema();return cachedEnv&&cachedSchema===s?cachedEnv:(cachedSchema=s,cachedEnv=s.parse(process.env),Container._envResolver=key=>cachedEnv?.[key],cachedEnv)}function getEnv(key,schema){return(schema?loadEnv(schema):loadEnv())[key]}function reloadEnv(){try{__require(`dotenv`).config({override:!0,quiet:!0})}catch{}cachedEnv=null,cachedSchema&&(typeof cachedSchema.parse==`function`?loadEnv(cachedSchema):loadEnvFromSchema(cachedSchema))}function resetEnvCache(){cachedEnv=null,cachedSchema=null}function loadEnvFromSchema(schema){let result=detectSchema(schema).safeParse(process.env);if(!result.success){let details=result.issues.map(i=>` ${i.path.join(`.`)||`(root)`}: ${i.message}`).join(`
12
+ `);throw Error(`Environment validation failed:\n${details}`)}return cachedEnv=result.data,cachedSchema=schema,Container._envResolver=key=>cachedEnv?.[key],result.data}export{loadEnvFromSchema as a,loadEnv as i,defineEnv as n,reloadEnv as o,getEnv as r,resetEnvCache as s,baseEnvSchema as t};
13
+ //# sourceMappingURL=env-Cj5QLqnZ.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"env-RP8lbK-Z.mjs","names":[],"sources":["../src/config/env.ts"],"sourcesContent":["import type { z } from 'zod'\nimport { detectSchema, type InferSchemaOutput } from '@forinda/kickjs-schema'\nimport { Container } from '../core/container'\nimport type { EnvKey } from '../core/decorators'\n\n/**\n * Lazy `zod` accessor. `zod` is an **optional** peer of\n * `@forinda/kickjs` — only the Zod-based env helpers (`baseEnvSchema`,\n * `defineEnv`, `loadEnv`) need it. Apps that validate env via Valibot /\n * Yup / Standard Schema go through `loadEnvFromSchema` (which only\n * touches the validator-agnostic `detectSchema`) and never load zod.\n *\n * Importing `zod` at module top-level made it a *hard* dependency of\n * the whole framework: `import { anything } from '@forinda/kickjs'`\n * eagerly evaluated this module, so a non-Zod app with no `zod`\n * installed crashed at module load / build time (\"Cannot find module\n * 'zod'\"). Deferring the require to first use keeps the import out of\n * the eager graph.\n */\nlet _zod: typeof import('zod') | undefined\nfunction getZod(): typeof import('zod') {\n if (_zod) return _zod\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n _zod = require('zod') as typeof import('zod')\n } catch {\n throw new Error(\n \"@forinda/kickjs: the 'zod' peer dependency is required for \" +\n 'baseEnvSchema / defineEnv / loadEnv but is not installed. ' +\n 'Install it (`pnpm add zod`), or define your env with a non-Zod ' +\n 'schema and call loadEnvFromSchema() instead — @forinda/kickjs-schema ' +\n 'supports Valibot, Yup, and any Standard Schema validator without zod.',\n )\n }\n return _zod\n}\n\n/**\n * Lazily load `.env` via dotenv if it is installed in the consumer app.\n * `dotenv` is an **optional peer dependency** of `@forinda/kickjs` —\n * apps that want `.env` file support install it themselves (the CLI\n * generators add it to every scaffolded `package.json`). Apps that\n * inject env via the shell, Docker, or a secret manager can skip it\n * entirely and `process.env` is read as-is.\n *\n * We call `dotenv.config()` directly rather than `require('dotenv/config')`\n * so that a re-evaluation of this module (e.g. Vite SSR full-reload after\n * editing `.env`) can re-read the file. `dotenv/config` is a side-effect\n * shim that CJS caches aggressively, so the second `require` is a no-op\n * and new keys never reach `process.env` — the root cause of the\n * \"DATABASE_URL undefined until restart\" bug on Windows dev.\n */\nfunction tryLoadDotenv(): void {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const dotenv = require('dotenv')\n // `quiet: true` suppresses dotenv 17's tip banner that goes to stdout.\n // Required for CLI tools whose stdout is parsed by callers (`kick explain\n // --json`, etc) — the banner would otherwise corrupt the JSON output.\n dotenv.config({ override: false, quiet: true })\n } catch {\n // dotenv not installed — fall through, env vars must come from\n // the environment directly. This is a valid deployment style.\n }\n}\ntryLoadDotenv()\n\n/**\n * Build the base env schema. Memoized so repeated calls reuse one\n * schema instance (matters for the `cachedSchema === s` identity check\n * in {@link loadEnv}). Loads zod lazily via {@link getZod}.\n */\n// Build the concrete base schema. This function is the single source of\n// the *type* of `baseEnvSchema` (via `ReturnType<typeof buildBaseEnvSchema>`)\n// AND its runtime value (via `getBaseEnvSchema`). It is NEVER called at\n// module load — only lazily through `getBaseEnvSchema()` — so referencing\n// its return type stays compile-time-only and pulls no eager zod import.\n// Spelling the type this way (rather than `z.ZodObject<any>`) preserves the\n// precise PORT/NODE_ENV/LOG_LEVEL shape, which `defineEnv` composition and\n// the `kick typegen` `KickEnv` inference depend on.\nfunction buildBaseEnvSchema() {\n const z = getZod()\n return z.object({\n // Server\n PORT: z.coerce.number().default(3000),\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n LOG_LEVEL: z.string().default('info'),\n })\n}\n\n/** Precise type of the base env schema — no runtime value, no eager zod. */\nexport type BaseEnvSchema = ReturnType<typeof buildBaseEnvSchema>\n\n/**\n * Memoized base schema. Reuses one instance so the `cachedSchema === s`\n * identity check in {@link loadEnv} holds. Loads zod lazily.\n */\nlet _baseEnvSchema: BaseEnvSchema | undefined\nexport function getBaseEnvSchema(): BaseEnvSchema {\n if (!_baseEnvSchema) _baseEnvSchema = buildBaseEnvSchema()\n return _baseEnvSchema\n}\n\n/**\n * Base environment schema with common server variables.\n * Users extend this with their own application-specific vars.\n *\n * Back-compat lazy view over {@link getBaseEnvSchema}: constructing the\n * Proxy does NOT load zod — zod is only required the moment a property\n * (`.extend`, `.parse`, `.shape`, …) is actually accessed. This keeps\n * `import { baseEnvSchema } from '@forinda/kickjs'` zero-cost for apps\n * that don't use the Zod env path, while keeping the precise schema type.\n */\nexport const baseEnvSchema: BaseEnvSchema = new Proxy({} as BaseEnvSchema, {\n get(_t, prop) {\n const real = getBaseEnvSchema() as unknown as Record<string | symbol, unknown>\n const value = real[prop]\n // Bind methods to the real schema so zod's internal `this` works.\n return typeof value === 'function' ? (value as (...a: unknown[]) => unknown).bind(real) : value\n },\n has(_t, prop) {\n return prop in (getBaseEnvSchema() as object)\n },\n}) as BaseEnvSchema\n\n/** Cached env config to avoid re-parsing — keyed by schema reference */\nlet cachedEnv: any = null\nlet cachedSchema: any = null\n\n/**\n * Define a custom env schema. The base schema (`PORT`, `NODE_ENV`,\n * `LOG_LEVEL`) is always merged in, so users never need to remember to\n * extend it themselves — both styles below produce the same result and\n * both surface the base keys in `KickEnv`.\n *\n * Returns a Zod schema that validates `process.env`.\n *\n * @example\n * ```ts\n * // Style A — return a fresh object; base fields are merged automatically\n * export default defineEnv(() =>\n * z.object({\n * DATABASE_URL: z.string().url(),\n * JWT_SECRET: z.string().min(32),\n * })\n * )\n *\n * // Style B — explicitly extend `base` (still works, identical result)\n * export default defineEnv((base) =>\n * base.extend({\n * DATABASE_URL: z.string().url(),\n * })\n * )\n * ```\n *\n * User-defined keys override base keys when they collide, so a project\n * can re-type `PORT` or tighten `NODE_ENV` without losing the rest of\n * the base shape.\n */\nexport function defineEnv<T extends z.ZodRawShape>(\n extend: (base: typeof baseEnvSchema) => z.ZodObject<T>,\n): z.ZodObject<typeof baseEnvSchema.shape & T> {\n const base = getBaseEnvSchema()\n const userSchema = extend(base as typeof baseEnvSchema)\n // Always merge the base in. `extend` lets the user's shape override\n // any colliding base keys, which preserves the escape hatch for\n // projects that want to re-type `PORT` etc.\n return base.extend(userSchema.shape) as z.ZodObject<typeof baseEnvSchema.shape & T>\n}\n\n/**\n * Parse and validate `process.env` against a Zod schema. Caches result per schema.\n *\n * **No-arg behaviour is sticky.** The first time `loadEnv(schema)` is called\n * with an extended schema, the cache holds that schema. Subsequent\n * `loadEnv()` calls (no arg) will reuse the cached extended schema rather\n * than falling back to `baseEnvSchema`.\n *\n * Stickiness matters because `ConfigService` and the `@Value()` decorator\n * both read through the no-arg form internally. Without it, resolving\n * either of them after a `loadEnv(extendedSchema)` call would silently\n * downgrade the env to the base shape and drop every user-defined key.\n *\n * To force a re-parse with a different schema, pass the new schema explicitly.\n * To start fresh, call `resetEnvCache()` (full reset) or `reloadEnv()`\n * (re-parse against the same schema after a `.env` file change).\n *\n * **Typing:**\n * - With an explicit schema → returns `z.infer<typeof schema>`.\n * - With no schema and `KickEnv` populated by typegen → returns\n * `KickEnv` (the project-wide typed env shape).\n * - With no schema and `KickEnv` empty → returns the base `Env`.\n */\nexport function loadEnv<T extends z.ZodObject<any>>(schema: T): z.infer<T>\nexport function loadEnv(): [EnvKey] extends [never] ? Env : KickEnv\nexport function loadEnv<T extends z.ZodObject<any>>(schema?: T): any {\n // Sticky: when called with no arg, prefer the most recently cached\n // schema over re-parsing with the base schema.\n const s = schema ?? cachedSchema ?? getBaseEnvSchema()\n // Re-parse if schema changed or no cache yet\n if (cachedEnv && cachedSchema === s) return cachedEnv\n cachedSchema = s\n cachedEnv = s.parse(process.env)\n\n // Register env resolver so @Value() reads validated values\n Container._envResolver = (key: string) => cachedEnv?.[key]\n\n return cachedEnv\n}\n\n/**\n * Get a single typed environment variable value.\n *\n * Three forms:\n * - **No-arg, KickEnv populated** (`kick typegen` has run) → key is\n * constrained to known `KickEnv` keys; return type inferred from\n * the schema.\n * - **With explicit schema** → key constrained to that schema's keys;\n * return type is `z.infer<typeof schema>[K]`.\n * - **No-arg, KickEnv empty** → loose `string` key, `any` return.\n *\n * @example\n * ```ts\n * // After `kick typegen` (KickEnv populated):\n * const port = getEnv('PORT') // typed as number\n * const url = getEnv('DATABASE_URL') // typed as string\n *\n * // With an inline schema (legacy / one-off):\n * const dbUrl = getEnv('DATABASE_URL', envSchema)\n * ```\n */\nexport function getEnv<K extends EnvKey>(key: K): KickEnv[K]\nexport function getEnv<T extends z.ZodObject<any>, K extends string & keyof z.infer<T>>(\n key: K,\n schema: T,\n): z.infer<T>[K]\nexport function getEnv(key: string, schema?: z.ZodObject<any>): any {\n const env = schema ? loadEnv(schema) : (loadEnv() as Record<string, any>)\n return env[key]\n}\n\n/**\n * Reload env from `process.env`. Re-reads `.env` via dotenv (if\n * installed), then re-parses the result against the **same** schema\n * the project registered at startup. Called during HMR rebuild and by\n * `envWatchPlugin` to pick up `.env` file changes mid-session.\n *\n * Crucially, this preserves the registered schema. Wiping it would\n * silently downgrade `ConfigService` and `@Value()` back to the base\n * schema on the next read, so every user-defined key would start\n * resolving as `undefined` after the first `.env` save — which was the\n * original \"config disappears in dev\" bug.\n *\n * If you need to fully forget the schema (e.g. tests that swap schemas\n * between cases), call {@link resetEnvCache} instead.\n */\nexport function reloadEnv(): void {\n // Re-read .env file into process.env if dotenv is installed.\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require('dotenv').config({ override: true, quiet: true })\n } catch {\n // dotenv not installed — nothing to reload, env comes from the\n // environment directly.\n }\n\n // Drop the parsed snapshot but keep the schema. Re-parse eagerly so\n // existing consumers (including ConfigService getters and @Value\n // resolvers that may already hold references to the env state) see\n // the new values on their very next read instead of having to\n // re-trigger any cache-population step.\n cachedEnv = null\n if (cachedSchema) {\n if (typeof cachedSchema.parse === 'function') {\n loadEnv(cachedSchema)\n } else {\n loadEnvFromSchema(cachedSchema)\n }\n }\n}\n\n/**\n * Fully reset the env cache, including the registered schema. Use this\n * in tests that need to swap schemas between cases — `reloadEnv()`\n * deliberately preserves the schema to avoid the \"user keys vanish on\n * `.env` reload\" footgun.\n */\nexport function resetEnvCache(): void {\n cachedEnv = null\n cachedSchema = null\n}\n\n/**\n * Schema-agnostic env loader. Accepts any schema supported by\n * `@forinda/kickjs-schema` (Zod, Valibot, Yup, Joi, Standard Schema,\n * KickSchema adapters, or plain validator functions).\n *\n * Unlike `loadEnv()` which uses Zod's `.parse()` and throws a\n * `ZodError`, this function uses `detectSchema().safeParse()` and\n * throws a plain `Error` with structured issue messages on failure.\n *\n * @example\n * ```ts\n * import * as v from 'valibot'\n * import { fromValibot } from '@forinda/kickjs-schema/valibot'\n *\n * const envSchema = fromValibot(v.object({\n * PORT: v.pipe(v.string(), v.transform(Number)),\n * NODE_ENV: v.picklist(['development', 'production', 'test']),\n * }))\n *\n * export const env = loadEnvFromSchema(envSchema)\n * ```\n */\n/**\n * Single inferring signature. `TSchema` is inferred from the call site;\n * the conditional return type maps the unknown-input case (TSchema\n * resolves to `unknown` only when the argument is `unknown`-typed\n * because nothing on the value side carried a richer brand) to\n * `Record<string, unknown>` rather than letting `InferSchemaOutput`\n * fall through to `unknown`. The previous two-overload form had the\n * generic overload win for `unknown`-typed inputs (`unknown` is\n * assignable to any `TSchema`), so the documented \"Record\" fallback\n * never actually applied.\n */\nexport function loadEnvFromSchema<TSchema>(\n schema: TSchema,\n): unknown extends TSchema ? Record<string, unknown> : InferSchemaOutput<TSchema>\nexport function loadEnvFromSchema(schema: unknown): unknown {\n const wrapped = detectSchema(schema)\n const result = wrapped.safeParse(process.env)\n if (!result.success) {\n const details = result.issues\n .map((i) => ` ${i.path.join('.') || '(root)'}: ${i.message}`)\n .join('\\n')\n throw new Error(`Environment validation failed:\\n${details}`)\n }\n cachedEnv = result.data\n cachedSchema = schema\n Container._envResolver = (key: string) => cachedEnv?.[key]\n return result.data\n}\n\n/**\n * Output type of {@link baseEnvSchema}. Hand-written (rather than\n * `z.infer<typeof baseEnvSchema>`) so the type is available without the\n * eager `zod` value import — `baseEnvSchema` is now a lazy Proxy typed\n * as `z.ZodObject<any>`, from which `z.infer` could not recover the\n * precise shape anyway.\n */\nexport type Env = {\n PORT: number\n NODE_ENV: 'development' | 'production' | 'test'\n LOG_LEVEL: string\n}\n"],"mappings":";;;;;;;;;;qHAmBA,IAAI,KACJ,SAAS,QAA+B,CACtC,GAAI,KAAM,OAAO,KACjB,GAAI,CAEF,KAAA,UAAe,KAAK,CACtB,MAAQ,CACN,MAAU,MACR,gUAKF,CACF,CACA,OAAO,IACT,CAiBA,SAAS,eAAsB,CAC7B,GAAI,CAMF,UAJuB,QAIlB,CAAC,CAAC,OAAO,CAAE,SAAU,GAAO,MAAO,EAAK,CAAC,CAChD,MAAQ,CAGR,CACF,CACA,cAAc,EAed,SAAS,oBAAqB,CAC5B,IAAM,EAAI,OAAO,EACjB,OAAO,EAAE,OAAO,CAEd,KAAM,EAAE,OAAO,OAAO,CAAC,CAAC,QAAQ,GAAI,EACpC,SAAU,EAAE,KAAK,CAAC,cAAe,aAAc,MAAM,CAAC,CAAC,CAAC,QAAQ,aAAa,EAC7E,UAAW,EAAE,OAAO,CAAC,CAAC,QAAQ,MAAM,CACtC,CAAC,CACH,CASA,IAAI,eACJ,SAAgB,kBAAkC,CAEhD,MADA,CAAqB,iBAAiB,mBAAmB,EAClD,cACT,CAYA,MAAa,cAA+B,IAAI,MAAM,CAAC,EAAoB,CACzE,IAAI,GAAI,KAAM,CACZ,IAAM,KAAO,iBAAiB,EACxB,MAAQ,KAAK,MAEnB,OAAO,OAAO,OAAU,WAAc,MAAuC,KAAK,IAAI,EAAI,KAC5F,EACA,IAAI,GAAI,KAAM,CACZ,OAAO,QAAS,iBAAiB,CACnC,CACF,CAAC,EAGD,IAAI,UAAiB,KACjB,aAAoB,KAgCxB,SAAgB,UACd,OAC6C,CAC7C,IAAM,KAAO,iBAAiB,EACxB,WAAa,OAAO,IAA4B,EAItD,OAAO,KAAK,OAAO,WAAW,KAAK,CACrC,CA2BA,SAAgB,QAAoC,OAAiB,CAGnE,IAAM,EAAI,QAAU,cAAgB,iBAAiB,EASrD,OAPI,WAAa,eAAiB,EAAU,WAC5C,aAAe,EACf,UAAY,EAAE,MAAM,QAAQ,GAAG,EAG/B,UAAU,aAAgB,KAAgB,YAAY,KAE/C,UACT,CA4BA,SAAgB,OAAO,IAAa,OAAgC,CAElE,OADY,OAAS,QAAQ,MAAM,EAAK,QAAQ,EAAA,CACrC,IACb,CAiBA,SAAgB,WAAkB,CAEhC,GAAI,CAEF,UAAQ,QAAQ,CAAC,CAAC,OAAO,CAAE,SAAU,GAAM,MAAO,EAAK,CAAC,CAC1D,MAAQ,CAGR,CAOA,UAAY,KACR,eACE,OAAO,aAAa,OAAU,WAChC,QAAQ,YAAY,EAEpB,kBAAkB,YAAY,EAGpC,CAQA,SAAgB,eAAsB,CACpC,UAAY,KACZ,aAAe,IACjB,CAsCA,SAAgB,kBAAkB,OAA0B,CAE1D,IAAM,OADU,aAAa,MACR,CAAC,CAAC,UAAU,QAAQ,GAAG,EAC5C,GAAI,CAAC,OAAO,QAAS,CACnB,IAAM,QAAU,OAAO,OACpB,IAAK,GAAM,KAAK,EAAE,KAAK,KAAK,GAAG,GAAK,SAAS,IAAI,EAAE,SAAS,CAAC,CAC7D,KAAK;CAAI,EACZ,MAAU,MAAM,mCAAmC,SAAS,CAC9D,CAIA,MAHA,WAAY,OAAO,KACnB,aAAe,OACf,UAAU,aAAgB,KAAgB,YAAY,KAC/C,OAAO,IAChB"}
1
+ {"version":3,"file":"env-Cj5QLqnZ.mjs","names":[],"sources":["../src/config/env.ts"],"sourcesContent":["import type { z } from 'zod'\nimport { detectSchema, type InferSchemaOutput } from '@forinda/kickjs-schema'\nimport { Container } from '../core/container'\nimport type { EnvKey } from '../core/decorators'\n\n/**\n * Lazy `zod` accessor. `zod` is an **optional** peer of\n * `@forinda/kickjs` — only the Zod-based env helpers (`baseEnvSchema`,\n * `defineEnv`, `loadEnv`) need it. Apps that validate env via Valibot /\n * Yup / Standard Schema go through `loadEnvFromSchema` (which only\n * touches the validator-agnostic `detectSchema`) and never load zod.\n *\n * Importing `zod` at module top-level made it a *hard* dependency of\n * the whole framework: `import { anything } from '@forinda/kickjs'`\n * eagerly evaluated this module, so a non-Zod app with no `zod`\n * installed crashed at module load / build time (\"Cannot find module\n * 'zod'\"). Deferring the require to first use keeps the import out of\n * the eager graph.\n */\nlet _zod: typeof import('zod') | undefined\nfunction getZod(): typeof import('zod') {\n if (_zod) return _zod\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n _zod = require('zod') as typeof import('zod')\n } catch {\n throw new Error(\n \"@forinda/kickjs: the 'zod' peer dependency is required for \" +\n 'baseEnvSchema / defineEnv / loadEnv but is not installed. ' +\n 'Install it (`pnpm add zod`), or define your env with a non-Zod ' +\n 'schema and call loadEnvFromSchema() instead — @forinda/kickjs-schema ' +\n 'supports Valibot, Yup, and any Standard Schema validator without zod.',\n )\n }\n return _zod\n}\n\n/**\n * Lazily load `.env` via dotenv if it is installed in the consumer app.\n * `dotenv` is an **optional peer dependency** of `@forinda/kickjs` —\n * apps that want `.env` file support install it themselves (the CLI\n * generators add it to every scaffolded `package.json`). Apps that\n * inject env via the shell, Docker, or a secret manager can skip it\n * entirely and `process.env` is read as-is.\n *\n * We call `dotenv.config()` directly rather than `require('dotenv/config')`\n * so that a re-evaluation of this module (e.g. Vite SSR full-reload after\n * editing `.env`) can re-read the file. `dotenv/config` is a side-effect\n * shim that CJS caches aggressively, so the second `require` is a no-op\n * and new keys never reach `process.env` — the root cause of the\n * \"DATABASE_URL undefined until restart\" bug on Windows dev.\n */\nfunction tryLoadDotenv(): void {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const dotenv = require('dotenv')\n // `quiet: true` suppresses dotenv 17's tip banner that goes to stdout.\n // Required for CLI tools whose stdout is parsed by callers (`kick explain\n // --json`, etc) — the banner would otherwise corrupt the JSON output.\n dotenv.config({ override: false, quiet: true })\n } catch {\n // dotenv not installed — fall through, env vars must come from\n // the environment directly. This is a valid deployment style.\n }\n}\ntryLoadDotenv()\n\n/**\n * Build the base env schema. Memoized so repeated calls reuse one\n * schema instance (matters for the `cachedSchema === s` identity check\n * in {@link loadEnv}). Loads zod lazily via {@link getZod}.\n */\n// Build the concrete base schema. This function is the single source of\n// the *type* of `baseEnvSchema` (via `ReturnType<typeof buildBaseEnvSchema>`)\n// AND its runtime value (via `getBaseEnvSchema`). It is NEVER called at\n// module load — only lazily through `getBaseEnvSchema()` — so referencing\n// its return type stays compile-time-only and pulls no eager zod import.\n// Spelling the type this way (rather than `z.ZodObject<any>`) preserves the\n// precise PORT/NODE_ENV/LOG_LEVEL shape, which `defineEnv` composition and\n// the `kick typegen` `KickEnv` inference depend on.\nfunction buildBaseEnvSchema() {\n const z = getZod()\n return z.object({\n // Server\n PORT: z.coerce.number().default(3000),\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n LOG_LEVEL: z.string().default('info'),\n })\n}\n\n/** Precise type of the base env schema — no runtime value, no eager zod. */\nexport type BaseEnvSchema = ReturnType<typeof buildBaseEnvSchema>\n\n/**\n * Memoized base schema. Reuses one instance so the `cachedSchema === s`\n * identity check in {@link loadEnv} holds. Loads zod lazily.\n */\nlet _baseEnvSchema: BaseEnvSchema | undefined\nexport function getBaseEnvSchema(): BaseEnvSchema {\n if (!_baseEnvSchema) _baseEnvSchema = buildBaseEnvSchema()\n return _baseEnvSchema\n}\n\n/**\n * Base environment schema with common server variables.\n * Users extend this with their own application-specific vars.\n *\n * Back-compat lazy view over {@link getBaseEnvSchema}: constructing the\n * Proxy does NOT load zod — zod is only required the moment a property\n * (`.extend`, `.parse`, `.shape`, …) is actually accessed. This keeps\n * `import { baseEnvSchema } from '@forinda/kickjs'` zero-cost for apps\n * that don't use the Zod env path, while keeping the precise schema type.\n */\nexport const baseEnvSchema: BaseEnvSchema = new Proxy({} as BaseEnvSchema, {\n get(_t, prop) {\n const real = getBaseEnvSchema() as unknown as Record<string | symbol, unknown>\n const value = real[prop]\n // Bind methods to the real schema so zod's internal `this` works.\n return typeof value === 'function' ? (value as (...a: unknown[]) => unknown).bind(real) : value\n },\n has(_t, prop) {\n return prop in (getBaseEnvSchema() as object)\n },\n}) as BaseEnvSchema\n\n/** Cached env config to avoid re-parsing — keyed by schema reference */\nlet cachedEnv: any = null\nlet cachedSchema: any = null\n\n/**\n * Define a custom env schema. The base schema (`PORT`, `NODE_ENV`,\n * `LOG_LEVEL`) is always merged in, so users never need to remember to\n * extend it themselves — both styles below produce the same result and\n * both surface the base keys in `KickEnv`.\n *\n * Returns a Zod schema that validates `process.env`.\n *\n * @example\n * ```ts\n * // Style A — return a fresh object; base fields are merged automatically\n * export default defineEnv(() =>\n * z.object({\n * DATABASE_URL: z.string().url(),\n * JWT_SECRET: z.string().min(32),\n * })\n * )\n *\n * // Style B — explicitly extend `base` (still works, identical result)\n * export default defineEnv((base) =>\n * base.extend({\n * DATABASE_URL: z.string().url(),\n * })\n * )\n * ```\n *\n * User-defined keys override base keys when they collide, so a project\n * can re-type `PORT` or tighten `NODE_ENV` without losing the rest of\n * the base shape.\n */\nexport function defineEnv<T extends z.ZodRawShape>(\n extend: (base: typeof baseEnvSchema) => z.ZodObject<T>,\n): z.ZodObject<typeof baseEnvSchema.shape & T> {\n const base = getBaseEnvSchema()\n const userSchema = extend(base as typeof baseEnvSchema)\n // Always merge the base in. `extend` lets the user's shape override\n // any colliding base keys, which preserves the escape hatch for\n // projects that want to re-type `PORT` etc.\n return base.extend(userSchema.shape) as z.ZodObject<typeof baseEnvSchema.shape & T>\n}\n\n/**\n * Parse and validate `process.env` against a Zod schema. Caches result per schema.\n *\n * **No-arg behaviour is sticky.** The first time `loadEnv(schema)` is called\n * with an extended schema, the cache holds that schema. Subsequent\n * `loadEnv()` calls (no arg) will reuse the cached extended schema rather\n * than falling back to `baseEnvSchema`.\n *\n * Stickiness matters because `ConfigService` and the `@Value()` decorator\n * both read through the no-arg form internally. Without it, resolving\n * either of them after a `loadEnv(extendedSchema)` call would silently\n * downgrade the env to the base shape and drop every user-defined key.\n *\n * To force a re-parse with a different schema, pass the new schema explicitly.\n * To start fresh, call `resetEnvCache()` (full reset) or `reloadEnv()`\n * (re-parse against the same schema after a `.env` file change).\n *\n * **Typing:**\n * - With an explicit schema → returns `z.infer<typeof schema>`.\n * - With no schema and `KickEnv` populated by typegen → returns\n * `KickEnv` (the project-wide typed env shape).\n * - With no schema and `KickEnv` empty → returns the base `Env`.\n */\nexport function loadEnv<T extends z.ZodObject<any>>(schema: T): z.infer<T>\nexport function loadEnv(): [EnvKey] extends [never] ? Env : KickEnv\nexport function loadEnv<T extends z.ZodObject<any>>(schema?: T): any {\n // Sticky: when called with no arg, prefer the most recently cached\n // schema over re-parsing with the base schema.\n const s = schema ?? cachedSchema ?? getBaseEnvSchema()\n // Re-parse if schema changed or no cache yet\n if (cachedEnv && cachedSchema === s) return cachedEnv\n cachedSchema = s\n cachedEnv = s.parse(process.env)\n\n // Register env resolver so @Value() reads validated values\n Container._envResolver = (key: string) => cachedEnv?.[key]\n\n return cachedEnv\n}\n\n/**\n * Get a single typed environment variable value.\n *\n * Three forms:\n * - **No-arg, KickEnv populated** (`kick typegen` has run) → key is\n * constrained to known `KickEnv` keys; return type inferred from\n * the schema.\n * - **With explicit schema** → key constrained to that schema's keys;\n * return type is `z.infer<typeof schema>[K]`.\n * - **No-arg, KickEnv empty** → loose `string` key, `any` return.\n *\n * @example\n * ```ts\n * // After `kick typegen` (KickEnv populated):\n * const port = getEnv('PORT') // typed as number\n * const url = getEnv('DATABASE_URL') // typed as string\n *\n * // With an inline schema (legacy / one-off):\n * const dbUrl = getEnv('DATABASE_URL', envSchema)\n * ```\n */\nexport function getEnv<K extends EnvKey>(key: K): KickEnv[K]\nexport function getEnv<T extends z.ZodObject<any>, K extends string & keyof z.infer<T>>(\n key: K,\n schema: T,\n): z.infer<T>[K]\nexport function getEnv(key: string, schema?: z.ZodObject<any>): any {\n const env = schema ? loadEnv(schema) : (loadEnv() as Record<string, any>)\n return env[key]\n}\n\n/**\n * Reload env from `process.env`. Re-reads `.env` via dotenv (if\n * installed), then re-parses the result against the **same** schema\n * the project registered at startup. Called during HMR rebuild and by\n * `envWatchPlugin` to pick up `.env` file changes mid-session.\n *\n * Crucially, this preserves the registered schema. Wiping it would\n * silently downgrade `ConfigService` and `@Value()` back to the base\n * schema on the next read, so every user-defined key would start\n * resolving as `undefined` after the first `.env` save — which was the\n * original \"config disappears in dev\" bug.\n *\n * If you need to fully forget the schema (e.g. tests that swap schemas\n * between cases), call {@link resetEnvCache} instead.\n */\nexport function reloadEnv(): void {\n // Re-read .env file into process.env if dotenv is installed.\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require('dotenv').config({ override: true, quiet: true })\n } catch {\n // dotenv not installed — nothing to reload, env comes from the\n // environment directly.\n }\n\n // Drop the parsed snapshot but keep the schema. Re-parse eagerly so\n // existing consumers (including ConfigService getters and @Value\n // resolvers that may already hold references to the env state) see\n // the new values on their very next read instead of having to\n // re-trigger any cache-population step.\n cachedEnv = null\n if (cachedSchema) {\n if (typeof cachedSchema.parse === 'function') {\n loadEnv(cachedSchema)\n } else {\n loadEnvFromSchema(cachedSchema)\n }\n }\n}\n\n/**\n * Fully reset the env cache, including the registered schema. Use this\n * in tests that need to swap schemas between cases — `reloadEnv()`\n * deliberately preserves the schema to avoid the \"user keys vanish on\n * `.env` reload\" footgun.\n */\nexport function resetEnvCache(): void {\n cachedEnv = null\n cachedSchema = null\n}\n\n/**\n * Schema-agnostic env loader. Accepts any schema supported by\n * `@forinda/kickjs-schema` (Zod, Valibot, Yup, Joi, Standard Schema,\n * KickSchema adapters, or plain validator functions).\n *\n * Unlike `loadEnv()` which uses Zod's `.parse()` and throws a\n * `ZodError`, this function uses `detectSchema().safeParse()` and\n * throws a plain `Error` with structured issue messages on failure.\n *\n * @example\n * ```ts\n * import * as v from 'valibot'\n * import { fromValibot } from '@forinda/kickjs-schema/valibot'\n *\n * const envSchema = fromValibot(v.object({\n * PORT: v.pipe(v.string(), v.transform(Number)),\n * NODE_ENV: v.picklist(['development', 'production', 'test']),\n * }))\n *\n * export const env = loadEnvFromSchema(envSchema)\n * ```\n */\n/**\n * Single inferring signature. `TSchema` is inferred from the call site;\n * the conditional return type maps the unknown-input case (TSchema\n * resolves to `unknown` only when the argument is `unknown`-typed\n * because nothing on the value side carried a richer brand) to\n * `Record<string, unknown>` rather than letting `InferSchemaOutput`\n * fall through to `unknown`. The previous two-overload form had the\n * generic overload win for `unknown`-typed inputs (`unknown` is\n * assignable to any `TSchema`), so the documented \"Record\" fallback\n * never actually applied.\n */\nexport function loadEnvFromSchema<TSchema>(\n schema: TSchema,\n): unknown extends TSchema ? Record<string, unknown> : InferSchemaOutput<TSchema>\nexport function loadEnvFromSchema(schema: unknown): unknown {\n const wrapped = detectSchema(schema)\n const result = wrapped.safeParse(process.env)\n if (!result.success) {\n const details = result.issues\n .map((i) => ` ${i.path.join('.') || '(root)'}: ${i.message}`)\n .join('\\n')\n throw new Error(`Environment validation failed:\\n${details}`)\n }\n cachedEnv = result.data\n cachedSchema = schema\n Container._envResolver = (key: string) => cachedEnv?.[key]\n return result.data\n}\n\n/**\n * Output type of {@link baseEnvSchema}. Hand-written (rather than\n * `z.infer<typeof baseEnvSchema>`) so the type is available without the\n * eager `zod` value import — `baseEnvSchema` is now a lazy Proxy typed\n * as `z.ZodObject<any>`, from which `z.infer` could not recover the\n * precise shape anyway.\n */\nexport type Env = {\n PORT: number\n NODE_ENV: 'development' | 'production' | 'test'\n LOG_LEVEL: string\n}\n"],"mappings":";;;;;;;;;;kKAmBA,IAAI,KACJ,SAAS,QAA+B,CACtC,GAAI,KAAM,OAAO,KACjB,GAAI,CAEF,KAAA,UAAe,KAAK,CACtB,MAAQ,CACN,MAAU,MACR,gUAKF,CACF,CACA,OAAO,IACT,CAiBA,SAAS,eAAsB,CAC7B,GAAI,CAMF,UAJuB,QAIlB,CAAC,CAAC,OAAO,CAAE,SAAU,GAAO,MAAO,EAAK,CAAC,CAChD,MAAQ,CAGR,CACF,CACA,cAAc,EAed,SAAS,oBAAqB,CAC5B,IAAM,EAAI,OAAO,EACjB,OAAO,EAAE,OAAO,CAEd,KAAM,EAAE,OAAO,OAAO,CAAC,CAAC,QAAQ,GAAI,EACpC,SAAU,EAAE,KAAK,CAAC,cAAe,aAAc,MAAM,CAAC,CAAC,CAAC,QAAQ,aAAa,EAC7E,UAAW,EAAE,OAAO,CAAC,CAAC,QAAQ,MAAM,CACtC,CAAC,CACH,CASA,IAAI,eACJ,SAAgB,kBAAkC,CAEhD,MADA,CAAqB,iBAAiB,mBAAmB,EAClD,cACT,CAYA,MAAa,cAA+B,IAAI,MAAM,CAAC,EAAoB,CACzE,IAAI,GAAI,KAAM,CACZ,IAAM,KAAO,iBAAiB,EACxB,MAAQ,KAAK,MAEnB,OAAO,OAAO,OAAU,WAAc,MAAuC,KAAK,IAAI,EAAI,KAC5F,EACA,IAAI,GAAI,KAAM,CACZ,OAAO,QAAS,iBAAiB,CACnC,CACF,CAAC,EAGD,IAAI,UAAiB,KACjB,aAAoB,KAgCxB,SAAgB,UACd,OAC6C,CAC7C,IAAM,KAAO,iBAAiB,EACxB,WAAa,OAAO,IAA4B,EAItD,OAAO,KAAK,OAAO,WAAW,KAAK,CACrC,CA2BA,SAAgB,QAAoC,OAAiB,CAGnE,IAAM,EAAI,QAAU,cAAgB,iBAAiB,EASrD,OAPI,WAAa,eAAiB,EAAU,WAC5C,aAAe,EACf,UAAY,EAAE,MAAM,QAAQ,GAAG,EAG/B,UAAU,aAAgB,KAAgB,YAAY,KAE/C,UACT,CA4BA,SAAgB,OAAO,IAAa,OAAgC,CAElE,OADY,OAAS,QAAQ,MAAM,EAAK,QAAQ,EAAA,CACrC,IACb,CAiBA,SAAgB,WAAkB,CAEhC,GAAI,CAEF,UAAQ,QAAQ,CAAC,CAAC,OAAO,CAAE,SAAU,GAAM,MAAO,EAAK,CAAC,CAC1D,MAAQ,CAGR,CAOA,UAAY,KACR,eACE,OAAO,aAAa,OAAU,WAChC,QAAQ,YAAY,EAEpB,kBAAkB,YAAY,EAGpC,CAQA,SAAgB,eAAsB,CACpC,UAAY,KACZ,aAAe,IACjB,CAsCA,SAAgB,kBAAkB,OAA0B,CAE1D,IAAM,OADU,aAAa,MACR,CAAC,CAAC,UAAU,QAAQ,GAAG,EAC5C,GAAI,CAAC,OAAO,QAAS,CACnB,IAAM,QAAU,OAAO,OACpB,IAAK,GAAM,KAAK,EAAE,KAAK,KAAK,GAAG,GAAK,SAAS,IAAI,EAAE,SAAS,CAAC,CAC7D,KAAK;CAAI,EACZ,MAAU,MAAM,mCAAmC,SAAS,CAC9D,CAIA,MAHA,WAAY,OAAO,KACnB,aAAe,OACf,UAAU,aAAgB,KAAgB,YAAY,KAC/C,OAAO,IAChB"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @forinda/kickjs v6.3.0
3
+ *
4
+ * Copyright (c) Felix Orinda
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ import{i as requestStore,t as RequestContext}from"./context-Cmnmn68a.mjs";import{a as disposeRequestStore,i as createRequestStore,t as applyHandlerResult}from"./reply-DI4EqF0R.mjs";import{validate}from"./http/middleware/validate.mjs";import{t as applyUploadConfig}from"./upload-config-D8gsl_ox.mjs";var WebRequestShim=class{method;url;headers;body;params={};query={};requestId;file;files;signal;constructor(request,url){this.method=request.method,this.url=url.pathname+url.search,this.signal=request.signal;let headers={};request.headers.forEach((value,key)=>{headers[key]=value}),this.headers=headers;let query={};for(let key of url.searchParams.keys()){let all=url.searchParams.getAll(key);query[key]=all.length>1?all:all[0]}this.query=query}once(event,listener){return event===`close`&&this.signal.addEventListener(`abort`,listener,{once:!0}),this}on(event,listener){return this.once(event,listener)}};const encoder=new TextEncoder;function toBytes(chunk){return chunk instanceof Uint8Array?chunk:encoder.encode(String(chunk))}var WebResponseDriver=class{signal;ready;settled=!1;_status=200;headers=new Headers;_headersSent=!1;writer;resolveReady;constructor(signal){this.signal=signal,this.ready=new Promise(resolve=>{this.resolveReady=resolve})}status(code){return this._status=code,this}json(data){return this.headers.has(`content-type`)||this.headers.set(`content-type`,`application/json; charset=utf-8`),this.finish(JSON.stringify(data))}send(data){return typeof data==`object`&&data&&!(data instanceof Uint8Array)?this.json(data):this.finish(data)}type(contentType){return this.headers.set(`content-type`,contentType),this}setHeader(name,value){if(Array.isArray(value)){this.headers.delete(name);for(let v of value)this.headers.append(name,String(v))}else this.headers.set(name,String(value));return this}render(){throw Error(`ctx.render() is not supported on the web runtime (no view engine).`)}writeHead(statusCode,headers){if(this._status=statusCode,headers)for(let[name,value]of Object.entries(headers))this.setHeader(name,value);return this.startStream(),this}flushHeaders(){return this.startStream(),this}write(chunk){return this.startStream(),this.writer.write(toBytes(chunk)).catch(()=>{}),!0}end(data){return this.writer?(data!==void 0&&this.write(data),this.writer.close().catch(()=>{}),this.writer=void 0,this):this.finish(data)}once(event,listener){return event===`close`&&this.signal&&this.signal.addEventListener(`abort`,listener,{once:!0}),this}get headersSent(){return this._headersSent}finish(body){if(this.settled)return this;this.settled=!0,this._headersSent=!0;let bodyAllowed=this._status!==204&&this._status!==304;return this.resolveReady(new Response(bodyAllowed?body??null:null,{status:this._status,headers:this.headers})),this}startStream(){if(this.writer||this.settled)return;let{readable,writable}=new TransformStream;this.writer=writable.getWriter(),this.settled=!0,this._headersSent=!0,this.signal?.addEventListener(`abort`,()=>{this.writer?.abort().catch(()=>{}),this.writer=void 0},{once:!0}),this.resolveReady(new Response(readable,{status:this._status,headers:this.headers}))}};const NOOP_NEXT=()=>{},BODY_METHODS=new Set([`POST`,`PUT`,`PATCH`,`DELETE`]);function defaultErrorResponse(err,driver){let status=typeof err?.status==`number`?err.status:typeof err?.statusCode==`number`?err.statusCode:500,message=status>=500?`Internal Server Error`:err?.message??`Request failed`;driver.status(status).json({error:message})}function compileWebRoute(entry,hooks){let validator=entry.meta.validation?validate(entry.meta.validation):void 0,upload=entry.meta.upload&&entry.meta.upload.mode!==`none`?entry.meta.upload:null;return async({request,url,params})=>{let req=new WebRequestShim(request,url);if(req.params=params,upload){let form=await request.formData().catch(()=>void 0);if(form){let rawParts=[],fields={};for(let[name,value]of form.entries())typeof value==`string`?fields[name]=value:rawParts.push({fieldname:name,filename:value.name??``,mimetype:value.type||`application/octet-stream`,buffer:new Uint8Array(await value.arrayBuffer())});let{file,files}=applyUploadConfig(rawParts,entry.meta.upload);req.file=file,req.files=files,req.body=fields}}else if(BODY_METHODS.has(request.method)){let contentType=req.headers[`content-type`]??``;if(contentType.includes(`application/json`))req.body=await request.json().catch(()=>void 0);else if(contentType.includes(`application/x-www-form-urlencoded`)){let text=await request.text().catch(()=>``);req.body=Object.fromEntries(new URLSearchParams(text))}else contentType.startsWith(`text/`)&&(req.body=await request.text().catch(()=>void 0))}let store=createRequestStore(req.headers[`x-request-id`]);req.requestId=store.requestId;let driver=new WebResponseDriver(request.signal);driver.setHeader(`x-request-id`,store.requestId);let pipeline=requestStore.run(store,async()=>{let ctx=new RequestContext(req,driver,NOOP_NEXT,driver);validator&&await new Promise((resolve,reject)=>{validator(req,void 0,err=>err?reject(err):resolve())});for(let mw of entry.middlewares){let advanced=!1;if(await new Promise((resolve,reject)=>{Promise.resolve(mw(ctx,err=>{advanced=!0,err?reject(err):resolve()})).catch(reject)}),!advanced||driver.settled)return}if(entry.contributorRunner&&await entry.contributorRunner(ctx),driver.settled)return;let result=await entry.handler(ctx);driver.settled||applyHandlerResult(ctx,result)});try{try{await Promise.race([pipeline,driver.ready])}catch(err){driver.settled||(hooks?.onError&&await hooks.onError(err,req,driver),driver.settled||defaultErrorResponse(err,driver))}return driver.settled||driver.status(404).json({error:`Not Found`}),await driver.ready}finally{Promise.resolve(pipeline).catch(()=>{}).then(()=>disposeRequestStore(store))}}}export{WebRequestShim as n,WebResponseDriver as r,compileWebRoute as t};
12
+ //# sourceMappingURL=handler-db1-nMd6.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-db1-nMd6.mjs","names":[],"sources":["../src/http/web/driver.ts","../src/http/web/handler.ts"],"sourcesContent":["// Web-standard driver pair — the request/response bridge between web\n// primitives (WHATWG Request/Response) and the KickJS RequestContext surface.\n//\n// Deliberately imports NOTHING from node: this module is the edge-safe core\n// shared by the h3 v2 runtime (`runtimes/h3-web.ts`, node bootstrap) and the\n// `@forinda/kickjs/web` fetch entry (edge/Bun/Deno). See\n// `web-standards-edge-design.md` §3.1.\n\nimport type { RuntimeResponse } from '../runtime'\n\n/**\n * Presents a web `Request` as the express-shaped `req` object that\n * `RequestContext` (and route validation) reads: plain-object `headers`,\n * assignable `body`/`params`/`query`, and `on/once('close')` bridged from\n * the request's `AbortSignal` — the only EventEmitter surface the context\n * layer uses (`ctx.signal`, SSE close).\n */\nexport class WebRequestShim {\n readonly method: string\n readonly url: string\n /** Lower-cased plain header object (RequestContext indexes it directly). */\n readonly headers: Record<string, string>\n body: unknown\n params: Record<string, string> = {}\n query: Record<string, unknown> = {}\n requestId?: string\n file?: unknown\n files?: unknown;\n // Ad-hoc props middleware may set (session, user, ...) — index signature\n // keeps assignment open like a node req object.\n [key: string]: unknown\n\n private readonly signal: AbortSignal\n\n constructor(request: Request, url: URL) {\n this.method = request.method\n // Express-shaped `req.url` is path + search, not the absolute URL.\n this.url = url.pathname + url.search\n this.signal = request.signal\n const headers: Record<string, string> = {}\n request.headers.forEach((value, key) => {\n headers[key] = value\n })\n this.headers = headers\n const query: Record<string, unknown> = {}\n for (const key of url.searchParams.keys()) {\n const all = url.searchParams.getAll(key)\n query[key] = all.length > 1 ? all : all[0]\n }\n this.query = query\n }\n\n /** `req.once('close')` — abort of the underlying request signal. */\n once(event: string, listener: () => void): this {\n if (event === 'close') this.signal.addEventListener('abort', listener, { once: true })\n return this\n }\n\n /** `req.on('close')` — same bridge; signals only abort once anyway. */\n on(event: string, listener: () => void): this {\n return this.once(event, listener)\n }\n}\n\nconst encoder = new TextEncoder()\n\nfunction toBytes(chunk: unknown): Uint8Array {\n if (chunk instanceof Uint8Array) return chunk\n return encoder.encode(String(chunk))\n}\n\n/**\n * `RuntimeResponse` over a web `Response`.\n *\n * Two modes:\n * - **Buffered** (default): `status/json/send/type/setHeader` record state;\n * the terminal call resolves {@link ready} with a complete `Response`.\n * - **Streaming**: the first `writeHead`/`flushHeaders`/`write` call switches\n * to a `TransformStream` and resolves {@link ready} IMMEDIATELY with a\n * streamed `Response` — SSE works on edge this way; `write` feeds the\n * writer, `end` closes it.\n *\n * A class so the method table lives on a shared prototype (one allocation\n * per request — same rationale as the Fastify/h3 driver classes).\n */\nexport class WebResponseDriver implements RuntimeResponse {\n /** Resolves with the final (or streamed) Response. */\n readonly ready: Promise<Response>\n /** True once a terminal/streaming action produced the Response. */\n settled = false\n\n private _status = 200\n private readonly headers = new Headers()\n private _headersSent = false\n private writer?: WritableStreamDefaultWriter<Uint8Array>\n private resolveReady!: (response: Response) => void\n\n constructor(private readonly signal?: AbortSignal) {\n this.ready = new Promise<Response>((resolve) => {\n this.resolveReady = resolve\n })\n }\n\n status(code: number): this {\n this._status = code\n return this\n }\n\n json(data: unknown): this {\n if (!this.headers.has('content-type'))\n this.headers.set('content-type', 'application/json; charset=utf-8')\n return this.finish(JSON.stringify(data))\n }\n\n send(data: unknown): this {\n if (\n data !== undefined &&\n data !== null &&\n typeof data === 'object' &&\n !(data instanceof Uint8Array)\n ) {\n return this.json(data)\n }\n return this.finish(data as string | Uint8Array | null | undefined)\n }\n\n type(contentType: string): this {\n this.headers.set('content-type', contentType)\n return this\n }\n\n setHeader(name: string, value: unknown): this {\n if (Array.isArray(value)) {\n this.headers.delete(name)\n for (const v of value) this.headers.append(name, String(v))\n } else {\n this.headers.set(name, String(value))\n }\n return this\n }\n\n render(): never {\n throw new Error('ctx.render() is not supported on the web runtime (no view engine).')\n }\n\n writeHead(statusCode: number, headers?: Record<string, string | number | string[]>): this {\n this._status = statusCode\n if (headers) {\n for (const [name, value] of Object.entries(headers)) this.setHeader(name, value)\n }\n this.startStream()\n return this\n }\n\n flushHeaders(): this {\n this.startStream()\n return this\n }\n\n write(chunk: unknown): boolean {\n this.startStream()\n // Fire-and-forget: backpressure is absorbed by the stream's queue. Write\n // failures (client gone) surface via the abort listener, not here.\n void this.writer!.write(toBytes(chunk)).catch(() => {})\n return true\n }\n\n end(data?: unknown): this {\n if (this.writer) {\n if (data !== undefined) this.write(data)\n void this.writer.close().catch(() => {})\n this.writer = undefined\n return this\n }\n return this.finish(data as string | Uint8Array | null | undefined)\n }\n\n once(event: string, listener: (...args: unknown[]) => void): this {\n if (event === 'close' && this.signal)\n this.signal.addEventListener('abort', listener as () => void, { once: true })\n return this\n }\n\n get headersSent(): boolean {\n return this._headersSent\n }\n\n /** Buffered terminal: build the final Response exactly once. */\n private finish(body?: string | Uint8Array | null): this {\n if (this.settled) return this\n this.settled = true\n this._headersSent = true\n // 204/304 must not carry a body per the Response constructor contract.\n const bodyAllowed = this._status !== 204 && this._status !== 304\n this.resolveReady(\n new Response(bodyAllowed ? (body ?? null) : null, {\n status: this._status,\n headers: this.headers,\n }),\n )\n return this\n }\n\n /** Switch to streaming mode and resolve `ready` with the streamed Response. */\n private startStream(): void {\n if (this.writer || this.settled) return\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()\n this.writer = writable.getWriter()\n this.settled = true\n this._headersSent = true\n // Abort from the client tears the stream down so the writer stops queueing.\n this.signal?.addEventListener(\n 'abort',\n () => {\n void this.writer?.abort().catch(() => {})\n this.writer = undefined\n },\n { once: true },\n )\n this.resolveReady(new Response(readable, { status: this._status, headers: this.headers }))\n }\n}\n","// Engine-neutral web pipeline: run one KickJS RouteEntry against a WHATWG\n// Request and produce a WHATWG Response. Shared by the h3 v2 runtime\n// (node bootstrap) and the `@forinda/kickjs/web` fetch entry (edge/Bun/Deno).\n// Edge-safe: no node imports beyond the sanctioned ALS request store.\n\nimport { RequestContext } from '../context'\nimport { applyHandlerResult } from '../reply'\nimport { requestStore } from '../request-store'\nimport { createRequestStore, disposeRequestStore } from '../middleware/request-scope'\nimport { validate } from '../middleware/validate'\nimport { applyUploadConfig, type RawUploadPart } from '../middleware/upload-config'\nimport type { RouteEntry, RuntimeResponse } from '../runtime'\nimport { WebRequestShim, WebResponseDriver } from './driver'\n\nconst NOOP_NEXT = (): void => {}\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nexport interface WebRouteInvocation {\n request: Request\n url: URL\n params: Record<string, string>\n}\n\nexport interface WebRouteHooks {\n /**\n * Error bridge — receives pipeline rejections (validation failures,\n * handler throws) with the shim + driver so an engine can dispatch the\n * framework's connect-style error handler. When absent (or when it\n * doesn't settle the driver), a minimal JSON error is produced from\n * `err.status`/`err.statusCode` (500 default).\n */\n onError?: (err: unknown, req: WebRequestShim, res: WebResponseDriver) => void | Promise<void>\n}\n\n/** Fallback error shape when no engine error handler settles the response. */\nfunction defaultErrorResponse(err: unknown, driver: WebResponseDriver): void {\n const status =\n typeof (err as { status?: number })?.status === 'number'\n ? (err as { status: number }).status\n : typeof (err as { statusCode?: number })?.statusCode === 'number'\n ? (err as { statusCode: number }).statusCode\n : 500\n const message =\n status >= 500 ? 'Internal Server Error' : ((err as Error)?.message ?? 'Request failed')\n driver.status(status).json({ error: message })\n}\n\n/**\n * Compile a RouteEntry into a `(invocation) => Promise<Response>` function.\n * Everything derivable at route-build time (validator, upload config) is\n * hoisted here — per-request work is shim + store + pipeline only.\n */\nexport function compileWebRoute(\n entry: RouteEntry,\n hooks?: WebRouteHooks,\n): (invocation: WebRouteInvocation) => Promise<Response> {\n const validator = entry.meta.validation ? validate(entry.meta.validation) : undefined\n const upload = entry.meta.upload && entry.meta.upload.mode !== 'none' ? entry.meta.upload : null\n\n return async ({ request, url, params }) => {\n const req = new WebRequestShim(request, url)\n req.params = params\n\n // Body: uploads consume the stream as FormData; everything else parses\n // by content-type. A failed parse leaves body undefined (validation\n // rejects it downstream if the route demands a shape).\n if (upload) {\n const form = await request.formData().catch(() => undefined)\n if (form) {\n const rawParts: RawUploadPart[] = []\n const fields: Record<string, unknown> = {}\n for (const [name, value] of form.entries()) {\n if (typeof value === 'string') {\n fields[name] = value\n } else {\n rawParts.push({\n fieldname: name,\n filename: value.name ?? '',\n mimetype: value.type || 'application/octet-stream',\n buffer: new Uint8Array(await value.arrayBuffer()) as unknown as Buffer,\n })\n }\n }\n const { file, files } = applyUploadConfig(rawParts, entry.meta.upload!)\n req.file = file\n req.files = files\n req.body = fields\n }\n } else if (BODY_METHODS.has(request.method)) {\n const contentType = req.headers['content-type'] ?? ''\n if (contentType.includes('application/json')) {\n req.body = await request.json().catch(() => undefined)\n } else if (contentType.includes('application/x-www-form-urlencoded')) {\n const text = await request.text().catch(() => '')\n req.body = Object.fromEntries(new URLSearchParams(text))\n } else if (contentType.startsWith('text/')) {\n req.body = await request.text().catch(() => undefined)\n }\n }\n\n const store = createRequestStore(req.headers['x-request-id'])\n req.requestId = store.requestId\n\n const driver = new WebResponseDriver(request.signal)\n driver.setHeader('x-request-id', store.requestId)\n\n const pipeline = requestStore.run(store, async () => {\n const ctx = new RequestContext(\n req as never,\n driver as never,\n NOOP_NEXT,\n driver as RuntimeResponse,\n )\n\n if (validator) {\n await new Promise<void>((resolve, reject) => {\n validator(req as never, undefined as never, (err?: unknown) =>\n err ? reject(err) : resolve(),\n )\n })\n }\n\n for (const mw of entry.middlewares) {\n let advanced = false\n await new Promise<void>((resolve, reject) => {\n const next = (err?: unknown): void => {\n advanced = true\n if (err) reject(err)\n else resolve()\n }\n Promise.resolve(mw(ctx, next)).catch(reject)\n })\n if (!advanced || driver.settled) return\n }\n\n if (entry.contributorRunner) await entry.contributorRunner(ctx)\n if (driver.settled) return\n const result = await entry.handler(ctx)\n // Return-value handlers (reply.ts): auto-send when nothing was written.\n // `undefined` falls through to the canonical 404 below.\n if (!driver.settled) applyHandlerResult(ctx, result)\n })\n\n try {\n // Streaming responses resolve `ready` mid-pipeline (SSE); buffered ones\n // resolve at the terminal ctx call. Race so both shapes return promptly.\n try {\n await Promise.race([pipeline, driver.ready])\n } catch (err) {\n if (!driver.settled) {\n if (hooks?.onError) await hooks.onError(err, req, driver)\n if (!driver.settled) defaultErrorResponse(err, driver)\n }\n }\n if (!driver.settled) {\n // Pipeline finished without responding — canonical 404 shape.\n driver.status(404).json({ error: 'Not Found' })\n }\n return await driver.ready\n } finally {\n // @PreDestroy teardown. For buffered responses this runs post-response;\n // for streams it runs when the pipeline (not the stream) completes —\n // stream lifetime is governed by the client abort signal.\n void Promise.resolve(pipeline)\n .catch(() => {})\n .then(() => disposeRequestStore(store))\n }\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-CFenLjbY.mjs";
2
+ import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-C_KlrNoH.mjs";
3
3
  export { Application, ApplicationOptions, MiddlewareEntry };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.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-KaKi6jgi.mjs";export{Application};
11
+ import{t as Application}from"../application-CHYTI1Q_.mjs";export{Application};
@@ -1,3 +1,3 @@
1
1
 
2
- import { t as bootstrap } from "../bootstrap-DodgsiDG.mjs";
2
+ import { t as bootstrap } from "../bootstrap-BgffgFNX.mjs";
3
3
  export { bootstrap };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.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-Dj96_O6j.mjs";export{bootstrap};
11
+ import{t as bootstrap}from"../bootstrap-sB0dlLu7.mjs";export{bootstrap};
@@ -1,4 +1,4 @@
1
1
 
2
- import { g as ContextMeta } from "../app-module-CuAS6hJC.mjs";
3
- import { C as Ctx, E as RouteShape, T as RequestContext, w as DeepReadonly } from "../adapter-D_uzU-QA.mjs";
4
- export { type ContextMeta, Ctx, DeepReadonly, RequestContext, RouteShape };
2
+ import { g as ContextMeta } from "../app-module-BwYtACD6.mjs";
3
+ import { C as Ctx, D as SseHandler, E as RouteShape, T as RequestContext, w as DeepReadonly } from "../adapter-stSpsUHs.mjs";
4
+ export { type ContextMeta, Ctx, DeepReadonly, RequestContext, RouteShape, SseHandler };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.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-Co72dNA_.mjs";export{RequestContext};
11
+ import{t as RequestContext}from"../context-Cmnmn68a.mjs";export{RequestContext};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,4 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{randomBytes}from"node:crypto";function csrf(options={}){let cookieName=options.cookie??`_csrf`,headerName=options.header??`x-csrf-token`,protectedMethods=new Set((options.methods??[`POST`,`PUT`,`PATCH`,`DELETE`]).map(m=>m.toUpperCase())),ignorePaths=new Set(options.ignorePaths??[]),tokenLength=options.tokenLength??32,cookieOpts={httpOnly:!0,sameSite:`strict`,secure:process.env.NODE_ENV===`production`,path:`/`,...options.cookieOptions};return(req,res,next)=>{let token=(req.cookies||{})[cookieName];if(token||(token=randomBytes(tokenLength).toString(`hex`),res.cookie(cookieName,token,cookieOpts)),!protectedMethods.has(req.method.toUpperCase())||ignorePaths.has(req.path))return next();let headerToken=req.headers[headerName];if(!headerToken||headerToken!==token)return res.status(403).json({message:`CSRF token mismatch`});next()}}export{csrf};
12
- //# sourceMappingURL=csrf.mjs.map
11
+ import{t as csrf}from"../../csrf-CbB5oAPn.mjs";export{csrf};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.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-kmJ4y4fJ.mjs";import{HttpException,ProblemException,normalizeProblem}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 instanceof ProblemException){if(err.problem.status>=500&&log.error(err,err.message),err.headers)for(let[k,v]of Object.entries(err.headers))res.setHeader(k,v);let body=normalizeProblem(err.problem);return res.setHeader(`Content-Type`,`application/problem+json`),res.status(body.status).json(body)}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-3S12kJ7Q.mjs";import{HttpException,ProblemException,normalizeProblem}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 instanceof ProblemException){if(err.problem.status>=500&&log.error(err,err.message),err.headers)for(let[k,v]of Object.entries(err.headers))res.setHeader(k,v);let body=normalizeProblem(err.problem);return res.setHeader(`Content-Type`,`application/problem+json`),res.status(body.status).json(body)}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,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.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{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};
11
+ import{r as registerDisposable}from"../../disposables-BBTIbZkX.mjs";var MemoryStore=class{windowMs;hits=new Map;cleanupTimer;constructor(windowMs){this.windowMs=windowMs,this.cleanupTimer=setInterval(()=>this.cleanup(),windowMs),this.cleanupTimer.unref&&this.cleanupTimer.unref(),registerDisposable(()=>this.dispose())}dispose(){clearInterval(this.cleanupTimer),this.hits.clear()}async increment(key){let now=Date.now(),entry=this.hits.get(key);if(entry&&entry.resetTime.getTime()>now)return entry.totalHits++,{totalHits:entry.totalHits,resetTime:entry.resetTime};let resetTime=new Date(now+this.windowMs),newEntry={totalHits:1,resetTime};return this.hits.set(key,newEntry),{totalHits:1,resetTime}}async decrement(key){let entry=this.hits.get(key);entry&&entry.totalHits>0&&entry.totalHits--}async reset(key){this.hits.delete(key)}cleanup(){let now=Date.now();for(let[key,entry]of this.hits)entry.resetTime.getTime()<=now&&this.hits.delete(key)}};function rateLimit(options={}){let max=options.max??100,windowMs=options.windowMs??6e4,message=options.message??`Too Many Requests`,statusCode=options.statusCode??429,keyGenerator=options.keyGenerator??(req=>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,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,CAAC,CAAC,KAAK,CAAE,OAAQ,CAAC,EAGhD,KAAK,CACP,CACF"}
1
+ {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../../src/http/middleware/rate-limit.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nimport { registerDisposable } from '../../core/disposables'\n\nexport interface RateLimitStore {\n increment(key: string): Promise<{ totalHits: number; resetTime: Date }>\n decrement(key: string): Promise<void>\n reset(key: string): Promise<void>\n}\n\nexport interface RateLimitOptions {\n /** Maximum number of requests per window (default: 100) */\n max?: number\n /** Time window in milliseconds (default: 60_000) */\n windowMs?: number\n /** Response message when rate limit is exceeded (default: 'Too Many Requests') */\n message?: string\n /** HTTP status code when rate limit is exceeded (default: 429) */\n statusCode?: number\n /** Function to generate the key for rate limiting (default: req.ip) */\n keyGenerator?: (req: Request) => string\n /** Whether to send rate limit headers (default: true) */\n headers?: boolean\n /** Custom store implementation (default: in-memory Map) */\n store?: RateLimitStore\n /** Function to skip rate limiting for certain requests */\n skip?: (req: Request) => boolean\n /** Paths to exclude from rate limiting */\n skipPaths?: string[]\n}\n\ninterface MemoryStoreEntry {\n totalHits: number\n resetTime: Date\n}\n\nclass MemoryStore implements RateLimitStore {\n private hits = new Map<string, MemoryStoreEntry>()\n private cleanupTimer: ReturnType<typeof setInterval>\n\n constructor(private windowMs: number) {\n this.cleanupTimer = setInterval(() => this.cleanup(), windowMs)\n // Allow the process to exit without waiting for the timer\n if (this.cleanupTimer.unref) {\n this.cleanupTimer.unref()\n }\n // Symmetric teardown: Application.shutdown() drains the disposables\n // registry — without this, every in-process app re-create (tests, HMR)\n // leaked one live interval + its hits Map.\n registerDisposable(() => this.dispose())\n }\n\n /** Clear the cleanup interval and drop all hit counters. */\n dispose(): void {\n clearInterval(this.cleanupTimer)\n this.hits.clear()\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n\n if (entry && entry.resetTime.getTime() > now) {\n entry.totalHits++\n return { totalHits: entry.totalHits, resetTime: entry.resetTime }\n }\n\n const resetTime = new Date(now + this.windowMs)\n const newEntry: MemoryStoreEntry = { totalHits: 1, resetTime }\n this.hits.set(key, newEntry)\n return { totalHits: 1, resetTime }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.totalHits > 0) {\n entry.totalHits--\n }\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n\n private cleanup(): void {\n const now = Date.now()\n for (const [key, entry] of this.hits) {\n if (entry.resetTime.getTime() <= now) {\n this.hits.delete(key)\n }\n }\n }\n}\n\n/**\n * Rate limiting middleware.\n *\n * Limits the number of requests a client can make within a time window.\n * Uses an in-memory store by default, but accepts a custom store for\n * distributed deployments (e.g. Redis).\n *\n * @example\n * ```ts\n * import { rateLimit } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * rateLimit({ max: 100, windowMs: 60_000 }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function rateLimit(options: RateLimitOptions = {}) {\n const max = options.max ?? 100\n const windowMs = options.windowMs ?? 60_000\n const message = options.message ?? 'Too Many Requests'\n const statusCode = options.statusCode ?? 429\n 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":";;;;;;;;;;oEAoCA,IAAM,YAAN,KAA4C,CAItB,SAHpB,KAAe,IAAI,IACnB,aAEA,YAAY,SAA0B,CAAlB,KAAA,SAAA,SAClB,KAAK,aAAe,gBAAkB,KAAK,QAAQ,EAAG,QAAQ,EAE1D,KAAK,aAAa,OACpB,KAAK,aAAa,MAAM,EAK1B,uBAAyB,KAAK,QAAQ,CAAC,CACzC,CAGA,SAAgB,CACd,cAAc,KAAK,YAAY,EAC/B,KAAK,KAAK,MAAM,CAClB,CAEA,MAAM,UAAU,IAA8D,CAC5E,IAAM,IAAM,KAAK,IAAI,EACf,MAAQ,KAAK,KAAK,IAAI,GAAG,EAE/B,GAAI,OAAS,MAAM,UAAU,QAAQ,EAAI,IAEvC,MADA,OAAM,YACC,CAAE,UAAW,MAAM,UAAW,UAAW,MAAM,SAAU,EAGlE,IAAM,UAAY,IAAI,KAAK,IAAM,KAAK,QAAQ,EACxC,SAA6B,CAAE,UAAW,EAAG,SAAU,EAE7D,OADA,KAAK,KAAK,IAAI,IAAK,QAAQ,EACpB,CAAE,UAAW,EAAG,SAAU,CACnC,CAEA,MAAM,UAAU,IAA4B,CAC1C,IAAM,MAAQ,KAAK,KAAK,IAAI,GAAG,EAC3B,OAAS,MAAM,UAAY,GAC7B,MAAM,WAEV,CAEA,MAAM,MAAM,IAA4B,CACtC,KAAK,KAAK,OAAO,GAAG,CACtB,CAEA,SAAwB,CACtB,IAAM,IAAM,KAAK,IAAI,EACrB,IAAK,GAAM,CAAC,IAAK,SAAU,KAAK,KAC1B,MAAM,UAAU,QAAQ,GAAK,KAC/B,KAAK,KAAK,OAAO,GAAG,CAG1B,CACF,EAsBA,SAAgB,UAAU,QAA4B,CAAC,EAAG,CACxD,IAAM,IAAM,QAAQ,KAAO,IACrB,SAAW,QAAQ,UAAY,IAC/B,QAAU,QAAQ,SAAW,oBAC7B,WAAa,QAAQ,YAAc,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,CAAC,CAAC,KAAK,CAAE,OAAQ,CAAC,EAGhD,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.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{randomUUID}from"node:crypto";const REQUEST_ID_HEADER=`x-request-id`;function requestId(){return(req,res,next)=>{let id=req.requestId||req.headers[`x-request-id`]||randomUUID();req.requestId=id,res.setHeader(REQUEST_ID_HEADER,id),next()}}export{REQUEST_ID_HEADER,requestId};
11
+ const REQUEST_ID_HEADER=`x-request-id`;function requestId(){return(req,res,next)=>{let id=req.requestId||req.headers[`x-request-id`]||crypto.randomUUID();req.requestId=id,res.setHeader(REQUEST_ID_HEADER,id),next()}}export{REQUEST_ID_HEADER,requestId};
12
12
  //# sourceMappingURL=request-id.mjs.map
@@ -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,WAAW,EAC/E,IAAa,UAAY,GAC1B,IAAI,UAAU,kBAAmB,EAAE,EACnC,KAAK,CACP,CACF"}
1
+ {"version":3,"file":"request-id.mjs","names":[],"sources":["../../../src/http/middleware/request-id.ts"],"sourcesContent":["import 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 // globalThis.crypto (Web Crypto) — portable across node/bun/deno/workers,\n // unlike node:crypto. Node ≥ 19 exposes it globally; engines pin ≥ 20.\n const id = existing || (req.headers[REQUEST_ID_HEADER] as string) || crypto.randomUUID()\n ;(req as any).requestId = id\n res.setHeader(REQUEST_ID_HEADER, id)\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAEA,MAAa,kBAAoB,eAYjC,SAAgB,WAAY,CAC1B,OAAQ,IAAc,IAAe,OAAuB,CAI1D,IAAM,GAHY,IAAyC,WAGnC,IAAI,QAAA,iBAAyC,OAAO,WAAW,EACtF,IAAa,UAAY,GAC1B,IAAI,UAAU,kBAAmB,EAAE,EACnC,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.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-kmJ4y4fJ.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-3S12kJ7Q.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,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.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{createHmac,randomUUID,timingSafeEqual}from"node:crypto";var MemoryStore=class{sessions=new Map;cleanupInterval;constructor(){this.cleanupInterval=setInterval(()=>this.purge(),6e4),this.cleanupInterval.unref()}async get(sid){let entry=this.sessions.get(sid);return entry?Date.now()>entry.expires?(this.sessions.delete(sid),null):entry.data:null}async set(sid,data,maxAge){this.sessions.set(sid,{data,expires:Date.now()+maxAge})}async destroy(sid){this.sessions.delete(sid)}async touch(sid,maxAge){let entry=this.sessions.get(sid);entry&&(entry.expires=Date.now()+maxAge)}purge(){let now=Date.now();for(let[sid,entry]of this.sessions)now>entry.expires&&this.sessions.delete(sid)}};function parseCookieHeader(header){let out={};for(let pair of header.split(`;`)){let eq=pair.indexOf(`=`);if(eq===-1)continue;let key=pair.slice(0,eq).trim();if(!key)continue;let value=pair.slice(eq+1).trim();value.startsWith(`"`)&&value.endsWith(`"`)&&(value=value.slice(1,-1));try{out[key]=decodeURIComponent(value)}catch{out[key]=value}}return out}function sign(value,secret){return`s:${value}.${createHmac(`sha256`,secret).update(value).digest(`base64url`)}`}function unsign(signed,secret){if(!signed.startsWith(`s:`))return!1;let raw=signed.slice(2),dotIndex=raw.lastIndexOf(`.`);if(dotIndex===-1)return!1;let value=raw.slice(0,dotIndex),providedSig=raw.slice(dotIndex+1),expectedSig=createHmac(`sha256`,secret).update(value).digest(`base64url`),a=Buffer.from(providedSig),b=Buffer.from(expectedSig);return a.length!==b.length||!timingSafeEqual(a,b)?!1:value}function session(options){let{secret,cookieName=`kick.sid`,maxAge=864e5,rolling=!1,saveUninitialized=!0,store=new MemoryStore,cookie:cookieOpts={}}=options,cookieDefaults={httpOnly:cookieOpts.httpOnly??!0,secure:cookieOpts.secure??process.env.NODE_ENV===`production`,sameSite:cookieOpts.sameSite??`lax`,path:cookieOpts.path??`/`,...cookieOpts.domain?{domain:cookieOpts.domain}:{},maxAge};return async(req,res,next)=>{let cookies=req.cookies;if(!cookies){let header=req.headers.cookie;cookies=header?parseCookieHeader(header):{}}let signedCookie=cookies[cookieName],sid=!1,sessionData=null,isNew=!1;signedCookie&&(sid=unsign(signedCookie,secret),sid&&(sessionData=await store.get(sid))),(!sid||!sessionData)&&(sid=randomUUID(),sessionData={},isNew=!0);let currentSid=sid,currentData={...sessionData},destroyed=!1,sessionObj={get id(){return currentSid},data:currentData,async regenerate(){await store.destroy(currentSid),currentSid=randomUUID(),currentData={},sessionObj.data=currentData,await store.set(currentSid,currentData,maxAge),res.cookie(cookieName,sign(currentSid,secret),cookieDefaults)},async destroy(){await store.destroy(currentSid),destroyed=!0,currentData={},sessionObj.data=currentData,res.clearCookie(cookieName,{path:cookieDefaults.path})},async save(){destroyed||await store.set(currentSid,currentData,maxAge)}};req.session=sessionObj,isNew&&res.cookie(cookieName,sign(currentSid,secret),cookieDefaults),res.on(`finish`,async()=>{destroyed||isNew&&!saveUninitialized&&Object.keys(currentData).length===0||(await store.set(currentSid,currentData,maxAge),rolling&&!isNew&&store.touch&&await store.touch(currentSid,maxAge))}),rolling&&!isNew&&res.cookie(cookieName,sign(currentSid,secret),cookieDefaults),next()}}export{session};
11
+ import{r as registerDisposable}from"../../disposables-BBTIbZkX.mjs";import{createHmac,randomUUID,timingSafeEqual}from"node:crypto";var MemoryStore=class{sessions=new Map;cleanupInterval;constructor(){this.cleanupInterval=setInterval(()=>this.purge(),6e4),this.cleanupInterval.unref(),registerDisposable(()=>this.dispose())}dispose(){clearInterval(this.cleanupInterval),this.sessions.clear()}async get(sid){let entry=this.sessions.get(sid);return entry?Date.now()>entry.expires?(this.sessions.delete(sid),null):entry.data:null}async set(sid,data,maxAge){this.sessions.set(sid,{data,expires:Date.now()+maxAge})}async destroy(sid){this.sessions.delete(sid)}async touch(sid,maxAge){let entry=this.sessions.get(sid);entry&&(entry.expires=Date.now()+maxAge)}purge(){let now=Date.now();for(let[sid,entry]of this.sessions)now>entry.expires&&this.sessions.delete(sid)}};function parseCookieHeader(header){let out={};for(let pair of header.split(`;`)){let eq=pair.indexOf(`=`);if(eq===-1)continue;let key=pair.slice(0,eq).trim();if(!key)continue;let value=pair.slice(eq+1).trim();value.startsWith(`"`)&&value.endsWith(`"`)&&(value=value.slice(1,-1));try{out[key]=decodeURIComponent(value)}catch{out[key]=value}}return out}function sign(value,secret){return`s:${value}.${createHmac(`sha256`,secret).update(value).digest(`base64url`)}`}function unsign(signed,secret){if(!signed.startsWith(`s:`))return!1;let raw=signed.slice(2),dotIndex=raw.lastIndexOf(`.`);if(dotIndex===-1)return!1;let value=raw.slice(0,dotIndex),providedSig=raw.slice(dotIndex+1),expectedSig=createHmac(`sha256`,secret).update(value).digest(`base64url`),a=Buffer.from(providedSig),b=Buffer.from(expectedSig);return a.length!==b.length||!timingSafeEqual(a,b)?!1:value}function session(options){let{secret,cookieName=`kick.sid`,maxAge=864e5,rolling=!1,saveUninitialized=!0,store=new MemoryStore,cookie:cookieOpts={}}=options,cookieDefaults={httpOnly:cookieOpts.httpOnly??!0,secure:cookieOpts.secure??process.env.NODE_ENV===`production`,sameSite:cookieOpts.sameSite??`lax`,path:cookieOpts.path??`/`,...cookieOpts.domain?{domain:cookieOpts.domain}:{},maxAge};return async(req,res,next)=>{let cookies=req.cookies;if(!cookies){let header=req.headers.cookie;cookies=header?parseCookieHeader(header):{}}let signedCookie=cookies[cookieName],sid=!1,sessionData=null,isNew=!1;signedCookie&&(sid=unsign(signedCookie,secret),sid&&(sessionData=await store.get(sid))),(!sid||!sessionData)&&(sid=randomUUID(),sessionData={},isNew=!0);let currentSid=sid,currentData={...sessionData},destroyed=!1,sessionObj={get id(){return currentSid},data:currentData,async regenerate(){await store.destroy(currentSid),currentSid=randomUUID(),currentData={},sessionObj.data=currentData,await store.set(currentSid,currentData,maxAge),res.cookie(cookieName,sign(currentSid,secret),cookieDefaults)},async destroy(){await store.destroy(currentSid),destroyed=!0,currentData={},sessionObj.data=currentData,res.clearCookie(cookieName,{path:cookieDefaults.path})},async save(){destroyed||await store.set(currentSid,currentData,maxAge)}};req.session=sessionObj,isNew&&res.cookie(cookieName,sign(currentSid,secret),cookieDefaults),res.on(`finish`,async()=>{destroyed||isNew&&!saveUninitialized&&Object.keys(currentData).length===0||(await store.set(currentSid,currentData,maxAge),rolling&&!isNew&&store.touch&&await store.touch(currentSid,maxAge))}),rolling&&!isNew&&res.cookie(cookieName,sign(currentSid,secret),cookieDefaults),next()}}export{session};
12
12
  //# sourceMappingURL=session.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"session.mjs","names":[],"sources":["../../../src/http/middleware/session.ts"],"sourcesContent":["import { randomUUID, createHmac, timingSafeEqual } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\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\nimport { registerDisposable } from '../../core/disposables'\n\nexport interface SessionData {\n [key: string]: unknown\n}\n\nexport interface SessionStore {\n get(sid: string): Promise<SessionData | null>\n set(sid: string, data: SessionData, maxAge: number): Promise<void>\n destroy(sid: string): Promise<void>\n touch?(sid: string, maxAge: number): Promise<void>\n}\n\nexport interface Session {\n id: string\n data: SessionData\n regenerate(): Promise<void>\n destroy(): Promise<void>\n save(): Promise<void>\n}\n\nexport interface SessionOptions {\n /** Secret used to sign the session cookie (required) */\n secret: string\n /** Cookie name (default: 'kick.sid') */\n cookieName?: string\n /** Session max age in milliseconds (default: 86400000 = 24h) */\n maxAge?: number\n /** Reset maxAge on every response (default: false) */\n rolling?: boolean\n /** Save new sessions that have not been modified (default: true) */\n saveUninitialized?: boolean\n /** Cookie options */\n cookie?: {\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n path?: string\n domain?: string\n }\n /** Custom session store (default: in-memory store with TTL cleanup) */\n store?: SessionStore\n}\n\n// ── In-Memory Store ───────────────────────────────────────────────────\n\nclass MemoryStore implements SessionStore {\n private readonly sessions = new Map<string, { data: SessionData; expires: number }>()\n private readonly cleanupInterval: ReturnType<typeof setInterval>\n\n constructor() {\n // Purge expired sessions every 60 seconds\n this.cleanupInterval = setInterval(() => this.purge(), 60_000)\n this.cleanupInterval.unref()\n // Symmetric teardown: Application.shutdown() drains the disposables\n // registry — without this, every in-process app re-create (tests, HMR)\n // leaked one live interval + its session Map.\n registerDisposable(() => this.dispose())\n }\n\n /** Clear the cleanup interval and drop all sessions. */\n dispose(): void {\n clearInterval(this.cleanupInterval)\n this.sessions.clear()\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const entry = this.sessions.get(sid)\n if (!entry) return null\n if (Date.now() > entry.expires) {\n this.sessions.delete(sid)\n return null\n }\n return entry.data\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n this.sessions.set(sid, { data, expires: Date.now() + maxAge })\n }\n\n async destroy(sid: string): Promise<void> {\n this.sessions.delete(sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const entry = this.sessions.get(sid)\n if (entry) {\n entry.expires = Date.now() + maxAge\n }\n }\n\n private purge() {\n const now = Date.now()\n for (const [sid, entry] of this.sessions) {\n if (now > entry.expires) {\n this.sessions.delete(sid)\n }\n }\n }\n}\n\n// ── Cookie Header Parsing ─────────────────────────────────────────────\n\nfunction parseCookieHeader(header: string): Record<string, string> {\n const out: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const eq = pair.indexOf('=')\n if (eq === -1) continue\n const key = pair.slice(0, eq).trim()\n if (!key) continue\n let value = pair.slice(eq + 1).trim()\n if (value.startsWith('\"') && value.endsWith('\"')) {\n value = value.slice(1, -1)\n }\n try {\n out[key] = decodeURIComponent(value)\n } catch {\n out[key] = value\n }\n }\n return out\n}\n\n// ── Cookie Signing ────────────────────────────────────────────────────\n\nfunction sign(value: string, secret: string): string {\n const signature = createHmac('sha256', secret).update(value).digest('base64url')\n return `s:${value}.${signature}`\n}\n\nfunction unsign(signed: string, secret: string): string | false {\n if (!signed.startsWith('s:')) return false\n const raw = signed.slice(2)\n const dotIndex = raw.lastIndexOf('.')\n if (dotIndex === -1) return false\n\n const value = raw.slice(0, dotIndex)\n const providedSig = raw.slice(dotIndex + 1)\n const expectedSig = createHmac('sha256', secret).update(value).digest('base64url')\n\n const a = Buffer.from(providedSig)\n const b = Buffer.from(expectedSig)\n\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n return false\n }\n\n return value\n}\n\n// ── Middleware Factory ────────────────────────────────────────────────\n\n/**\n * Session management middleware.\n *\n * Attaches a `req.session` object with `id`, `data`, `regenerate()`,\n * `destroy()`, and `save()` methods. Session IDs are signed with\n * HMAC-SHA256 to prevent cookie tampering.\n *\n * Reads the session cookie from `req.cookies` when an upstream cookie\n * parser is present; otherwise parses `req.headers.cookie` directly, so\n * no cookie parser is required upstream.\n *\n * @example\n * ```ts\n * import { session } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * session({ secret: process.env.SESSION_SECRET! }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function session(options: SessionOptions) {\n const {\n secret,\n cookieName = 'kick.sid',\n maxAge = 86_400_000,\n rolling = false,\n saveUninitialized = true,\n store = new MemoryStore(),\n cookie: cookieOpts = {},\n } = options\n\n const cookieDefaults = {\n httpOnly: cookieOpts.httpOnly ?? true,\n secure: cookieOpts.secure ?? process.env.NODE_ENV === 'production',\n sameSite: cookieOpts.sameSite ?? ('lax' as const),\n path: cookieOpts.path ?? '/',\n ...(cookieOpts.domain ? { domain: cookieOpts.domain } : {}),\n maxAge,\n }\n\n return async (req: Request, res: Response, next: NextFunction) => {\n let cookies = (req as any).cookies as Record<string, string> | undefined\n if (!cookies) {\n const header = req.headers.cookie\n cookies = header ? parseCookieHeader(header) : {}\n }\n const signedCookie = cookies[cookieName]\n let sid: string | false = false\n let sessionData: SessionData | null = null\n let isNew = false\n\n // Attempt to recover existing session\n if (signedCookie) {\n sid = unsign(signedCookie, secret)\n if (sid) {\n sessionData = await store.get(sid)\n }\n }\n\n // Create new session if none found\n if (!sid || !sessionData) {\n sid = randomUUID()\n sessionData = {}\n isNew = true\n }\n\n let currentSid = sid\n let currentData = { ...sessionData }\n let destroyed = false\n\n const sessionObj: Session = {\n get id() {\n return currentSid\n },\n data: currentData,\n\n async regenerate() {\n await store.destroy(currentSid)\n currentSid = randomUUID()\n currentData = {}\n sessionObj.data = currentData\n await store.set(currentSid, currentData, maxAge)\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n },\n\n async destroy() {\n await store.destroy(currentSid)\n destroyed = true\n currentData = {}\n sessionObj.data = currentData\n res.clearCookie(cookieName, { path: cookieDefaults.path })\n },\n\n async save() {\n if (!destroyed) {\n await store.set(currentSid, currentData, maxAge)\n }\n },\n }\n\n ;(req as any).session = sessionObj\n\n // Set cookie for new sessions\n if (isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n // Auto-save on response finish\n res.on('finish', async () => {\n if (destroyed) return\n if (isNew && !saveUninitialized && Object.keys(currentData).length === 0) return\n\n await store.set(currentSid, currentData, maxAge)\n\n if (rolling && !isNew) {\n if (store.touch) {\n await store.touch(currentSid, maxAge)\n }\n }\n })\n\n // Rolling: refresh cookie on every response\n if (rolling && !isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;mIAiDA,IAAM,YAAN,KAA0C,CACxC,SAA4B,IAAI,IAChC,gBAEA,aAAc,CAEZ,KAAK,gBAAkB,gBAAkB,KAAK,MAAM,EAAG,GAAM,EAC7D,KAAK,gBAAgB,MAAM,EAI3B,uBAAyB,KAAK,QAAQ,CAAC,CACzC,CAGA,SAAgB,CACd,cAAc,KAAK,eAAe,EAClC,KAAK,SAAS,MAAM,CACtB,CAEA,MAAM,IAAI,IAA0C,CAClD,IAAM,MAAQ,KAAK,SAAS,IAAI,GAAG,EAMnC,OALK,MACD,KAAK,IAAI,EAAI,MAAM,SACrB,KAAK,SAAS,OAAO,GAAG,EACjB,MAEF,MAAM,KALM,IAMrB,CAEA,MAAM,IAAI,IAAa,KAAmB,OAA+B,CACvE,KAAK,SAAS,IAAI,IAAK,CAAE,KAAM,QAAS,KAAK,IAAI,EAAI,MAAO,CAAC,CAC/D,CAEA,MAAM,QAAQ,IAA4B,CACxC,KAAK,SAAS,OAAO,GAAG,CAC1B,CAEA,MAAM,MAAM,IAAa,OAA+B,CACtD,IAAM,MAAQ,KAAK,SAAS,IAAI,GAAG,EAC/B,QACF,MAAM,QAAU,KAAK,IAAI,EAAI,OAEjC,CAEA,OAAgB,CACd,IAAM,IAAM,KAAK,IAAI,EACrB,IAAK,GAAM,CAAC,IAAK,SAAU,KAAK,SAC1B,IAAM,MAAM,SACd,KAAK,SAAS,OAAO,GAAG,CAG9B,CACF,EAIA,SAAS,kBAAkB,OAAwC,CACjE,IAAM,IAA8B,CAAC,EACrC,IAAK,IAAM,QAAQ,OAAO,MAAM,GAAG,EAAG,CACpC,IAAM,GAAK,KAAK,QAAQ,GAAG,EAC3B,GAAI,KAAO,GAAI,SACf,IAAM,IAAM,KAAK,MAAM,EAAG,EAAE,CAAC,CAAC,KAAK,EACnC,GAAI,CAAC,IAAK,SACV,IAAI,MAAQ,KAAK,MAAM,GAAK,CAAC,CAAC,CAAC,KAAK,EAChC,MAAM,WAAW,GAAG,GAAK,MAAM,SAAS,GAAG,IAC7C,MAAQ,MAAM,MAAM,EAAG,EAAE,GAE3B,GAAI,CACF,IAAI,KAAO,mBAAmB,KAAK,CACrC,MAAQ,CACN,IAAI,KAAO,KACb,CACF,CACA,OAAO,GACT,CAIA,SAAS,KAAK,MAAe,OAAwB,CAEnD,MAAO,KAAK,MAAM,GADA,WAAW,SAAU,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,WACvC,GAC/B,CAEA,SAAS,OAAO,OAAgB,OAAgC,CAC9D,GAAI,CAAC,OAAO,WAAW,IAAI,EAAG,MAAO,GACrC,IAAM,IAAM,OAAO,MAAM,CAAC,EACpB,SAAW,IAAI,YAAY,GAAG,EACpC,GAAI,WAAa,GAAI,MAAO,GAE5B,IAAM,MAAQ,IAAI,MAAM,EAAG,QAAQ,EAC7B,YAAc,IAAI,MAAM,SAAW,CAAC,EACpC,YAAc,WAAW,SAAU,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,WAAW,EAE3E,EAAI,OAAO,KAAK,WAAW,EAC3B,EAAI,OAAO,KAAK,WAAW,EAMjC,OAJI,EAAE,SAAW,EAAE,QAAU,CAAC,gBAAgB,EAAG,CAAC,EACzC,GAGF,KACT,CA4BA,SAAgB,QAAQ,QAAyB,CAC/C,GAAM,CACJ,OACA,WAAa,WACb,OAAS,MACT,QAAU,GACV,kBAAoB,GACpB,MAAQ,IAAI,YACZ,OAAQ,WAAa,CAAC,GACpB,QAEE,eAAiB,CACrB,SAAU,WAAW,UAAY,GACjC,OAAQ,WAAW,QAAU,QAAQ,IAAI,WAAa,aACtD,SAAU,WAAW,UAAa,MAClC,KAAM,WAAW,MAAQ,IACzB,GAAI,WAAW,OAAS,CAAE,OAAQ,WAAW,MAAO,EAAI,CAAC,EACzD,MACF,EAEA,OAAO,MAAO,IAAc,IAAe,OAAuB,CAChE,IAAI,QAAW,IAAY,QAC3B,GAAI,CAAC,QAAS,CACZ,IAAM,OAAS,IAAI,QAAQ,OAC3B,QAAU,OAAS,kBAAkB,MAAM,EAAI,CAAC,CAClD,CACA,IAAM,aAAe,QAAQ,YACzB,IAAsB,GACtB,YAAkC,KAClC,MAAQ,GAGR,eACF,IAAM,OAAO,aAAc,MAAM,EAC7B,MACF,YAAc,MAAM,MAAM,IAAI,GAAG,KAKjC,CAAC,KAAO,CAAC,eACX,IAAM,WAAW,EACjB,YAAc,CAAC,EACf,MAAQ,IAGV,IAAI,WAAa,IACb,YAAc,CAAE,GAAG,WAAY,EAC/B,UAAY,GAEV,WAAsB,CAC1B,IAAI,IAAK,CACP,OAAO,UACT,EACA,KAAM,YAEN,MAAM,YAAa,CACjB,MAAM,MAAM,QAAQ,UAAU,EAC9B,WAAa,WAAW,EACxB,YAAc,CAAC,EACf,WAAW,KAAO,YAClB,MAAM,MAAM,IAAI,WAAY,YAAa,MAAM,EAC/C,IAAI,OAAO,WAAY,KAAK,WAAY,MAAM,EAAG,cAAc,CACjE,EAEA,MAAM,SAAU,CACd,MAAM,MAAM,QAAQ,UAAU,EAC9B,UAAY,GACZ,YAAc,CAAC,EACf,WAAW,KAAO,YAClB,IAAI,YAAY,WAAY,CAAE,KAAM,eAAe,IAAK,CAAC,CAC3D,EAEA,MAAM,MAAO,CACN,WACH,MAAM,MAAM,IAAI,WAAY,YAAa,MAAM,CAEnD,CACF,EAEC,IAAa,QAAU,WAGpB,OACF,IAAI,OAAO,WAAY,KAAK,WAAY,MAAM,EAAG,cAAc,EAIjE,IAAI,GAAG,SAAU,SAAY,CACvB,WACA,OAAS,CAAC,mBAAqB,OAAO,KAAK,WAAW,CAAC,CAAC,SAAW,IAEvE,MAAM,MAAM,IAAI,WAAY,YAAa,MAAM,EAE3C,SAAW,CAAC,OACV,MAAM,OACR,MAAM,MAAM,MAAM,WAAY,MAAM,EAG1C,CAAC,EAGG,SAAW,CAAC,OACd,IAAI,OAAO,WAAY,KAAK,WAAY,MAAM,EAAG,cAAc,EAGjE,KAAK,CACP,CACF"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as SpaAdapterOptions, t as SpaAdapter } from "../../spa-BdvZFXJz.mjs";
2
+ import { n as SpaAdapterOptions, t as SpaAdapter } from "../../spa-BwO1qOuA.mjs";
3
3
  export { SpaAdapter, SpaAdapterOptions };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.1.1
2
+ * @forinda/kickjs v6.3.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{v as __require}from"../../container-Oy24XRnS.mjs";import{Logger}from"../../core/logger.mjs";import"../../core-kmJ4y4fJ.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};
11
+ import{t as __require}from"../../rolldown-runtime-Dv-xQPN8.mjs";import{Logger}from"../../core/logger.mjs";import"../../core-3S12kJ7Q.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