@forinda/kickjs 5.7.0 → 5.8.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 (140) hide show
  1. package/dist/{adapter-CoJp35rz.d.mts → adapter-RzfEzd-c.d.mts} +46 -11
  2. package/dist/adapter-RzfEzd-c.d.mts.map +1 -0
  3. package/dist/app-module-BE7hAASB.d.mts.map +1 -1
  4. package/dist/{application-DmekGKp7.d.mts → application-CwLcDTyg.d.mts} +3 -3
  5. package/dist/application-CwLcDTyg.d.mts.map +1 -0
  6. package/dist/application-h14rczhJ.mjs +12 -0
  7. package/dist/application-h14rczhJ.mjs.map +1 -0
  8. package/dist/{bootstrap-CwP2GGu0.d.mts → bootstrap-B8y526yc.d.mts} +2 -2
  9. package/dist/bootstrap-B8y526yc.d.mts.map +1 -0
  10. package/dist/{bootstrap-D86s_Aac.mjs → bootstrap-V0q7pgA4.mjs} +3 -3
  11. package/dist/{bootstrap-D86s_Aac.mjs.map → bootstrap-V0q7pgA4.mjs.map} +1 -1
  12. package/dist/cache-XrNWj1KQ.d.mts.map +1 -1
  13. package/dist/config/index.d.mts +1 -1
  14. package/dist/config/index.mjs +2 -2
  15. package/dist/{config-BVvCHSX2.mjs → config-C_oycBvB.mjs} +3 -3
  16. package/dist/{config-BVvCHSX2.mjs.map → config-C_oycBvB.mjs.map} +1 -1
  17. package/dist/container-BZKeHc5X.d.mts.map +1 -1
  18. package/dist/{container-COmj1vM3.mjs → container-BvRsVif5.mjs} +3 -3
  19. package/dist/{container-COmj1vM3.mjs.map → container-BvRsVif5.mjs.map} +1 -1
  20. package/dist/context-ByhFuf19.mjs +12 -0
  21. package/dist/context-ByhFuf19.mjs.map +1 -0
  22. package/dist/context-aocg9gO-.d.mts.map +1 -1
  23. package/dist/context-decorator-BYf8HdMl.d.mts.map +1 -1
  24. package/dist/core/adapter.d.mts +2 -2
  25. package/dist/core/adapter.mjs +1 -1
  26. package/dist/core/app-module.mjs +1 -1
  27. package/dist/core/cache.mjs +1 -1
  28. package/dist/core/cache.mjs.map +1 -1
  29. package/dist/core/container.mjs +2 -2
  30. package/dist/core/cron.mjs +2 -2
  31. package/dist/core/cron.mjs.map +1 -1
  32. package/dist/core/decorators.d.mts +1 -1
  33. package/dist/core/decorators.mjs +2 -2
  34. package/dist/core/decorators.mjs.map +1 -1
  35. package/dist/core/errors.mjs +2 -2
  36. package/dist/core/errors.mjs.map +1 -1
  37. package/dist/core/interfaces.mjs +1 -1
  38. package/dist/core/interfaces.mjs.map +1 -1
  39. package/dist/core/logger.mjs +1 -1
  40. package/dist/core/logger.mjs.map +1 -1
  41. package/dist/core/path.mjs +1 -1
  42. package/dist/core/path.mjs.map +1 -1
  43. package/dist/core/plugin.d.mts +1 -1
  44. package/dist/core/plugin.mjs +1 -1
  45. package/dist/core/reactivity.mjs +1 -1
  46. package/dist/core/reactivity.mjs.map +1 -1
  47. package/dist/{core-CJKmg8QR.mjs → core-BQHQsimv.mjs} +3 -3
  48. package/dist/{core-CJKmg8QR.mjs.map → core-BQHQsimv.mjs.map} +1 -1
  49. package/dist/cors-BfgqfLPd.d.mts.map +1 -1
  50. package/dist/cron-GNoYydfg.d.mts.map +1 -1
  51. package/dist/csrf-DiK9zept.d.mts.map +1 -1
  52. package/dist/{decorators-ZcjsV1O0.d.mts → decorators-BYi8lB37.d.mts} +7 -1
  53. package/dist/decorators-BYi8lB37.d.mts.map +1 -0
  54. package/dist/{env-DfB1WNRf.mjs → env-D3Q6sGZP.mjs} +3 -3
  55. package/dist/{env-DfB1WNRf.mjs.map → env-D3Q6sGZP.mjs.map} +1 -1
  56. package/dist/errors-uaHCeXRz.d.mts.map +1 -1
  57. package/dist/execution-context-DKnt61pY.d.mts.map +1 -1
  58. package/dist/helmet-CaMbjzBG.d.mts.map +1 -1
  59. package/dist/http/application.d.mts +1 -1
  60. package/dist/http/application.mjs +2 -2
  61. package/dist/http/bootstrap.d.mts +1 -1
  62. package/dist/http/bootstrap.mjs +2 -2
  63. package/dist/http/context.mjs +2 -2
  64. package/dist/http/middleware/cors.mjs +1 -1
  65. package/dist/http/middleware/cors.mjs.map +1 -1
  66. package/dist/http/middleware/csrf.mjs +1 -1
  67. package/dist/http/middleware/csrf.mjs.map +1 -1
  68. package/dist/http/middleware/error-handler.mjs +2 -2
  69. package/dist/http/middleware/error-handler.mjs.map +1 -1
  70. package/dist/http/middleware/helmet.mjs +1 -1
  71. package/dist/http/middleware/helmet.mjs.map +1 -1
  72. package/dist/http/middleware/rate-limit.mjs +2 -2
  73. package/dist/http/middleware/rate-limit.mjs.map +1 -1
  74. package/dist/http/middleware/request-id.mjs +1 -1
  75. package/dist/http/middleware/request-id.mjs.map +1 -1
  76. package/dist/http/middleware/request-logger.mjs +2 -2
  77. package/dist/http/middleware/request-logger.mjs.map +1 -1
  78. package/dist/http/middleware/session.mjs +1 -1
  79. package/dist/http/middleware/session.mjs.map +1 -1
  80. package/dist/http/middleware/spa.d.mts +1 -1
  81. package/dist/http/middleware/spa.mjs +2 -2
  82. package/dist/http/middleware/spa.mjs.map +1 -1
  83. package/dist/http/middleware/upload.d.mts +1 -1
  84. package/dist/http/middleware/upload.mjs +1 -1
  85. package/dist/http/middleware/upload.mjs.map +1 -1
  86. package/dist/http/middleware/validate.mjs +2 -2
  87. package/dist/http/middleware/validate.mjs.map +1 -1
  88. package/dist/http/middleware/views.d.mts +1 -1
  89. package/dist/http/middleware/views.mjs +2 -2
  90. package/dist/http/middleware/views.mjs.map +1 -1
  91. package/dist/http/query/index.mjs +2 -2
  92. package/dist/http/router-builder.d.mts +1 -1
  93. package/dist/http/router-builder.mjs +2 -2
  94. package/dist/http/router-builder.mjs.map +1 -1
  95. package/dist/index-B9cyOQAe.d.mts.map +1 -1
  96. package/dist/{index-CkKLXZHg.d.mts → index-C5aLtJkk.d.mts} +3 -3
  97. package/dist/index-C5aLtJkk.d.mts.map +1 -0
  98. package/dist/{index-oB1CDq5i.d.mts → index-DM1PteZT.d.mts} +2 -2
  99. package/dist/index-DM1PteZT.d.mts.map +1 -0
  100. package/dist/index.d.mts +12 -12
  101. package/dist/index.mjs +2 -2
  102. package/dist/index.mjs.map +1 -1
  103. package/dist/interfaces-B50n7dwl.d.mts.map +1 -1
  104. package/dist/logger-CLVRZf2N.d.mts.map +1 -1
  105. package/dist/{metadata-D9F5Lm-z.mjs → metadata-DN0sBHmY.mjs} +2 -2
  106. package/dist/{metadata-D9F5Lm-z.mjs.map → metadata-DN0sBHmY.mjs.map} +1 -1
  107. package/dist/path-Diviu6hv.d.mts.map +1 -1
  108. package/dist/{plugin-NMHVmDRF.d.mts → plugin-BGK3PJOJ.d.mts} +2 -2
  109. package/dist/plugin-BGK3PJOJ.d.mts.map +1 -0
  110. package/dist/{query-Di7R8qNX.mjs → query-BuX74AmB.mjs} +2 -2
  111. package/dist/{query-Di7R8qNX.mjs.map → query-BuX74AmB.mjs.map} +1 -1
  112. package/dist/reactivity-CfJceo4H.d.mts.map +1 -1
  113. package/dist/request-id-BgglvZHN.d.mts.map +1 -1
  114. package/dist/request-logger-CWHbAzTE.d.mts.map +1 -1
  115. package/dist/{router-builder-BfCCeQB-.d.mts → router-builder-QNfwAjQ8.d.mts} +2 -2
  116. package/dist/router-builder-QNfwAjQ8.d.mts.map +1 -0
  117. package/dist/session-N0hActOO.d.mts.map +1 -1
  118. package/dist/{spa-DIXtRBj-.d.mts → spa-BlW5xxU5.d.mts} +2 -2
  119. package/dist/spa-BlW5xxU5.d.mts.map +1 -0
  120. package/dist/{upload-DqRwWhX0.d.mts → upload-5goxbO9A.d.mts} +2 -2
  121. package/dist/upload-5goxbO9A.d.mts.map +1 -0
  122. package/dist/validate-HwAyWTwH.d.mts.map +1 -1
  123. package/dist/{views-DDT9vghc.d.mts → views-CNCTSpcw.d.mts} +2 -2
  124. package/dist/views-CNCTSpcw.d.mts.map +1 -0
  125. package/package.json +4 -4
  126. package/dist/adapter-CoJp35rz.d.mts.map +0 -1
  127. package/dist/application-Bg_-CIPK.mjs +0 -12
  128. package/dist/application-Bg_-CIPK.mjs.map +0 -1
  129. package/dist/application-DmekGKp7.d.mts.map +0 -1
  130. package/dist/bootstrap-CwP2GGu0.d.mts.map +0 -1
  131. package/dist/context-CvJQzbBF.mjs +0 -12
  132. package/dist/context-CvJQzbBF.mjs.map +0 -1
  133. package/dist/decorators-ZcjsV1O0.d.mts.map +0 -1
  134. package/dist/index-CkKLXZHg.d.mts.map +0 -1
  135. package/dist/index-oB1CDq5i.d.mts.map +0 -1
  136. package/dist/plugin-NMHVmDRF.d.mts.map +0 -1
  137. package/dist/router-builder-BfCCeQB-.d.mts.map +0 -1
  138. package/dist/spa-DIXtRBj-.d.mts.map +0 -1
  139. package/dist/upload-DqRwWhX0.d.mts.map +0 -1
  140. package/dist/views-DDT9vghc.d.mts.map +0 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{h as __require,t as Container}from"./container-COmj1vM3.mjs";import{z}from"zod";function tryLoadDotenv(){try{__require(`dotenv`).config({override:!1,quiet:!0})}catch{}}tryLoadDotenv();const baseEnvSchema=z.object({PORT:z.coerce.number().default(3e3),NODE_ENV:z.enum([`development`,`production`,`test`]).default(`development`),LOG_LEVEL:z.string().default(`info`)});let cachedEnv=null,cachedSchema=null;function defineEnv(extend){let userSchema=extend(baseEnvSchema);return baseEnvSchema.extend(userSchema.shape)}function loadEnv(schema){let s=schema??cachedSchema??baseEnvSchema;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&&loadEnv(cachedSchema)}function resetEnvCache(){cachedEnv=null,cachedSchema=null}export{reloadEnv as a,loadEnv as i,defineEnv as n,resetEnvCache as o,getEnv as r,baseEnvSchema as t};
12
- //# sourceMappingURL=env-DfB1WNRf.mjs.map
11
+ import{h as __require,t as Container}from"./container-BvRsVif5.mjs";import{z}from"zod";function tryLoadDotenv(){try{__require(`dotenv`).config({override:!1,quiet:!0})}catch{}}tryLoadDotenv();const baseEnvSchema=z.object({PORT:z.coerce.number().default(3e3),NODE_ENV:z.enum([`development`,`production`,`test`]).default(`development`),LOG_LEVEL:z.string().default(`info`)});let cachedEnv=null,cachedSchema=null;function defineEnv(extend){let userSchema=extend(baseEnvSchema);return baseEnvSchema.extend(userSchema.shape)}function loadEnv(schema){let s=schema??cachedSchema??baseEnvSchema;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&&loadEnv(cachedSchema)}function resetEnvCache(){cachedEnv=null,cachedSchema=null}export{reloadEnv as a,loadEnv as i,defineEnv as n,resetEnvCache as o,getEnv as r,baseEnvSchema as t};
12
+ //# sourceMappingURL=env-D3Q6sGZP.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"env-DfB1WNRf.mjs","names":[],"sources":["../src/config/env.ts"],"sourcesContent":["import { z } from 'zod'\nimport { Container } from '../core/container'\nimport type { EnvKey } from '../core/decorators'\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 * Base environment schema with common server variables.\n * Users extend this with their own application-specific vars.\n */\nexport const baseEnvSchema = 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/** 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 userSchema = extend(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 baseEnvSchema.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 ?? baseEnvSchema\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 loadEnv(cachedSchema)\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\nexport type Env = z.infer<typeof baseEnvSchema>\n"],"mappings":";;;;;;;;;;uFAmBA,SAAS,eAAsB,CAC7B,GAAI,CAMF,UAJuB,SAIjB,CAAC,OAAO,CAAE,SAAU,GAAO,MAAO,GAAM,CAAC,MACzC,GAKV,eAAe,CAMf,MAAa,cAAgB,EAAE,OAAO,CAEpC,KAAM,EAAE,OAAO,QAAQ,CAAC,QAAQ,IAAK,CACrC,SAAU,EAAE,KAAK,CAAC,cAAe,aAAc,OAAO,CAAC,CAAC,QAAQ,cAAc,CAC9E,UAAW,EAAE,QAAQ,CAAC,QAAQ,OAAO,CACtC,CAAC,CAGF,IAAI,UAAiB,KACjB,aAAoB,KAgCxB,SAAgB,UACd,OAC6C,CAC7C,IAAM,WAAa,OAAO,cAAc,CAIxC,OAAO,cAAc,OAAO,WAAW,MAAM,CA4B/C,SAAgB,QAAoC,OAAiB,CAGnE,IAAM,EAAI,QAAU,cAAgB,cASpC,OAPI,WAAa,eAAiB,EAAU,WAC5C,aAAe,EACf,UAAY,EAAE,MAAM,QAAQ,IAAI,CAGhC,UAAU,aAAgB,KAAgB,YAAY,KAE/C,WA6BT,SAAgB,OAAO,IAAa,OAAgC,CAElE,OADY,OAAS,QAAQ,OAAO,CAAI,SAAS,EACtC,KAkBb,SAAgB,WAAkB,CAEhC,GAAI,CAEF,UAAQ,SAAS,CAAC,OAAO,CAAE,SAAU,GAAM,MAAO,GAAM,CAAC,MACnD,EAUR,UAAY,KACR,cACF,QAAQ,aAAa,CAUzB,SAAgB,eAAsB,CACpC,UAAY,KACZ,aAAe"}
1
+ {"version":3,"file":"env-D3Q6sGZP.mjs","names":[],"sources":["../src/config/env.ts"],"sourcesContent":["import { z } from 'zod'\nimport { Container } from '../core/container'\nimport type { EnvKey } from '../core/decorators'\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 * Base environment schema with common server variables.\n * Users extend this with their own application-specific vars.\n */\nexport const baseEnvSchema = 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/** 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 userSchema = extend(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 baseEnvSchema.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 ?? baseEnvSchema\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 loadEnv(cachedSchema)\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\nexport type Env = z.infer<typeof baseEnvSchema>\n"],"mappings":";;;;;;;;;;uFAmBA,SAAS,eAAsB,CAC7B,GAAI,CAMF,UAJuB,QAIlB,EAAE,OAAO,CAAE,SAAU,GAAO,MAAO,EAAK,CAAC,CAChD,MAAQ,CAGR,CACF,CACA,cAAc,EAMd,MAAa,cAAgB,EAAE,OAAO,CAEpC,KAAM,EAAE,OAAO,OAAO,EAAE,QAAQ,GAAI,EACpC,SAAU,EAAE,KAAK,CAAC,cAAe,aAAc,MAAM,CAAC,EAAE,QAAQ,aAAa,EAC7E,UAAW,EAAE,OAAO,EAAE,QAAQ,MAAM,CACtC,CAAC,EAGD,IAAI,UAAiB,KACjB,aAAoB,KAgCxB,SAAgB,UACd,OAC6C,CAC7C,IAAM,WAAa,OAAO,aAAa,EAIvC,OAAO,cAAc,OAAO,WAAW,KAAK,CAC9C,CA2BA,SAAgB,QAAoC,OAAiB,CAGnE,IAAM,EAAI,QAAU,cAAgB,cASpC,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,GACrC,IACb,CAiBA,SAAgB,WAAkB,CAEhC,GAAI,CAEF,UAAQ,QAAQ,EAAE,OAAO,CAAE,SAAU,GAAM,MAAO,EAAK,CAAC,CAC1D,MAAQ,CAGR,CAOA,UAAY,KACR,cACF,QAAQ,YAAY,CAExB,CAQA,SAAgB,eAAsB,CACpC,UAAY,KACZ,aAAe,IACjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors-uaHCeXRz.d.mts","names":[],"sources":["../src/core/errors.ts"],"mappings":";;;AAUA;;;;;;;;;cAAa,UAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;KAmCD,cAAA,WAAyB,UAAA,eAAyB,UAAA;;UAG7C,eAAA;EACf,KAAA;EACA,OAAA;EACA,IAAA;AAAA;;;AAeF;;;;;;;;;;cAAa,aAAA,SAAsB,KAAA;EAAA,SAEf,MAAA;EAAA,SAEA,OAAA;EAAA,SACA,OAAA,GAAU,MAAA;cAHV,MAAA,UAChB,OAAA,UACgB,OAAA,wBACA,OAAA,GAAU,MAAA;EAsDsD;EA/ClF,WAAA,CAAY,OAAA,EAAS,MAAA,mBAAyB,aAAA;EA0DuC;EAAA,OAlD9E,YAAA,CAAa,KAAA,OAAY,OAAA,YAAmB,aAAA;EAAA,OAc5C,UAAA,CAAW,OAAA,YAAuB,aAAA;EAAA,OAGlC,YAAA,CAAa,OAAA,WAA0B,eAAA,YAAwB,aAAA;EAAA,OAQ/D,SAAA,CAAU,OAAA,YAAqB,aAAA;EAAA,OAG/B,QAAA,CAAS,OAAA,YAAqB,aAAA;EAAA,OAG9B,gBAAA,CAAiB,cAAA,YAA0B,OAAA,YAA8B,aAAA;EAAA,OAKzE,QAAA,CAAS,OAAA,YAAoB,aAAA;EAAA,OAG7B,aAAA,CAAc,OAAA,WAAkC,OAAA,GAAU,eAAA,KAAiB,aAAA;EAAA,OAG3E,eAAA,CAAgB,OAAA,WAA+B,iBAAA,YAA0B,aAAA;EAAA,OAQzE,kBAAA,CAAmB,OAAA,WAAiC,iBAAA,YAA0B,aAAA;EAAA,OAQ9E,QAAA,CAAS,OAAA,YAAiC,aAAA;AAAA"}
1
+ {"version":3,"file":"errors-uaHCeXRz.d.mts","names":[],"sources":["../src/core/errors.ts"],"mappings":";;;AAUA;;;;;;;;;cAAa,UAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;KAmCD,cAAA,WAAyB,UAAA,eAAyB,UAAU;;UAGvD,eAAA;EACf,KAAA;EACA,OAAA;EACA,IAAA;AAAA;;AAAI;AAeN;;;;;;;;;;cAAa,aAAA,SAAsB,KAAA;EAAA,SAEf,MAAA;EAAA,SAEA,OAAA;EAAA,SACA,OAAA,GAAU,MAAA;cAHV,MAAA,UAChB,OAAA,UACgB,OAAA,wBACA,OAAA,GAAU,MAAA;EAsDsD;EA/ClF,WAAA,CAAY,OAAA,EAAS,MAAA,mBAAyB,aAAA;EA0DuC;EAAA,OAlD9E,YAAA,CAAa,KAAA,OAAY,OAAA,YAAmB,aAAA;EAAA,OAc5C,UAAA,CAAW,OAAA,YAAuB,aAAA;EAAA,OAGlC,YAAA,CAAa,OAAA,WAA0B,eAAA,YAAwB,aAAA;EAAA,OAQ/D,SAAA,CAAU,OAAA,YAAqB,aAAA;EAAA,OAG/B,QAAA,CAAS,OAAA,YAAqB,aAAA;EAAA,OAG9B,gBAAA,CAAiB,cAAA,YAA0B,OAAA,YAA8B,aAAA;EAAA,OAKzE,QAAA,CAAS,OAAA,YAAoB,aAAA;EAAA,OAG7B,aAAA,CAAc,OAAA,WAAkC,OAAA,GAAU,eAAA,KAAiB,aAAA;EAAA,OAG3E,eAAA,CAAgB,OAAA,WAA+B,iBAAA,YAA0B,aAAA;EAAA,OAQzE,kBAAA,CAAmB,OAAA,WAAiC,iBAAA,YAA0B,aAAA;EAAA,OAQ9E,QAAA,CAAS,OAAA,YAAiC,aAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"execution-context-DKnt61pY.d.mts","names":[],"sources":["../src/core/execution-context.ts"],"mappings":";;;AAmCA;;;;;AAGA;;;;;;;;;;;;;;;;;;;AAYA;;;;;;;;UAfiB,WAAA;;KAGL,SAAA,yCAAkD,CAAA,eAAgB,WAAA,GAC1E,WAAA,CAAY,CAAA,IACZ,QAAA;;;;;;;;;UAUa,gBAAA;EAIsB;EAFrC,GAAA,mBAAsB,GAAA,EAAK,CAAA,GAAI,SAAA,CAAU,CAAA;EAEX;EAA9B,GAAA,mBAAsB,GAAA,EAAK,CAAA,EAAG,KAAA,EAAO,SAAA,CAAU,CAAA;EAE7B;EAAA,SAAT,SAAA;AAAA"}
1
+ {"version":3,"file":"execution-context-DKnt61pY.d.mts","names":[],"sources":["../src/core/execution-context.ts"],"mappings":";;;AAmCA;;;;AAA4B;AAG5B;;;;;;;;;;;;;;;;;;AAEY;AAUZ;;;;;;;;UAfiB,WAAA;;KAGL,SAAA,yCAAkD,CAAA,eAAgB,WAAA,GAC1E,WAAA,CAAY,CAAA,IACZ,QAAA;;;;;;;;;UAUa,gBAAA;EAIsB;EAFrC,GAAA,mBAAsB,GAAA,EAAK,CAAA,GAAI,SAAA,CAAU,CAAA;EAEX;EAA9B,GAAA,mBAAsB,GAAA,EAAK,CAAA,EAAG,KAAA,EAAO,SAAA,CAAU,CAAA;EAE7B;EAAA,SAAT,SAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"helmet-CaMbjzBG.d.mts","names":[],"sources":["../src/http/middleware/helmet.ts"],"mappings":";;;;UAEiB,aAAA;;EAEf,qBAAA,aAAkC,MAAA;EAFN;EAI5B,OAAA;EAFwC;EAIxC,UAAA;EAJkC;EAMlC,IAAA;IAAiB,MAAA;IAAiB,iBAAA;IAA6B,OAAA;EAAA;EAAA;EAE/D,WAAA;EAEA;EAAA,aAAA;EAIA;EAFA,cAAA;EAES;EAAT,SAAA;AAAA;;;;;;;;;;;;iBAcc,MAAA,CAAO,OAAA,GAAS,aAAA,IAYtB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA"}
1
+ {"version":3,"file":"helmet-CaMbjzBG.d.mts","names":[],"sources":["../src/http/middleware/helmet.ts"],"mappings":";;;;UAEiB,aAAA;;EAEf,qBAAA,aAAkC,MAAM;EAFZ;EAI5B,OAAA;EAFwC;EAIxC,UAAA;EAJkC;EAMlC,IAAA;IAAiB,MAAA;IAAiB,iBAAA;IAA6B,OAAA;EAAA;EAAA;EAE/D,WAAA;EAEA;EAAA,aAAA;EAIA;EAFA,cAAA;EAES;EAAT,SAAA;AAAA;;;;;;;;;;;;iBAcc,MAAA,CAAO,OAAA,GAAS,aAAA,IAYtB,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-DmekGKp7.mjs";
2
+ import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-CwLcDTyg.mjs";
3
3
  export { Application, ApplicationOptions, MiddlewareEntry };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.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-Bg_-CIPK.mjs";export{Application};
11
+ import{t as Application}from"../application-h14rczhJ.mjs";export{Application};
@@ -1,3 +1,3 @@
1
1
 
2
- import { t as bootstrap } from "../bootstrap-CwP2GGu0.mjs";
2
+ import { t as bootstrap } from "../bootstrap-B8y526yc.mjs";
3
3
  export { bootstrap };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.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-D86s_Aac.mjs";export{bootstrap};
11
+ import{t as bootstrap}from"../bootstrap-V0q7pgA4.mjs";export{bootstrap};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.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-CvJQzbBF.mjs";export{RequestContext};
11
+ import{t as RequestContext}from"../context-ByhFuf19.mjs";export{RequestContext};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"cors.mjs","names":[],"sources":["../../../src/http/middleware/cors.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface CorsOptions {\n /** Allowed origin(s). `true` reflects request origin, `'*'` allows all, `false` rejects all. Default: `false` (restrictive) */\n origin?: boolean | string | RegExp | (string | RegExp)[]\n /** Allowed HTTP methods. Default: `['GET','HEAD','PUT','PATCH','POST','DELETE']` */\n methods?: string[]\n /** Allowed request headers. Default: reflects `Access-Control-Request-Headers` */\n allowedHeaders?: string[]\n /** Headers exposed to the browser. Default: none */\n exposedHeaders?: string[]\n /** Allow credentials (cookies, authorization). Default: false */\n credentials?: boolean\n /** Preflight cache duration in seconds. Default: 86400 (24h) */\n maxAge?: number\n /** Handle preflight OPTIONS requests. Default: true */\n preflight?: boolean\n}\n\nconst DEFAULT_METHODS = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']\n\nfunction isOriginAllowed(requestOrigin: string, allowed: CorsOptions['origin']): boolean {\n if (allowed === true || allowed === '*') return true\n if (typeof allowed === 'string') return requestOrigin === allowed\n if (allowed instanceof RegExp) return allowed.test(requestOrigin)\n if (Array.isArray(allowed)) {\n return allowed.some((pattern) =>\n typeof pattern === 'string' ? requestOrigin === pattern : pattern.test(requestOrigin),\n )\n }\n return false\n}\n\n/**\n * CORS middleware with correct spec behavior.\n *\n * - Reflects origin from allowlist (sets `Vary: Origin`)\n * - Handles preflight `OPTIONS` with `204 No Content`\n * - Supports string, regex, and array origin matching\n *\n * @example\n * ```ts\n * // Allow all origins\n * bootstrap({ middleware: [cors(), express.json()] })\n *\n * // Allowlist specific origins\n * bootstrap({\n * middleware: [\n * cors({\n * origin: ['https://app.example.com', /\\.example\\.com$/],\n * credentials: true,\n * }),\n * express.json(),\n * ],\n * })\n * ```\n */\nexport function cors(options: CorsOptions = {}) {\n const {\n origin = false,\n methods = DEFAULT_METHODS,\n allowedHeaders,\n exposedHeaders,\n credentials = false,\n maxAge = 86400,\n preflight = true,\n } = options\n\n return (req: Request, res: Response, next: NextFunction) => {\n const requestOrigin = req.headers.origin\n\n // Determine the Access-Control-Allow-Origin value\n if (origin === '*' && !credentials) {\n res.setHeader('Access-Control-Allow-Origin', '*')\n } else if (requestOrigin) {\n if (isOriginAllowed(requestOrigin, origin)) {\n res.setHeader('Access-Control-Allow-Origin', requestOrigin)\n // Must vary by Origin when reflecting — CDN/proxy correctness\n res.setHeader('Vary', 'Origin')\n }\n }\n\n if (credentials) {\n res.setHeader('Access-Control-Allow-Credentials', 'true')\n }\n\n if (exposedHeaders?.length) {\n res.setHeader('Access-Control-Expose-Headers', exposedHeaders.join(', '))\n }\n\n // Preflight handling\n if (preflight && req.method === 'OPTIONS') {\n res.setHeader('Access-Control-Allow-Methods', methods.join(', '))\n\n const reqHeaders = allowedHeaders ?? req.headers['access-control-request-headers']\n if (reqHeaders) {\n const value = Array.isArray(reqHeaders) ? reqHeaders.join(', ') : reqHeaders\n res.setHeader('Access-Control-Allow-Headers', value)\n }\n\n if (maxAge) {\n res.setHeader('Access-Control-Max-Age', String(maxAge))\n }\n\n res.statusCode = 204\n res.setHeader('Content-Length', '0')\n res.end()\n return\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAM,gBAAkB,CAAC,MAAO,OAAQ,MAAO,QAAS,OAAQ,SAAS,CAEzE,SAAS,gBAAgB,cAAuB,QAAyC,CASvF,OARI,UAAY,IAAQ,UAAY,IAAY,GAC5C,OAAO,SAAY,SAAiB,gBAAkB,QACtD,mBAAmB,OAAe,QAAQ,KAAK,cAAc,CAC7D,MAAM,QAAQ,QAAQ,CACjB,QAAQ,KAAM,SACnB,OAAO,SAAY,SAAW,gBAAkB,QAAU,QAAQ,KAAK,cAAc,CACtF,CAEI,GA2BT,SAAgB,KAAK,QAAuB,EAAE,CAAE,CAC9C,GAAM,CACJ,OAAS,GACT,QAAU,gBACV,eACA,eACA,YAAc,GACd,OAAS,MACT,UAAY,IACV,QAEJ,OAAQ,IAAc,IAAe,OAAuB,CAC1D,IAAM,cAAgB,IAAI,QAAQ,OAsBlC,GAnBI,SAAW,KAAO,CAAC,YACrB,IAAI,UAAU,8BAA+B,IAAI,CACxC,eACL,gBAAgB,cAAe,OAAO,GACxC,IAAI,UAAU,8BAA+B,cAAc,CAE3D,IAAI,UAAU,OAAQ,SAAS,EAI/B,aACF,IAAI,UAAU,mCAAoC,OAAO,CAGvD,gBAAgB,QAClB,IAAI,UAAU,gCAAiC,eAAe,KAAK,KAAK,CAAC,CAIvE,WAAa,IAAI,SAAW,UAAW,CACzC,IAAI,UAAU,+BAAgC,QAAQ,KAAK,KAAK,CAAC,CAEjE,IAAM,WAAa,gBAAkB,IAAI,QAAQ,kCACjD,GAAI,WAAY,CACd,IAAM,MAAQ,MAAM,QAAQ,WAAW,CAAG,WAAW,KAAK,KAAK,CAAG,WAClE,IAAI,UAAU,+BAAgC,MAAM,CAGlD,QACF,IAAI,UAAU,yBAA0B,OAAO,OAAO,CAAC,CAGzD,IAAI,WAAa,IACjB,IAAI,UAAU,iBAAkB,IAAI,CACpC,IAAI,KAAK,CACT,OAGF,MAAM"}
1
+ {"version":3,"file":"cors.mjs","names":[],"sources":["../../../src/http/middleware/cors.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface CorsOptions {\n /** Allowed origin(s). `true` reflects request origin, `'*'` allows all, `false` rejects all. Default: `false` (restrictive) */\n origin?: boolean | string | RegExp | (string | RegExp)[]\n /** Allowed HTTP methods. Default: `['GET','HEAD','PUT','PATCH','POST','DELETE']` */\n methods?: string[]\n /** Allowed request headers. Default: reflects `Access-Control-Request-Headers` */\n allowedHeaders?: string[]\n /** Headers exposed to the browser. Default: none */\n exposedHeaders?: string[]\n /** Allow credentials (cookies, authorization). Default: false */\n credentials?: boolean\n /** Preflight cache duration in seconds. Default: 86400 (24h) */\n maxAge?: number\n /** Handle preflight OPTIONS requests. Default: true */\n preflight?: boolean\n}\n\nconst DEFAULT_METHODS = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']\n\nfunction isOriginAllowed(requestOrigin: string, allowed: CorsOptions['origin']): boolean {\n if (allowed === true || allowed === '*') return true\n if (typeof allowed === 'string') return requestOrigin === allowed\n if (allowed instanceof RegExp) return allowed.test(requestOrigin)\n if (Array.isArray(allowed)) {\n return allowed.some((pattern) =>\n typeof pattern === 'string' ? requestOrigin === pattern : pattern.test(requestOrigin),\n )\n }\n return false\n}\n\n/**\n * CORS middleware with correct spec behavior.\n *\n * - Reflects origin from allowlist (sets `Vary: Origin`)\n * - Handles preflight `OPTIONS` with `204 No Content`\n * - Supports string, regex, and array origin matching\n *\n * @example\n * ```ts\n * // Allow all origins\n * bootstrap({ middleware: [cors(), express.json()] })\n *\n * // Allowlist specific origins\n * bootstrap({\n * middleware: [\n * cors({\n * origin: ['https://app.example.com', /\\.example\\.com$/],\n * credentials: true,\n * }),\n * express.json(),\n * ],\n * })\n * ```\n */\nexport function cors(options: CorsOptions = {}) {\n const {\n origin = false,\n methods = DEFAULT_METHODS,\n allowedHeaders,\n exposedHeaders,\n credentials = false,\n maxAge = 86400,\n preflight = true,\n } = options\n\n return (req: Request, res: Response, next: NextFunction) => {\n const requestOrigin = req.headers.origin\n\n // Determine the Access-Control-Allow-Origin value\n if (origin === '*' && !credentials) {\n res.setHeader('Access-Control-Allow-Origin', '*')\n } else if (requestOrigin) {\n if (isOriginAllowed(requestOrigin, origin)) {\n res.setHeader('Access-Control-Allow-Origin', requestOrigin)\n // Must vary by Origin when reflecting — CDN/proxy correctness\n res.setHeader('Vary', 'Origin')\n }\n }\n\n if (credentials) {\n res.setHeader('Access-Control-Allow-Credentials', 'true')\n }\n\n if (exposedHeaders?.length) {\n res.setHeader('Access-Control-Expose-Headers', exposedHeaders.join(', '))\n }\n\n // Preflight handling\n if (preflight && req.method === 'OPTIONS') {\n res.setHeader('Access-Control-Allow-Methods', methods.join(', '))\n\n const reqHeaders = allowedHeaders ?? req.headers['access-control-request-headers']\n if (reqHeaders) {\n const value = Array.isArray(reqHeaders) ? reqHeaders.join(', ') : reqHeaders\n res.setHeader('Access-Control-Allow-Headers', value)\n }\n\n if (maxAge) {\n res.setHeader('Access-Control-Max-Age', String(maxAge))\n }\n\n res.statusCode = 204\n res.setHeader('Content-Length', '0')\n res.end()\n return\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAM,gBAAkB,CAAC,MAAO,OAAQ,MAAO,QAAS,OAAQ,QAAQ,EAExE,SAAS,gBAAgB,cAAuB,QAAyC,CASvF,OARI,UAAY,IAAQ,UAAY,IAAY,GAC5C,OAAO,SAAY,SAAiB,gBAAkB,QACtD,mBAAmB,OAAe,QAAQ,KAAK,aAAa,EAC5D,MAAM,QAAQ,OAAO,EAChB,QAAQ,KAAM,SACnB,OAAO,SAAY,SAAW,gBAAkB,QAAU,QAAQ,KAAK,aAAa,CACtF,EAEK,EACT,CA0BA,SAAgB,KAAK,QAAuB,CAAC,EAAG,CAC9C,GAAM,CACJ,OAAS,GACT,QAAU,gBACV,eACA,eACA,YAAc,GACd,OAAS,MACT,UAAY,IACV,QAEJ,OAAQ,IAAc,IAAe,OAAuB,CAC1D,IAAM,cAAgB,IAAI,QAAQ,OAsBlC,GAnBI,SAAW,KAAO,CAAC,YACrB,IAAI,UAAU,8BAA+B,GAAG,EACvC,eACL,gBAAgB,cAAe,MAAM,IACvC,IAAI,UAAU,8BAA+B,aAAa,EAE1D,IAAI,UAAU,OAAQ,QAAQ,GAI9B,aACF,IAAI,UAAU,mCAAoC,MAAM,EAGtD,gBAAgB,QAClB,IAAI,UAAU,gCAAiC,eAAe,KAAK,IAAI,CAAC,EAItE,WAAa,IAAI,SAAW,UAAW,CACzC,IAAI,UAAU,+BAAgC,QAAQ,KAAK,IAAI,CAAC,EAEhE,IAAM,WAAa,gBAAkB,IAAI,QAAQ,kCACjD,GAAI,WAAY,CACd,IAAM,MAAQ,MAAM,QAAQ,UAAU,EAAI,WAAW,KAAK,IAAI,EAAI,WAClE,IAAI,UAAU,+BAAgC,KAAK,CACrD,CAEI,QACF,IAAI,UAAU,yBAA0B,OAAO,MAAM,CAAC,EAGxD,IAAI,WAAa,IACjB,IAAI,UAAU,iBAAkB,GAAG,EACnC,IAAI,IAAI,EACR,MACF,CAEA,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"csrf.mjs","names":[],"sources":["../../../src/http/middleware/csrf.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport interface CsrfOptions {\n /** Cookie name for the CSRF token (default: '_csrf') */\n cookie?: string\n /** Header name to check for the token (default: 'x-csrf-token') */\n header?: string\n /** HTTP methods that require CSRF validation (default: POST, PUT, PATCH, DELETE) */\n methods?: string[]\n /** Paths to exclude from CSRF checks (e.g. webhooks) */\n ignorePaths?: string[]\n /** Token byte length before hex encoding (default: 32 = 64 hex chars) */\n tokenLength?: number\n /** Cookie options */\n cookieOptions?: {\n httpOnly?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n secure?: boolean\n path?: string\n }\n}\n\n/**\n * Double-submit cookie CSRF protection middleware.\n *\n * On every request, sets a CSRF token cookie. For state-changing methods\n * (POST, PUT, PATCH, DELETE), validates that the request header matches\n * the cookie value.\n *\n * @example\n * ```ts\n * import { csrf } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * cookieParser(),\n * csrf(),\n * // ... other middleware\n * ],\n * })\n * ```\n *\n * Client usage:\n * 1. Read the `_csrf` cookie value\n * 2. Send it in the `x-csrf-token` header on every mutating request\n */\nexport function csrf(options: CsrfOptions = {}) {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const cookieOpts = {\n httpOnly: true,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Generate or reuse CSRF token\n const cookies = (req as any).cookies || {}\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomBytes(tokenLength).toString('hex')\n res.cookie(cookieName, token, cookieOpts)\n }\n\n // Skip validation for safe methods and ignored paths\n if (!protectedMethods.has(req.method.toUpperCase())) {\n return next()\n }\n\n if (ignorePaths.has(req.path)) {\n return next()\n }\n\n // Validate: header token must match cookie token\n const headerToken = req.headers[headerName] as string | undefined\n\n if (!headerToken || headerToken !== token) {\n return res.status(403).json({\n message: 'CSRF token mismatch',\n })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;qCAgDA,SAAgB,KAAK,QAAuB,EAAE,CAAE,CAC9C,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,SAAS,EAAE,IAAK,GAAM,EAAE,aAAa,CAAC,CACpF,CACK,YAAc,IAAI,IAAI,QAAQ,aAAe,EAAE,CAAC,CAChD,YAAc,QAAQ,aAAe,GACrC,WAAa,CACjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,cACZ,CAED,OAAQ,IAAc,IAAe,OAAuB,CAG1D,IAAI,OADa,IAAY,SAAW,EAAE,EACtB,YAYpB,GAVK,QACH,MAAQ,YAAY,YAAY,CAAC,SAAS,MAAM,CAChD,IAAI,OAAO,WAAY,MAAO,WAAW,EAIvC,CAAC,iBAAiB,IAAI,IAAI,OAAO,aAAa,CAAC,EAI/C,YAAY,IAAI,IAAI,KAAK,CAC3B,OAAO,MAAM,CAIf,IAAM,YAAc,IAAI,QAAQ,YAEhC,GAAI,CAAC,aAAe,cAAgB,MAClC,OAAO,IAAI,OAAO,IAAI,CAAC,KAAK,CAC1B,QAAS,sBACV,CAAC,CAGJ,MAAM"}
1
+ {"version":3,"file":"csrf.mjs","names":[],"sources":["../../../src/http/middleware/csrf.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport interface CsrfOptions {\n /** Cookie name for the CSRF token (default: '_csrf') */\n cookie?: string\n /** Header name to check for the token (default: 'x-csrf-token') */\n header?: string\n /** HTTP methods that require CSRF validation (default: POST, PUT, PATCH, DELETE) */\n methods?: string[]\n /** Paths to exclude from CSRF checks (e.g. webhooks) */\n ignorePaths?: string[]\n /** Token byte length before hex encoding (default: 32 = 64 hex chars) */\n tokenLength?: number\n /** Cookie options */\n cookieOptions?: {\n httpOnly?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n secure?: boolean\n path?: string\n }\n}\n\n/**\n * Double-submit cookie CSRF protection middleware.\n *\n * On every request, sets a CSRF token cookie. For state-changing methods\n * (POST, PUT, PATCH, DELETE), validates that the request header matches\n * the cookie value.\n *\n * @example\n * ```ts\n * import { csrf } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * cookieParser(),\n * csrf(),\n * // ... other middleware\n * ],\n * })\n * ```\n *\n * Client usage:\n * 1. Read the `_csrf` cookie value\n * 2. Send it in the `x-csrf-token` header on every mutating request\n */\nexport function csrf(options: CsrfOptions = {}) {\n const cookieName = options.cookie ?? '_csrf'\n const headerName = options.header ?? 'x-csrf-token'\n const protectedMethods = new Set(\n (options.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE']).map((m) => m.toUpperCase()),\n )\n const ignorePaths = new Set(options.ignorePaths ?? [])\n const tokenLength = options.tokenLength ?? 32\n const cookieOpts = {\n httpOnly: true,\n sameSite: 'strict' as const,\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n ...options.cookieOptions,\n }\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Generate or reuse CSRF token\n const cookies = (req as any).cookies || {}\n let token = cookies[cookieName]\n\n if (!token) {\n token = randomBytes(tokenLength).toString('hex')\n res.cookie(cookieName, token, cookieOpts)\n }\n\n // Skip validation for safe methods and ignored paths\n if (!protectedMethods.has(req.method.toUpperCase())) {\n return next()\n }\n\n if (ignorePaths.has(req.path)) {\n return next()\n }\n\n // Validate: header token must match cookie token\n const headerToken = req.headers[headerName] as string | undefined\n\n if (!headerToken || headerToken !== token) {\n return res.status(403).json({\n message: 'CSRF token mismatch',\n })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;qCAgDA,SAAgB,KAAK,QAAuB,CAAC,EAAG,CAC9C,IAAM,WAAa,QAAQ,QAAU,QAC/B,WAAa,QAAQ,QAAU,eAC/B,iBAAmB,IAAI,KAC1B,QAAQ,SAAW,CAAC,OAAQ,MAAO,QAAS,QAAQ,GAAG,IAAK,GAAM,EAAE,YAAY,CAAC,CACpF,EACM,YAAc,IAAI,IAAI,QAAQ,aAAe,CAAC,CAAC,EAC/C,YAAc,QAAQ,aAAe,GACrC,WAAa,CACjB,SAAU,GACV,SAAU,SACV,OAAQ,QAAQ,IAAI,WAAa,aACjC,KAAM,IACN,GAAG,QAAQ,aACb,EAEA,OAAQ,IAAc,IAAe,OAAuB,CAG1D,IAAI,OADa,IAAY,SAAW,CAAC,GACrB,YAYpB,GAVK,QACH,MAAQ,YAAY,WAAW,EAAE,SAAS,KAAK,EAC/C,IAAI,OAAO,WAAY,MAAO,UAAU,GAItC,CAAC,iBAAiB,IAAI,IAAI,OAAO,YAAY,CAAC,GAI9C,YAAY,IAAI,IAAI,IAAI,EAC1B,OAAO,KAAK,EAId,IAAM,YAAc,IAAI,QAAQ,YAEhC,GAAI,CAAC,aAAe,cAAgB,MAClC,OAAO,IAAI,OAAO,GAAG,EAAE,KAAK,CAC1B,QAAS,qBACX,CAAC,EAGH,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.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-CJKmg8QR.mjs";import{HttpException}from"../../core/errors.mjs";const log=createLogger(`ErrorHandler`);function notFoundHandler(){return(_req,res,_next)=>{res.status(404).json({message:`Not Found`})}}function errorHandler(){let isProduction=process.env.NODE_ENV===`production`;return(err,req,res,_next)=>{if(res.headersSent){log.warn(`Error after headers sent: ${err?.message||`Unknown`}`);return}if(err?.name===`ZodError`){let firstIssue=err.issues?.[0];return res.status(422).json({message:firstIssue?.message||`Validation failed`,...isProduction?{}:{errors:err.issues}})}if(err instanceof HttpException){if(err.status>=500&&log.error(err,err.message),err.headers)for(let[k,v]of Object.entries(err.headers))res.setHeader(k,v);let exposeDetails=!isProduction&&err.details!==void 0;return res.status(err.status).json({message:err.message,...exposeDetails?{errors:err.details}:{}})}let status=err.status||err.statusCode||500;log.error(err,`${req.method} ${req.originalUrl} — ${err.message||`Unhandled error`}`);let message=status===500?`Internal Server Error`:err.message||`Error`;res.status(status).json({message})}}export{errorHandler,notFoundHandler};
11
+ import{createLogger}from"../../core/logger.mjs";import"../../core-BQHQsimv.mjs";import{HttpException}from"../../core/errors.mjs";const log=createLogger(`ErrorHandler`);function notFoundHandler(){return(_req,res,_next)=>{res.status(404).json({message:`Not Found`})}}function errorHandler(){let isProduction=process.env.NODE_ENV===`production`;return(err,req,res,_next)=>{if(res.headersSent){log.warn(`Error after headers sent: ${err?.message||`Unknown`}`);return}if(err?.name===`ZodError`){let firstIssue=err.issues?.[0];return res.status(422).json({message:firstIssue?.message||`Validation failed`,...isProduction?{}:{errors:err.issues}})}if(err instanceof HttpException){if(err.status>=500&&log.error(err,err.message),err.headers)for(let[k,v]of Object.entries(err.headers))res.setHeader(k,v);let exposeDetails=!isProduction&&err.details!==void 0;return res.status(err.status).json({message:err.message,...exposeDetails?{errors:err.details}:{}})}let status=err.status||err.statusCode||500;log.error(err,`${req.method} ${req.originalUrl} — ${err.message||`Unhandled error`}`);let message=status===500?`Internal Server Error`:err.message||`Error`;res.status(status).json({message})}}export{errorHandler,notFoundHandler};
12
12
  //# sourceMappingURL=error-handler.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-handler.mjs","names":[],"sources":["../../../src/http/middleware/error-handler.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { HttpException, createLogger } from '../../core'\n\nconst log = createLogger('ErrorHandler')\n\n/** Catch-all for unmatched routes */\nexport function notFoundHandler() {\n return (_req: Request, res: Response, _next: NextFunction) => {\n res.status(404).json({ message: 'Not Found' })\n }\n}\n\n/**\n * Global error handler.\n *\n * Default behavior strips error `details` from the response in production\n * (NODE_ENV === 'production') so internal context (DB errors, validation\n * issues, custom payloads) does not leak to clients. Apps that want to\n * expose details in production — for client-facing field-level validation,\n * for example — should pass their own `onError` to `bootstrap()` and decide\n * the policy explicitly.\n */\nexport function errorHandler() {\n const isProduction = process.env.NODE_ENV === 'production'\n return (err: any, req: Request, res: Response, _next: NextFunction) => {\n // Don't write after headers are already sent\n if (res.headersSent) {\n log.warn(`Error after headers sent: ${err?.message || 'Unknown'}`)\n return\n }\n\n // Zod validation errors\n if (err?.name === 'ZodError') {\n const firstIssue = err.issues?.[0]\n return res.status(422).json({\n message: firstIssue?.message || 'Validation failed',\n ...(isProduction ? {} : { errors: err.issues }),\n })\n }\n\n // HttpException (expected application errors)\n if (err instanceof HttpException) {\n if (err.status >= 500) {\n log.error(err, err.message)\n }\n if (err.headers) {\n for (const [k, v] of Object.entries(err.headers)) {\n res.setHeader(k, v)\n }\n }\n const exposeDetails = !isProduction && err.details !== undefined\n return res.status(err.status).json({\n message: err.message,\n ...(exposeDetails ? { errors: err.details } : {}),\n })\n }\n\n // Unexpected errors — always log\n const status = err.status || err.statusCode || 500\n log.error(err, `${req.method} ${req.originalUrl} — ${err.message || 'Unhandled error'}`)\n const message = status === 500 ? 'Internal Server Error' : err.message || 'Error'\n res.status(status).json({ message })\n }\n}\n"],"mappings":";;;;;;;;;;iIAGA,MAAM,IAAM,aAAa,eAAe,CAGxC,SAAgB,iBAAkB,CAChC,OAAQ,KAAe,IAAe,QAAwB,CAC5D,IAAI,OAAO,IAAI,CAAC,KAAK,CAAE,QAAS,YAAa,CAAC,EAclD,SAAgB,cAAe,CAC7B,IAAM,aAAe,QAAQ,IAAI,WAAa,aAC9C,OAAQ,IAAU,IAAc,IAAe,QAAwB,CAErE,GAAI,IAAI,YAAa,CACnB,IAAI,KAAK,6BAA6B,KAAK,SAAW,YAAY,CAClE,OAIF,GAAI,KAAK,OAAS,WAAY,CAC5B,IAAM,WAAa,IAAI,SAAS,GAChC,OAAO,IAAI,OAAO,IAAI,CAAC,KAAK,CAC1B,QAAS,YAAY,SAAW,oBAChC,GAAI,aAAe,EAAE,CAAG,CAAE,OAAQ,IAAI,OAAQ,CAC/C,CAAC,CAIJ,GAAI,eAAe,cAAe,CAIhC,GAHI,IAAI,QAAU,KAChB,IAAI,MAAM,IAAK,IAAI,QAAQ,CAEzB,IAAI,QACN,IAAK,GAAM,CAAC,EAAG,KAAM,OAAO,QAAQ,IAAI,QAAQ,CAC9C,IAAI,UAAU,EAAG,EAAE,CAGvB,IAAM,cAAgB,CAAC,cAAgB,IAAI,UAAY,IAAA,GACvD,OAAO,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK,CACjC,QAAS,IAAI,QACb,GAAI,cAAgB,CAAE,OAAQ,IAAI,QAAS,CAAG,EAAE,CACjD,CAAC,CAIJ,IAAM,OAAS,IAAI,QAAU,IAAI,YAAc,IAC/C,IAAI,MAAM,IAAK,GAAG,IAAI,OAAO,GAAG,IAAI,YAAY,KAAK,IAAI,SAAW,oBAAoB,CACxF,IAAM,QAAU,SAAW,IAAM,wBAA0B,IAAI,SAAW,QAC1E,IAAI,OAAO,OAAO,CAAC,KAAK,CAAE,QAAS,CAAC"}
1
+ {"version":3,"file":"error-handler.mjs","names":[],"sources":["../../../src/http/middleware/error-handler.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { HttpException, createLogger } from '../../core'\n\nconst log = createLogger('ErrorHandler')\n\n/** Catch-all for unmatched routes */\nexport function notFoundHandler() {\n return (_req: Request, res: Response, _next: NextFunction) => {\n res.status(404).json({ message: 'Not Found' })\n }\n}\n\n/**\n * Global error handler.\n *\n * Default behavior strips error `details` from the response in production\n * (NODE_ENV === 'production') so internal context (DB errors, validation\n * issues, custom payloads) does not leak to clients. Apps that want to\n * expose details in production — for client-facing field-level validation,\n * for example — should pass their own `onError` to `bootstrap()` and decide\n * the policy explicitly.\n */\nexport function errorHandler() {\n const isProduction = process.env.NODE_ENV === 'production'\n return (err: any, req: Request, res: Response, _next: NextFunction) => {\n // Don't write after headers are already sent\n if (res.headersSent) {\n log.warn(`Error after headers sent: ${err?.message || 'Unknown'}`)\n return\n }\n\n // Zod validation errors\n if (err?.name === 'ZodError') {\n const firstIssue = err.issues?.[0]\n return res.status(422).json({\n message: firstIssue?.message || 'Validation failed',\n ...(isProduction ? {} : { errors: err.issues }),\n })\n }\n\n // HttpException (expected application errors)\n if (err instanceof HttpException) {\n if (err.status >= 500) {\n log.error(err, err.message)\n }\n if (err.headers) {\n for (const [k, v] of Object.entries(err.headers)) {\n res.setHeader(k, v)\n }\n }\n const exposeDetails = !isProduction && err.details !== undefined\n return res.status(err.status).json({\n message: err.message,\n ...(exposeDetails ? { errors: err.details } : {}),\n })\n }\n\n // Unexpected errors — always log\n const status = err.status || err.statusCode || 500\n log.error(err, `${req.method} ${req.originalUrl} — ${err.message || 'Unhandled error'}`)\n const message = status === 500 ? 'Internal Server Error' : err.message || 'Error'\n res.status(status).json({ message })\n }\n}\n"],"mappings":";;;;;;;;;;iIAGA,MAAM,IAAM,aAAa,cAAc,EAGvC,SAAgB,iBAAkB,CAChC,OAAQ,KAAe,IAAe,QAAwB,CAC5D,IAAI,OAAO,GAAG,EAAE,KAAK,CAAE,QAAS,WAAY,CAAC,CAC/C,CACF,CAYA,SAAgB,cAAe,CAC7B,IAAM,aAAe,QAAQ,IAAI,WAAa,aAC9C,OAAQ,IAAU,IAAc,IAAe,QAAwB,CAErE,GAAI,IAAI,YAAa,CACnB,IAAI,KAAK,6BAA6B,KAAK,SAAW,WAAW,EACjE,MACF,CAGA,GAAI,KAAK,OAAS,WAAY,CAC5B,IAAM,WAAa,IAAI,SAAS,GAChC,OAAO,IAAI,OAAO,GAAG,EAAE,KAAK,CAC1B,QAAS,YAAY,SAAW,oBAChC,GAAI,aAAe,CAAC,EAAI,CAAE,OAAQ,IAAI,MAAO,CAC/C,CAAC,CACH,CAGA,GAAI,eAAe,cAAe,CAIhC,GAHI,IAAI,QAAU,KAChB,IAAI,MAAM,IAAK,IAAI,OAAO,EAExB,IAAI,QACN,IAAK,GAAM,CAAC,EAAG,KAAM,OAAO,QAAQ,IAAI,OAAO,EAC7C,IAAI,UAAU,EAAG,CAAC,EAGtB,IAAM,cAAgB,CAAC,cAAgB,IAAI,UAAY,IAAA,GACvD,OAAO,IAAI,OAAO,IAAI,MAAM,EAAE,KAAK,CACjC,QAAS,IAAI,QACb,GAAI,cAAgB,CAAE,OAAQ,IAAI,OAAQ,EAAI,CAAC,CACjD,CAAC,CACH,CAGA,IAAM,OAAS,IAAI,QAAU,IAAI,YAAc,IAC/C,IAAI,MAAM,IAAK,GAAG,IAAI,OAAO,GAAG,IAAI,YAAY,KAAK,IAAI,SAAW,mBAAmB,EACvF,IAAM,QAAU,SAAW,IAAM,wBAA0B,IAAI,SAAW,QAC1E,IAAI,OAAO,MAAM,EAAE,KAAK,CAAE,OAAQ,CAAC,CACrC,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"helmet.mjs","names":[],"sources":["../../../src/http/middleware/helmet.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface HelmetOptions {\n /** Set Content-Security-Policy header (default: self-only policy) */\n contentSecurityPolicy?: boolean | Record<string, string[]>\n /** Set X-Content-Type-Options: nosniff (default: true) */\n noSniff?: boolean\n /** Set X-Frame-Options (default: 'DENY') */\n frameguard?: false | 'DENY' | 'SAMEORIGIN'\n /** Set Strict-Transport-Security (default: max-age=31536000; includeSubDomains) */\n hsts?: false | { maxAge?: number; includeSubDomains?: boolean; preload?: boolean }\n /** Set X-DNS-Prefetch-Control (default: off) */\n dnsPrefetch?: boolean\n /** Remove X-Powered-By header (default: true) */\n hidePoweredBy?: boolean\n /** Set Referrer-Policy (default: 'no-referrer') */\n referrerPolicy?: false | string\n /** Set X-XSS-Protection: 0 (default: true — disables legacy XSS auditor) */\n xssFilter?: boolean\n}\n\n/**\n * Security headers middleware. Lightweight alternative to the `helmet` npm package\n * with sensible defaults for API servers.\n *\n * @example\n * ```ts\n * bootstrap({\n * middleware: [helmet(), requestId(), express.json()],\n * })\n * ```\n */\nexport function helmet(options: HelmetOptions = {}) {\n const {\n noSniff = true,\n frameguard = 'DENY',\n hsts = { maxAge: 31536000, includeSubDomains: true },\n dnsPrefetch = false,\n hidePoweredBy = true,\n referrerPolicy = 'no-referrer',\n xssFilter = true,\n contentSecurityPolicy = false,\n } = options\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (hidePoweredBy) res.removeHeader('X-Powered-By')\n if (noSniff) res.setHeader('X-Content-Type-Options', 'nosniff')\n if (frameguard) res.setHeader('X-Frame-Options', frameguard)\n if (xssFilter) res.setHeader('X-XSS-Protection', '0')\n if (referrerPolicy) res.setHeader('Referrer-Policy', referrerPolicy)\n if (!dnsPrefetch) res.setHeader('X-DNS-Prefetch-Control', 'off')\n\n if (hsts) {\n const maxAge = hsts.maxAge ?? 31536000\n let value = `max-age=${maxAge}`\n if (hsts.includeSubDomains) value += '; includeSubDomains'\n if (hsts.preload) value += '; preload'\n res.setHeader('Strict-Transport-Security', value)\n }\n\n if (contentSecurityPolicy) {\n const policy =\n typeof contentSecurityPolicy === 'object'\n ? Object.entries(contentSecurityPolicy)\n .map(([key, values]) => `${key} ${values.join(' ')}`)\n .join('; ')\n : \"default-src 'self'\"\n res.setHeader('Content-Security-Policy', policy)\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAgCA,SAAgB,OAAO,QAAyB,EAAE,CAAE,CAClD,GAAM,CACJ,QAAU,GACV,WAAa,OACb,KAAO,CAAE,OAAQ,QAAU,kBAAmB,GAAM,CACpD,YAAc,GACd,cAAgB,GAChB,eAAiB,cACjB,UAAY,GACZ,sBAAwB,IACtB,QAEJ,OAAQ,IAAc,IAAe,OAAuB,CAQ1D,GAPI,eAAe,IAAI,aAAa,eAAe,CAC/C,SAAS,IAAI,UAAU,yBAA0B,UAAU,CAC3D,YAAY,IAAI,UAAU,kBAAmB,WAAW,CACxD,WAAW,IAAI,UAAU,mBAAoB,IAAI,CACjD,gBAAgB,IAAI,UAAU,kBAAmB,eAAe,CAC/D,aAAa,IAAI,UAAU,yBAA0B,MAAM,CAE5D,KAAM,CAER,IAAI,MAAQ,WADG,KAAK,QAAU,UAE1B,KAAK,oBAAmB,OAAS,uBACjC,KAAK,UAAS,OAAS,aAC3B,IAAI,UAAU,4BAA6B,MAAM,CAGnD,GAAI,sBAAuB,CACzB,IAAM,OACJ,OAAO,uBAA0B,SAC7B,OAAO,QAAQ,sBAAsB,CAClC,KAAK,CAAC,IAAK,UAAY,GAAG,IAAI,GAAG,OAAO,KAAK,IAAI,GAAG,CACpD,KAAK,KAAK,CACb,qBACN,IAAI,UAAU,0BAA2B,OAAO,CAGlD,MAAM"}
1
+ {"version":3,"file":"helmet.mjs","names":[],"sources":["../../../src/http/middleware/helmet.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface HelmetOptions {\n /** Set Content-Security-Policy header (default: self-only policy) */\n contentSecurityPolicy?: boolean | Record<string, string[]>\n /** Set X-Content-Type-Options: nosniff (default: true) */\n noSniff?: boolean\n /** Set X-Frame-Options (default: 'DENY') */\n frameguard?: false | 'DENY' | 'SAMEORIGIN'\n /** Set Strict-Transport-Security (default: max-age=31536000; includeSubDomains) */\n hsts?: false | { maxAge?: number; includeSubDomains?: boolean; preload?: boolean }\n /** Set X-DNS-Prefetch-Control (default: off) */\n dnsPrefetch?: boolean\n /** Remove X-Powered-By header (default: true) */\n hidePoweredBy?: boolean\n /** Set Referrer-Policy (default: 'no-referrer') */\n referrerPolicy?: false | string\n /** Set X-XSS-Protection: 0 (default: true — disables legacy XSS auditor) */\n xssFilter?: boolean\n}\n\n/**\n * Security headers middleware. Lightweight alternative to the `helmet` npm package\n * with sensible defaults for API servers.\n *\n * @example\n * ```ts\n * bootstrap({\n * middleware: [helmet(), requestId(), express.json()],\n * })\n * ```\n */\nexport function helmet(options: HelmetOptions = {}) {\n const {\n noSniff = true,\n frameguard = 'DENY',\n hsts = { maxAge: 31536000, includeSubDomains: true },\n dnsPrefetch = false,\n hidePoweredBy = true,\n referrerPolicy = 'no-referrer',\n xssFilter = true,\n contentSecurityPolicy = false,\n } = options\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (hidePoweredBy) res.removeHeader('X-Powered-By')\n if (noSniff) res.setHeader('X-Content-Type-Options', 'nosniff')\n if (frameguard) res.setHeader('X-Frame-Options', frameguard)\n if (xssFilter) res.setHeader('X-XSS-Protection', '0')\n if (referrerPolicy) res.setHeader('Referrer-Policy', referrerPolicy)\n if (!dnsPrefetch) res.setHeader('X-DNS-Prefetch-Control', 'off')\n\n if (hsts) {\n const maxAge = hsts.maxAge ?? 31536000\n let value = `max-age=${maxAge}`\n if (hsts.includeSubDomains) value += '; includeSubDomains'\n if (hsts.preload) value += '; preload'\n res.setHeader('Strict-Transport-Security', value)\n }\n\n if (contentSecurityPolicy) {\n const policy =\n typeof contentSecurityPolicy === 'object'\n ? Object.entries(contentSecurityPolicy)\n .map(([key, values]) => `${key} ${values.join(' ')}`)\n .join('; ')\n : \"default-src 'self'\"\n res.setHeader('Content-Security-Policy', policy)\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAgCA,SAAgB,OAAO,QAAyB,CAAC,EAAG,CAClD,GAAM,CACJ,QAAU,GACV,WAAa,OACb,KAAO,CAAE,OAAQ,QAAU,kBAAmB,EAAK,EACnD,YAAc,GACd,cAAgB,GAChB,eAAiB,cACjB,UAAY,GACZ,sBAAwB,IACtB,QAEJ,OAAQ,IAAc,IAAe,OAAuB,CAQ1D,GAPI,eAAe,IAAI,aAAa,cAAc,EAC9C,SAAS,IAAI,UAAU,yBAA0B,SAAS,EAC1D,YAAY,IAAI,UAAU,kBAAmB,UAAU,EACvD,WAAW,IAAI,UAAU,mBAAoB,GAAG,EAChD,gBAAgB,IAAI,UAAU,kBAAmB,cAAc,EAC9D,aAAa,IAAI,UAAU,yBAA0B,KAAK,EAE3D,KAAM,CAER,IAAI,MAAQ,WADG,KAAK,QAAU,UAE1B,KAAK,oBAAmB,OAAS,uBACjC,KAAK,UAAS,OAAS,aAC3B,IAAI,UAAU,4BAA6B,KAAK,CAClD,CAEA,GAAI,sBAAuB,CACzB,IAAM,OACJ,OAAO,uBAA0B,SAC7B,OAAO,QAAQ,qBAAqB,EACjC,KAAK,CAAC,IAAK,UAAY,GAAG,IAAI,GAAG,OAAO,KAAK,GAAG,GAAG,EACnD,KAAK,IAAI,EACZ,qBACN,IAAI,UAAU,0BAA2B,MAAM,CACjD,CAEA,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- var MemoryStore=class{hits=new Map;cleanupTimer;constructor(windowMs){this.windowMs=windowMs,this.cleanupTimer=setInterval(()=>this.cleanup(),windowMs),this.cleanupTimer.unref&&this.cleanupTimer.unref()}async increment(key){let now=Date.now(),entry=this.hits.get(key);if(entry&&entry.resetTime.getTime()>now)return entry.totalHits++,{totalHits:entry.totalHits,resetTime:entry.resetTime};let resetTime=new Date(now+this.windowMs),newEntry={totalHits:1,resetTime};return this.hits.set(key,newEntry),{totalHits:1,resetTime}}async decrement(key){let entry=this.hits.get(key);entry&&entry.totalHits>0&&entry.totalHits--}async reset(key){this.hits.delete(key)}cleanup(){let now=Date.now();for(let[key,entry]of this.hits)entry.resetTime.getTime()<=now&&this.hits.delete(key)}};function rateLimit(options={}){let max=options.max??100,windowMs=options.windowMs??6e4,message=options.message??`Too Many Requests`,statusCode=options.statusCode??429,keyGenerator=options.keyGenerator??(req=>req.ip??`127.0.0.1`),sendHeaders=options.headers??!0,store=options.store??new MemoryStore(windowMs),skip=options.skip,skipPaths=new Set(options.skipPaths??[]);return async(req,res,next)=>{if(skipPaths.has(req.path)||skip&&skip(req))return next();let key=keyGenerator(req),{totalHits,resetTime}=await store.increment(key),remaining=Math.max(0,max-totalHits);if(sendHeaders&&(res.setHeader(`RateLimit-Limit`,max),res.setHeader(`RateLimit-Remaining`,remaining),res.setHeader(`RateLimit-Reset`,Math.ceil(resetTime.getTime()/1e3))),totalHits>max)return res.status(statusCode).json({message});next()}}export{rateLimit};
11
+ var MemoryStore=class{windowMs;hits=new Map;cleanupTimer;constructor(windowMs){this.windowMs=windowMs,this.cleanupTimer=setInterval(()=>this.cleanup(),windowMs),this.cleanupTimer.unref&&this.cleanupTimer.unref()}async increment(key){let now=Date.now(),entry=this.hits.get(key);if(entry&&entry.resetTime.getTime()>now)return entry.totalHits++,{totalHits:entry.totalHits,resetTime:entry.resetTime};let resetTime=new Date(now+this.windowMs),newEntry={totalHits:1,resetTime};return this.hits.set(key,newEntry),{totalHits:1,resetTime}}async decrement(key){let entry=this.hits.get(key);entry&&entry.totalHits>0&&entry.totalHits--}async reset(key){this.hits.delete(key)}cleanup(){let now=Date.now();for(let[key,entry]of this.hits)entry.resetTime.getTime()<=now&&this.hits.delete(key)}};function rateLimit(options={}){let max=options.max??100,windowMs=options.windowMs??6e4,message=options.message??`Too Many Requests`,statusCode=options.statusCode??429,keyGenerator=options.keyGenerator??(req=>req.ip??`127.0.0.1`),sendHeaders=options.headers??!0,store=options.store??new MemoryStore(windowMs),skip=options.skip,skipPaths=new Set(options.skipPaths??[]);return async(req,res,next)=>{if(skipPaths.has(req.path)||skip&&skip(req))return next();let key=keyGenerator(req),{totalHits,resetTime}=await store.increment(key),remaining=Math.max(0,max-totalHits);if(sendHeaders&&(res.setHeader(`RateLimit-Limit`,max),res.setHeader(`RateLimit-Remaining`,remaining),res.setHeader(`RateLimit-Reset`,Math.ceil(resetTime.getTime()/1e3))),totalHits>max)return res.status(statusCode).json({message});next()}}export{rateLimit};
12
12
  //# sourceMappingURL=rate-limit.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../../src/http/middleware/rate-limit.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface RateLimitStore {\n increment(key: string): Promise<{ totalHits: number; resetTime: Date }>\n decrement(key: string): Promise<void>\n reset(key: string): Promise<void>\n}\n\nexport interface RateLimitOptions {\n /** Maximum number of requests per window (default: 100) */\n max?: number\n /** Time window in milliseconds (default: 60_000) */\n windowMs?: number\n /** Response message when rate limit is exceeded (default: 'Too Many Requests') */\n message?: string\n /** HTTP status code when rate limit is exceeded (default: 429) */\n statusCode?: number\n /** Function to generate the key for rate limiting (default: req.ip) */\n keyGenerator?: (req: Request) => string\n /** Whether to send rate limit headers (default: true) */\n headers?: boolean\n /** Custom store implementation (default: in-memory Map) */\n store?: RateLimitStore\n /** Function to skip rate limiting for certain requests */\n skip?: (req: Request) => boolean\n /** Paths to exclude from rate limiting */\n skipPaths?: string[]\n}\n\ninterface MemoryStoreEntry {\n totalHits: number\n resetTime: Date\n}\n\nclass MemoryStore implements RateLimitStore {\n private hits = new Map<string, MemoryStoreEntry>()\n private cleanupTimer: ReturnType<typeof setInterval>\n\n constructor(private windowMs: number) {\n this.cleanupTimer = setInterval(() => this.cleanup(), windowMs)\n // Allow the process to exit without waiting for the timer\n if (this.cleanupTimer.unref) {\n this.cleanupTimer.unref()\n }\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n\n if (entry && entry.resetTime.getTime() > now) {\n entry.totalHits++\n return { totalHits: entry.totalHits, resetTime: entry.resetTime }\n }\n\n const resetTime = new Date(now + this.windowMs)\n const newEntry: MemoryStoreEntry = { totalHits: 1, resetTime }\n this.hits.set(key, newEntry)\n return { totalHits: 1, resetTime }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.totalHits > 0) {\n entry.totalHits--\n }\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n\n private cleanup(): void {\n const now = Date.now()\n for (const [key, entry] of this.hits) {\n if (entry.resetTime.getTime() <= now) {\n this.hits.delete(key)\n }\n }\n }\n}\n\n/**\n * Rate limiting middleware.\n *\n * Limits the number of requests a client can make within a time window.\n * Uses an in-memory store by default, but accepts a custom store for\n * distributed deployments (e.g. Redis).\n *\n * @example\n * ```ts\n * import { rateLimit } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * rateLimit({ max: 100, windowMs: 60_000 }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function rateLimit(options: RateLimitOptions = {}) {\n const max = options.max ?? 100\n const windowMs = options.windowMs ?? 60_000\n const message = options.message ?? 'Too Many Requests'\n const statusCode = options.statusCode ?? 429\n const keyGenerator = options.keyGenerator ?? ((req: Request) => req.ip ?? '127.0.0.1')\n const sendHeaders = options.headers ?? true\n const store = options.store ?? new MemoryStore(windowMs)\n const skip = options.skip\n const skipPaths = new Set(options.skipPaths ?? [])\n\n return async (req: Request, res: Response, next: NextFunction) => {\n // Skip if path is in the skip list\n if (skipPaths.has(req.path)) {\n return next()\n }\n\n // Skip if the skip function returns true\n if (skip && skip(req)) {\n return next()\n }\n\n const key = keyGenerator(req)\n const { totalHits, resetTime } = await store.increment(key)\n const remaining = Math.max(0, max - totalHits)\n\n if (sendHeaders) {\n res.setHeader('RateLimit-Limit', max)\n res.setHeader('RateLimit-Remaining', remaining)\n res.setHeader('RateLimit-Reset', Math.ceil(resetTime.getTime() / 1000))\n }\n\n if (totalHits > max) {\n return res.status(statusCode).json({ message })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAkCA,IAAM,YAAN,KAA4C,CAC1C,KAAe,IAAI,IACnB,aAEA,YAAY,SAA0B,CAAlB,KAAA,SAAA,SAClB,KAAK,aAAe,gBAAkB,KAAK,SAAS,CAAE,SAAS,CAE3D,KAAK,aAAa,OACpB,KAAK,aAAa,OAAO,CAI7B,MAAM,UAAU,IAA8D,CAC5E,IAAM,IAAM,KAAK,KAAK,CAChB,MAAQ,KAAK,KAAK,IAAI,IAAI,CAEhC,GAAI,OAAS,MAAM,UAAU,SAAS,CAAG,IAEvC,MADA,OAAM,YACC,CAAE,UAAW,MAAM,UAAW,UAAW,MAAM,UAAW,CAGnE,IAAM,UAAY,IAAI,KAAK,IAAM,KAAK,SAAS,CACzC,SAA6B,CAAE,UAAW,EAAG,UAAW,CAE9D,OADA,KAAK,KAAK,IAAI,IAAK,SAAS,CACrB,CAAE,UAAW,EAAG,UAAW,CAGpC,MAAM,UAAU,IAA4B,CAC1C,IAAM,MAAQ,KAAK,KAAK,IAAI,IAAI,CAC5B,OAAS,MAAM,UAAY,GAC7B,MAAM,YAIV,MAAM,MAAM,IAA4B,CACtC,KAAK,KAAK,OAAO,IAAI,CAGvB,SAAwB,CACtB,IAAM,IAAM,KAAK,KAAK,CACtB,IAAK,GAAM,CAAC,IAAK,SAAU,KAAK,KAC1B,MAAM,UAAU,SAAS,EAAI,KAC/B,KAAK,KAAK,OAAO,IAAI,GA0B7B,SAAgB,UAAU,QAA4B,EAAE,CAAE,CACxD,IAAM,IAAM,QAAQ,KAAO,IACrB,SAAW,QAAQ,UAAY,IAC/B,QAAU,QAAQ,SAAW,oBAC7B,WAAa,QAAQ,YAAc,IACnC,aAAe,QAAQ,eAAkB,KAAiB,IAAI,IAAM,aACpE,YAAc,QAAQ,SAAW,GACjC,MAAQ,QAAQ,OAAS,IAAI,YAAY,SAAS,CAClD,KAAO,QAAQ,KACf,UAAY,IAAI,IAAI,QAAQ,WAAa,EAAE,CAAC,CAElD,OAAO,MAAO,IAAc,IAAe,OAAuB,CAOhE,GALI,UAAU,IAAI,IAAI,KAAK,EAKvB,MAAQ,KAAK,IAAI,CACnB,OAAO,MAAM,CAGf,IAAM,IAAM,aAAa,IAAI,CACvB,CAAE,UAAW,WAAc,MAAM,MAAM,UAAU,IAAI,CACrD,UAAY,KAAK,IAAI,EAAG,IAAM,UAAU,CAQ9C,GANI,cACF,IAAI,UAAU,kBAAmB,IAAI,CACrC,IAAI,UAAU,sBAAuB,UAAU,CAC/C,IAAI,UAAU,kBAAmB,KAAK,KAAK,UAAU,SAAS,CAAG,IAAK,CAAC,EAGrE,UAAY,IACd,OAAO,IAAI,OAAO,WAAW,CAAC,KAAK,CAAE,QAAS,CAAC,CAGjD,MAAM"}
1
+ {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../../src/http/middleware/rate-limit.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface RateLimitStore {\n increment(key: string): Promise<{ totalHits: number; resetTime: Date }>\n decrement(key: string): Promise<void>\n reset(key: string): Promise<void>\n}\n\nexport interface RateLimitOptions {\n /** Maximum number of requests per window (default: 100) */\n max?: number\n /** Time window in milliseconds (default: 60_000) */\n windowMs?: number\n /** Response message when rate limit is exceeded (default: 'Too Many Requests') */\n message?: string\n /** HTTP status code when rate limit is exceeded (default: 429) */\n statusCode?: number\n /** Function to generate the key for rate limiting (default: req.ip) */\n keyGenerator?: (req: Request) => string\n /** Whether to send rate limit headers (default: true) */\n headers?: boolean\n /** Custom store implementation (default: in-memory Map) */\n store?: RateLimitStore\n /** Function to skip rate limiting for certain requests */\n skip?: (req: Request) => boolean\n /** Paths to exclude from rate limiting */\n skipPaths?: string[]\n}\n\ninterface MemoryStoreEntry {\n totalHits: number\n resetTime: Date\n}\n\nclass MemoryStore implements RateLimitStore {\n private hits = new Map<string, MemoryStoreEntry>()\n private cleanupTimer: ReturnType<typeof setInterval>\n\n constructor(private windowMs: number) {\n this.cleanupTimer = setInterval(() => this.cleanup(), windowMs)\n // Allow the process to exit without waiting for the timer\n if (this.cleanupTimer.unref) {\n this.cleanupTimer.unref()\n }\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n\n if (entry && entry.resetTime.getTime() > now) {\n entry.totalHits++\n return { totalHits: entry.totalHits, resetTime: entry.resetTime }\n }\n\n const resetTime = new Date(now + this.windowMs)\n const newEntry: MemoryStoreEntry = { totalHits: 1, resetTime }\n this.hits.set(key, newEntry)\n return { totalHits: 1, resetTime }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.totalHits > 0) {\n entry.totalHits--\n }\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n\n private cleanup(): void {\n const now = Date.now()\n for (const [key, entry] of this.hits) {\n if (entry.resetTime.getTime() <= now) {\n this.hits.delete(key)\n }\n }\n }\n}\n\n/**\n * Rate limiting middleware.\n *\n * Limits the number of requests a client can make within a time window.\n * Uses an in-memory store by default, but accepts a custom store for\n * distributed deployments (e.g. Redis).\n *\n * @example\n * ```ts\n * import { rateLimit } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * rateLimit({ max: 100, windowMs: 60_000 }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function rateLimit(options: RateLimitOptions = {}) {\n const max = options.max ?? 100\n const windowMs = options.windowMs ?? 60_000\n const message = options.message ?? 'Too Many Requests'\n const statusCode = options.statusCode ?? 429\n const keyGenerator = options.keyGenerator ?? ((req: Request) => req.ip ?? '127.0.0.1')\n const sendHeaders = options.headers ?? true\n const store = options.store ?? new MemoryStore(windowMs)\n const skip = options.skip\n const skipPaths = new Set(options.skipPaths ?? [])\n\n return async (req: Request, res: Response, next: NextFunction) => {\n // Skip if path is in the skip list\n if (skipPaths.has(req.path)) {\n return next()\n }\n\n // Skip if the skip function returns true\n if (skip && skip(req)) {\n return next()\n }\n\n const key = keyGenerator(req)\n const { totalHits, resetTime } = await store.increment(key)\n const remaining = Math.max(0, max - totalHits)\n\n if (sendHeaders) {\n res.setHeader('RateLimit-Limit', max)\n res.setHeader('RateLimit-Remaining', remaining)\n res.setHeader('RateLimit-Reset', Math.ceil(resetTime.getTime() / 1000))\n }\n\n if (totalHits > max) {\n return res.status(statusCode).json({ message })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;AAkCA,IAAM,YAAN,KAA4C,CAItB,SAHpB,KAAe,IAAI,IACnB,aAEA,YAAY,SAA0B,CAAlB,KAAA,SAAA,SAClB,KAAK,aAAe,gBAAkB,KAAK,QAAQ,EAAG,QAAQ,EAE1D,KAAK,aAAa,OACpB,KAAK,aAAa,MAAM,CAE5B,CAEA,MAAM,UAAU,IAA8D,CAC5E,IAAM,IAAM,KAAK,IAAI,EACf,MAAQ,KAAK,KAAK,IAAI,GAAG,EAE/B,GAAI,OAAS,MAAM,UAAU,QAAQ,EAAI,IAEvC,MADA,OAAM,YACC,CAAE,UAAW,MAAM,UAAW,UAAW,MAAM,SAAU,EAGlE,IAAM,UAAY,IAAI,KAAK,IAAM,KAAK,QAAQ,EACxC,SAA6B,CAAE,UAAW,EAAG,SAAU,EAE7D,OADA,KAAK,KAAK,IAAI,IAAK,QAAQ,EACpB,CAAE,UAAW,EAAG,SAAU,CACnC,CAEA,MAAM,UAAU,IAA4B,CAC1C,IAAM,MAAQ,KAAK,KAAK,IAAI,GAAG,EAC3B,OAAS,MAAM,UAAY,GAC7B,MAAM,WAEV,CAEA,MAAM,MAAM,IAA4B,CACtC,KAAK,KAAK,OAAO,GAAG,CACtB,CAEA,SAAwB,CACtB,IAAM,IAAM,KAAK,IAAI,EACrB,IAAK,GAAM,CAAC,IAAK,SAAU,KAAK,KAC1B,MAAM,UAAU,QAAQ,GAAK,KAC/B,KAAK,KAAK,OAAO,GAAG,CAG1B,CACF,EAsBA,SAAgB,UAAU,QAA4B,CAAC,EAAG,CACxD,IAAM,IAAM,QAAQ,KAAO,IACrB,SAAW,QAAQ,UAAY,IAC/B,QAAU,QAAQ,SAAW,oBAC7B,WAAa,QAAQ,YAAc,IACnC,aAAe,QAAQ,eAAkB,KAAiB,IAAI,IAAM,aACpE,YAAc,QAAQ,SAAW,GACjC,MAAQ,QAAQ,OAAS,IAAI,YAAY,QAAQ,EACjD,KAAO,QAAQ,KACf,UAAY,IAAI,IAAI,QAAQ,WAAa,CAAC,CAAC,EAEjD,OAAO,MAAO,IAAc,IAAe,OAAuB,CAOhE,GALI,UAAU,IAAI,IAAI,IAAI,GAKtB,MAAQ,KAAK,GAAG,EAClB,OAAO,KAAK,EAGd,IAAM,IAAM,aAAa,GAAG,EACtB,CAAE,UAAW,WAAc,MAAM,MAAM,UAAU,GAAG,EACpD,UAAY,KAAK,IAAI,EAAG,IAAM,SAAS,EAQ7C,GANI,cACF,IAAI,UAAU,kBAAmB,GAAG,EACpC,IAAI,UAAU,sBAAuB,SAAS,EAC9C,IAAI,UAAU,kBAAmB,KAAK,KAAK,UAAU,QAAQ,EAAI,GAAI,CAAC,GAGpE,UAAY,IACd,OAAO,IAAI,OAAO,UAAU,EAAE,KAAK,CAAE,OAAQ,CAAC,EAGhD,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"request-id.mjs","names":[],"sources":["../../../src/http/middleware/request-id.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport const REQUEST_ID_HEADER = 'x-request-id'\n\n/**\n * Middleware that generates or propagates a unique request ID.\n *\n * Idempotent: if `req.requestId` is already set (e.g. by\n * `requestScopeMiddleware` earlier in the pipeline), this middleware reuses\n * it instead of generating a divergent second value. Keeps the\n * X-Request-Id response header, the ALS store's `requestId`, and\n * `RequestContext.requestId` in agreement for every request, regardless of\n * mounting order.\n */\nexport function requestId() {\n return (req: Request, res: Response, next: NextFunction) => {\n const existing = (req as Request & { requestId?: string }).requestId\n const id = existing || (req.headers[REQUEST_ID_HEADER] as string) || randomUUID()\n ;(req as any).requestId = id\n res.setHeader(REQUEST_ID_HEADER, id)\n next()\n }\n}\n"],"mappings":";;;;;;;;;;oCAGA,MAAa,kBAAoB,eAYjC,SAAgB,WAAY,CAC1B,OAAQ,IAAc,IAAe,OAAuB,CAE1D,IAAM,GADY,IAAyC,WACnC,IAAI,QAAA,iBAAyC,YAAY,CAC/E,IAAY,UAAY,GAC1B,IAAI,UAAU,kBAAmB,GAAG,CACpC,MAAM"}
1
+ {"version":3,"file":"request-id.mjs","names":[],"sources":["../../../src/http/middleware/request-id.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport const REQUEST_ID_HEADER = 'x-request-id'\n\n/**\n * Middleware that generates or propagates a unique request ID.\n *\n * Idempotent: if `req.requestId` is already set (e.g. by\n * `requestScopeMiddleware` earlier in the pipeline), this middleware reuses\n * it instead of generating a divergent second value. Keeps the\n * X-Request-Id response header, the ALS store's `requestId`, and\n * `RequestContext.requestId` in agreement for every request, regardless of\n * mounting order.\n */\nexport function requestId() {\n return (req: Request, res: Response, next: NextFunction) => {\n const existing = (req as Request & { requestId?: string }).requestId\n const id = existing || (req.headers[REQUEST_ID_HEADER] as string) || randomUUID()\n ;(req as any).requestId = id\n res.setHeader(REQUEST_ID_HEADER, id)\n next()\n }\n}\n"],"mappings":";;;;;;;;;;oCAGA,MAAa,kBAAoB,eAYjC,SAAgB,WAAY,CAC1B,OAAQ,IAAc,IAAe,OAAuB,CAE1D,IAAM,GADY,IAAyC,WACnC,IAAI,QAAA,iBAAyC,WAAW,EAC/E,IAAa,UAAY,GAC1B,IAAI,UAAU,kBAAmB,EAAE,EACnC,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.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-CJKmg8QR.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-BQHQsimv.mjs";function requestLogger(options={}){let log=createLogger(options.name??`HTTP`),level=options.level??`info`,skip=options.skip??[];return(req,res,next)=>{if(skip.some(prefix=>req.path.startsWith(prefix)))return next();let start=Date.now();res.on(`finish`,()=>{let duration=Date.now()-start,requestId=req.requestId||req.headers[`x-request-id`]||`-`,status=res.statusCode;log[level](`${req.method} ${req.originalUrl} ${status} ${duration}ms ${requestId}`)}),next()}}export{requestLogger};
12
12
  //# sourceMappingURL=request-logger.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-logger.mjs","names":[],"sources":["../../../src/http/middleware/request-logger.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { createLogger } from '../../core'\n\nexport interface RequestLoggerOptions {\n /** Logger name (default: 'HTTP') */\n name?: string\n /** Log level for successful requests (default: 'info') */\n level?: 'info' | 'debug' | 'trace'\n /** Skip logging for paths matching these prefixes (e.g. ['/health', '/_debug']) */\n skip?: string[]\n}\n\n/**\n * Middleware that logs every request with method, URL, status, duration, and request ID.\n *\n * @example\n * ```ts\n * bootstrap({\n * middleware: [requestId(), requestLogger(), express.json()],\n * })\n * ```\n *\n * Output:\n * ```\n * [HTTP] GET /api/v1/users 200 12ms req-abc123\n * [HTTP] POST /api/v1/users 201 45ms req-def456\n * ```\n */\nexport function requestLogger(options: RequestLoggerOptions = {}) {\n const log = createLogger(options.name ?? 'HTTP')\n const level = options.level ?? 'info'\n const skip = options.skip ?? []\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Skip logging for excluded paths\n if (skip.some((prefix) => req.path.startsWith(prefix))) {\n return next()\n }\n\n const start = Date.now()\n\n res.on('finish', () => {\n const duration = Date.now() - start\n const requestId = (req as any).requestId || req.headers['x-request-id'] || '-'\n const status = res.statusCode\n\n log[level](`${req.method} ${req.originalUrl} ${status} ${duration}ms ${requestId}`)\n })\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;gFA4BA,SAAgB,cAAc,QAAgC,EAAE,CAAE,CAChE,IAAM,IAAM,aAAa,QAAQ,MAAQ,OAAO,CAC1C,MAAQ,QAAQ,OAAS,OACzB,KAAO,QAAQ,MAAQ,EAAE,CAE/B,OAAQ,IAAc,IAAe,OAAuB,CAE1D,GAAI,KAAK,KAAM,QAAW,IAAI,KAAK,WAAW,OAAO,CAAC,CACpD,OAAO,MAAM,CAGf,IAAM,MAAQ,KAAK,KAAK,CAExB,IAAI,GAAG,aAAgB,CACrB,IAAM,SAAW,KAAK,KAAK,CAAG,MACxB,UAAa,IAAY,WAAa,IAAI,QAAQ,iBAAmB,IACrE,OAAS,IAAI,WAEnB,IAAI,OAAO,GAAG,IAAI,OAAO,GAAG,IAAI,YAAY,GAAG,OAAO,GAAG,SAAS,KAAK,YAAY,EACnF,CAEF,MAAM"}
1
+ {"version":3,"file":"request-logger.mjs","names":[],"sources":["../../../src/http/middleware/request-logger.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { createLogger } from '../../core'\n\nexport interface RequestLoggerOptions {\n /** Logger name (default: 'HTTP') */\n name?: string\n /** Log level for successful requests (default: 'info') */\n level?: 'info' | 'debug' | 'trace'\n /** Skip logging for paths matching these prefixes (e.g. ['/health', '/_debug']) */\n skip?: string[]\n}\n\n/**\n * Middleware that logs every request with method, URL, status, duration, and request ID.\n *\n * @example\n * ```ts\n * bootstrap({\n * middleware: [requestId(), requestLogger(), express.json()],\n * })\n * ```\n *\n * Output:\n * ```\n * [HTTP] GET /api/v1/users 200 12ms req-abc123\n * [HTTP] POST /api/v1/users 201 45ms req-def456\n * ```\n */\nexport function requestLogger(options: RequestLoggerOptions = {}) {\n const log = createLogger(options.name ?? 'HTTP')\n const level = options.level ?? 'info'\n const skip = options.skip ?? []\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Skip logging for excluded paths\n if (skip.some((prefix) => req.path.startsWith(prefix))) {\n return next()\n }\n\n const start = Date.now()\n\n res.on('finish', () => {\n const duration = Date.now() - start\n const requestId = (req as any).requestId || req.headers['x-request-id'] || '-'\n const status = res.statusCode\n\n log[level](`${req.method} ${req.originalUrl} ${status} ${duration}ms ${requestId}`)\n })\n\n next()\n }\n}\n"],"mappings":";;;;;;;;;;gFA4BA,SAAgB,cAAc,QAAgC,CAAC,EAAG,CAChE,IAAM,IAAM,aAAa,QAAQ,MAAQ,MAAM,EACzC,MAAQ,QAAQ,OAAS,OACzB,KAAO,QAAQ,MAAQ,CAAC,EAE9B,OAAQ,IAAc,IAAe,OAAuB,CAE1D,GAAI,KAAK,KAAM,QAAW,IAAI,KAAK,WAAW,MAAM,CAAC,EACnD,OAAO,KAAK,EAGd,IAAM,MAAQ,KAAK,IAAI,EAEvB,IAAI,GAAG,aAAgB,CACrB,IAAM,SAAW,KAAK,IAAI,EAAI,MACxB,UAAa,IAAY,WAAa,IAAI,QAAQ,iBAAmB,IACrE,OAAS,IAAI,WAEnB,IAAI,OAAO,GAAG,IAAI,OAAO,GAAG,IAAI,YAAY,GAAG,OAAO,GAAG,SAAS,KAAK,WAAW,CACpF,CAAC,EAED,KAAK,CACP,CACF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"session.mjs","names":[],"sources":["../../../src/http/middleware/session.ts"],"sourcesContent":["import { randomUUID, createHmac, timingSafeEqual } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport interface SessionData {\n [key: string]: unknown\n}\n\nexport interface SessionStore {\n get(sid: string): Promise<SessionData | null>\n set(sid: string, data: SessionData, maxAge: number): Promise<void>\n destroy(sid: string): Promise<void>\n touch?(sid: string, maxAge: number): Promise<void>\n}\n\nexport interface Session {\n id: string\n data: SessionData\n regenerate(): Promise<void>\n destroy(): Promise<void>\n save(): Promise<void>\n}\n\nexport interface SessionOptions {\n /** Secret used to sign the session cookie (required) */\n secret: string\n /** Cookie name (default: 'kick.sid') */\n cookieName?: string\n /** Session max age in milliseconds (default: 86400000 = 24h) */\n maxAge?: number\n /** Reset maxAge on every response (default: false) */\n rolling?: boolean\n /** Save new sessions that have not been modified (default: true) */\n saveUninitialized?: boolean\n /** Cookie options */\n cookie?: {\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n path?: string\n domain?: string\n }\n /** Custom session store (default: in-memory store with TTL cleanup) */\n store?: SessionStore\n}\n\n// ── In-Memory Store ───────────────────────────────────────────────────\n\nclass MemoryStore implements SessionStore {\n private readonly sessions = new Map<string, { data: SessionData; expires: number }>()\n private readonly cleanupInterval: ReturnType<typeof setInterval>\n\n constructor() {\n // Purge expired sessions every 60 seconds\n this.cleanupInterval = setInterval(() => this.purge(), 60_000)\n this.cleanupInterval.unref()\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const entry = this.sessions.get(sid)\n if (!entry) return null\n if (Date.now() > entry.expires) {\n this.sessions.delete(sid)\n return null\n }\n return entry.data\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n this.sessions.set(sid, { data, expires: Date.now() + maxAge })\n }\n\n async destroy(sid: string): Promise<void> {\n this.sessions.delete(sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const entry = this.sessions.get(sid)\n if (entry) {\n entry.expires = Date.now() + maxAge\n }\n }\n\n private purge() {\n const now = Date.now()\n for (const [sid, entry] of this.sessions) {\n if (now > entry.expires) {\n this.sessions.delete(sid)\n }\n }\n }\n}\n\n// ── Cookie Header Parsing ─────────────────────────────────────────────\n\nfunction parseCookieHeader(header: string): Record<string, string> {\n const out: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const eq = pair.indexOf('=')\n if (eq === -1) continue\n const key = pair.slice(0, eq).trim()\n if (!key) continue\n let value = pair.slice(eq + 1).trim()\n if (value.startsWith('\"') && value.endsWith('\"')) {\n value = value.slice(1, -1)\n }\n try {\n out[key] = decodeURIComponent(value)\n } catch {\n out[key] = value\n }\n }\n return out\n}\n\n// ── Cookie Signing ────────────────────────────────────────────────────\n\nfunction sign(value: string, secret: string): string {\n const signature = createHmac('sha256', secret).update(value).digest('base64url')\n return `s:${value}.${signature}`\n}\n\nfunction unsign(signed: string, secret: string): string | false {\n if (!signed.startsWith('s:')) return false\n const raw = signed.slice(2)\n const dotIndex = raw.lastIndexOf('.')\n if (dotIndex === -1) return false\n\n const value = raw.slice(0, dotIndex)\n const providedSig = raw.slice(dotIndex + 1)\n const expectedSig = createHmac('sha256', secret).update(value).digest('base64url')\n\n const a = Buffer.from(providedSig)\n const b = Buffer.from(expectedSig)\n\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n return false\n }\n\n return value\n}\n\n// ── Middleware Factory ────────────────────────────────────────────────\n\n/**\n * Session management middleware.\n *\n * Attaches a `req.session` object with `id`, `data`, `regenerate()`,\n * `destroy()`, and `save()` methods. Session IDs are signed with\n * HMAC-SHA256 to prevent cookie tampering.\n *\n * Reads the session cookie from `req.cookies` when an upstream cookie\n * parser is present; otherwise parses `req.headers.cookie` directly, so\n * no cookie parser is required upstream.\n *\n * @example\n * ```ts\n * import { session } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * session({ secret: process.env.SESSION_SECRET! }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function session(options: SessionOptions) {\n const {\n secret,\n cookieName = 'kick.sid',\n maxAge = 86_400_000,\n rolling = false,\n saveUninitialized = true,\n store = new MemoryStore(),\n cookie: cookieOpts = {},\n } = options\n\n const cookieDefaults = {\n httpOnly: cookieOpts.httpOnly ?? true,\n secure: cookieOpts.secure ?? process.env.NODE_ENV === 'production',\n sameSite: cookieOpts.sameSite ?? ('lax' as const),\n path: cookieOpts.path ?? '/',\n ...(cookieOpts.domain ? { domain: cookieOpts.domain } : {}),\n maxAge,\n }\n\n return async (req: Request, res: Response, next: NextFunction) => {\n let cookies = (req as any).cookies as Record<string, string> | undefined\n if (!cookies) {\n const header = req.headers.cookie\n cookies = header ? parseCookieHeader(header) : {}\n }\n const signedCookie = cookies[cookieName]\n let sid: string | false = false\n let sessionData: SessionData | null = null\n let isNew = false\n\n // Attempt to recover existing session\n if (signedCookie) {\n sid = unsign(signedCookie, secret)\n if (sid) {\n sessionData = await store.get(sid)\n }\n }\n\n // Create new session if none found\n if (!sid || !sessionData) {\n sid = randomUUID()\n sessionData = {}\n isNew = true\n }\n\n let currentSid = sid\n let currentData = { ...sessionData }\n let destroyed = false\n\n const sessionObj: Session = {\n get id() {\n return currentSid\n },\n data: currentData,\n\n async regenerate() {\n await store.destroy(currentSid)\n currentSid = randomUUID()\n currentData = {}\n sessionObj.data = currentData\n await store.set(currentSid, currentData, maxAge)\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n },\n\n async destroy() {\n await store.destroy(currentSid)\n destroyed = true\n currentData = {}\n sessionObj.data = currentData\n res.clearCookie(cookieName, { path: cookieDefaults.path })\n },\n\n async save() {\n if (!destroyed) {\n await store.set(currentSid, currentData, maxAge)\n }\n },\n }\n\n ;(req as any).session = sessionObj\n\n // Set cookie for new sessions\n if (isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n // Auto-save on response finish\n res.on('finish', async () => {\n if (destroyed) return\n if (isNew && !saveUninitialized && Object.keys(currentData).length === 0) return\n\n await store.set(currentSid, currentData, maxAge)\n\n if (rolling && !isNew) {\n if (store.touch) {\n await store.touch(currentSid, maxAge)\n }\n }\n })\n\n // Rolling: refresh cookie on every response\n if (rolling && !isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n next()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session.mjs","names":[],"sources":["../../../src/http/middleware/session.ts"],"sourcesContent":["import { randomUUID, createHmac, timingSafeEqual } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport interface SessionData {\n [key: string]: unknown\n}\n\nexport interface SessionStore {\n get(sid: string): Promise<SessionData | null>\n set(sid: string, data: SessionData, maxAge: number): Promise<void>\n destroy(sid: string): Promise<void>\n touch?(sid: string, maxAge: number): Promise<void>\n}\n\nexport interface Session {\n id: string\n data: SessionData\n regenerate(): Promise<void>\n destroy(): Promise<void>\n save(): Promise<void>\n}\n\nexport interface SessionOptions {\n /** Secret used to sign the session cookie (required) */\n secret: string\n /** Cookie name (default: 'kick.sid') */\n cookieName?: string\n /** Session max age in milliseconds (default: 86400000 = 24h) */\n maxAge?: number\n /** Reset maxAge on every response (default: false) */\n rolling?: boolean\n /** Save new sessions that have not been modified (default: true) */\n saveUninitialized?: boolean\n /** Cookie options */\n cookie?: {\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n path?: string\n domain?: string\n }\n /** Custom session store (default: in-memory store with TTL cleanup) */\n store?: SessionStore\n}\n\n// ── In-Memory Store ───────────────────────────────────────────────────\n\nclass MemoryStore implements SessionStore {\n private readonly sessions = new Map<string, { data: SessionData; expires: number }>()\n private readonly cleanupInterval: ReturnType<typeof setInterval>\n\n constructor() {\n // Purge expired sessions every 60 seconds\n this.cleanupInterval = setInterval(() => this.purge(), 60_000)\n this.cleanupInterval.unref()\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const entry = this.sessions.get(sid)\n if (!entry) return null\n if (Date.now() > entry.expires) {\n this.sessions.delete(sid)\n return null\n }\n return entry.data\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n this.sessions.set(sid, { data, expires: Date.now() + maxAge })\n }\n\n async destroy(sid: string): Promise<void> {\n this.sessions.delete(sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const entry = this.sessions.get(sid)\n if (entry) {\n entry.expires = Date.now() + maxAge\n }\n }\n\n private purge() {\n const now = Date.now()\n for (const [sid, entry] of this.sessions) {\n if (now > entry.expires) {\n this.sessions.delete(sid)\n }\n }\n }\n}\n\n// ── Cookie Header Parsing ─────────────────────────────────────────────\n\nfunction parseCookieHeader(header: string): Record<string, string> {\n const out: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const eq = pair.indexOf('=')\n if (eq === -1) continue\n const key = pair.slice(0, eq).trim()\n if (!key) continue\n let value = pair.slice(eq + 1).trim()\n if (value.startsWith('\"') && value.endsWith('\"')) {\n value = value.slice(1, -1)\n }\n try {\n out[key] = decodeURIComponent(value)\n } catch {\n out[key] = value\n }\n }\n return out\n}\n\n// ── Cookie Signing ────────────────────────────────────────────────────\n\nfunction sign(value: string, secret: string): string {\n const signature = createHmac('sha256', secret).update(value).digest('base64url')\n return `s:${value}.${signature}`\n}\n\nfunction unsign(signed: string, secret: string): string | false {\n if (!signed.startsWith('s:')) return false\n const raw = signed.slice(2)\n const dotIndex = raw.lastIndexOf('.')\n if (dotIndex === -1) return false\n\n const value = raw.slice(0, dotIndex)\n const providedSig = raw.slice(dotIndex + 1)\n const expectedSig = createHmac('sha256', secret).update(value).digest('base64url')\n\n const a = Buffer.from(providedSig)\n const b = Buffer.from(expectedSig)\n\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n return false\n }\n\n return value\n}\n\n// ── Middleware Factory ────────────────────────────────────────────────\n\n/**\n * Session management middleware.\n *\n * Attaches a `req.session` object with `id`, `data`, `regenerate()`,\n * `destroy()`, and `save()` methods. Session IDs are signed with\n * HMAC-SHA256 to prevent cookie tampering.\n *\n * Reads the session cookie from `req.cookies` when an upstream cookie\n * parser is present; otherwise parses `req.headers.cookie` directly, so\n * no cookie parser is required upstream.\n *\n * @example\n * ```ts\n * import { session } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * session({ secret: process.env.SESSION_SECRET! }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function session(options: SessionOptions) {\n const {\n secret,\n cookieName = 'kick.sid',\n maxAge = 86_400_000,\n rolling = false,\n saveUninitialized = true,\n store = new MemoryStore(),\n cookie: cookieOpts = {},\n } = options\n\n const cookieDefaults = {\n httpOnly: cookieOpts.httpOnly ?? true,\n secure: cookieOpts.secure ?? process.env.NODE_ENV === 'production',\n sameSite: cookieOpts.sameSite ?? ('lax' as const),\n path: cookieOpts.path ?? '/',\n ...(cookieOpts.domain ? { domain: cookieOpts.domain } : {}),\n maxAge,\n }\n\n return async (req: Request, res: Response, next: NextFunction) => {\n let cookies = (req as any).cookies as Record<string, string> | undefined\n if (!cookies) {\n const header = req.headers.cookie\n cookies = header ? parseCookieHeader(header) : {}\n }\n const signedCookie = cookies[cookieName]\n let sid: string | false = false\n let sessionData: SessionData | null = null\n let isNew = false\n\n // Attempt to recover existing session\n if (signedCookie) {\n sid = unsign(signedCookie, secret)\n if (sid) {\n sessionData = await store.get(sid)\n }\n }\n\n // Create new session if none found\n if (!sid || !sessionData) {\n sid = randomUUID()\n sessionData = {}\n isNew = true\n }\n\n let currentSid = sid\n let currentData = { ...sessionData }\n let destroyed = false\n\n const sessionObj: Session = {\n get id() {\n return currentSid\n },\n data: currentData,\n\n async regenerate() {\n await store.destroy(currentSid)\n currentSid = randomUUID()\n currentData = {}\n sessionObj.data = currentData\n await store.set(currentSid, currentData, maxAge)\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n },\n\n async destroy() {\n await store.destroy(currentSid)\n destroyed = true\n currentData = {}\n sessionObj.data = currentData\n res.clearCookie(cookieName, { path: cookieDefaults.path })\n },\n\n async save() {\n if (!destroyed) {\n await store.set(currentSid, currentData, maxAge)\n }\n },\n }\n\n ;(req as any).session = sessionObj\n\n // Set cookie for new sessions\n if (isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n // Auto-save on response finish\n res.on('finish', async () => {\n if (destroyed) return\n if (isNew && !saveUninitialized && Object.keys(currentData).length === 0) return\n\n await store.set(currentSid, currentData, maxAge)\n\n if (rolling && !isNew) {\n if (store.touch) {\n await store.touch(currentSid, maxAge)\n }\n }\n })\n\n // Rolling: refresh cookie on every response\n if (rolling && !isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n next()\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { n as SpaAdapterOptions, t as SpaAdapter } from "../../spa-DIXtRBj-.mjs";
2
+ import { n as SpaAdapterOptions, t as SpaAdapter } from "../../spa-BlW5xxU5.mjs";
3
3
  export { SpaAdapter, SpaAdapterOptions };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{h as __require}from"../../container-COmj1vM3.mjs";import{Logger}from"../../core/logger.mjs";import"../../core-CJKmg8QR.mjs";import{existsSync,readFileSync}from"node:fs";import{join,resolve}from"node:path";const log=Logger.for(`SpaAdapter`);var SpaAdapter=class{name=`SpaAdapter`;clientDir;apiPrefixes;excludePaths;cacheControl;indexCacheControl;indexHtml=null;constructor(options={}){this.options=options,this.clientDir=resolve(options.clientDir??`dist/client`),this.excludePaths=options.exclude??[],this.cacheControl=options.cacheControl??`public, max-age=31536000, immutable`,this.indexCacheControl=options.indexCacheControl??`no-cache`;let prefix=options.apiPrefix??`/api`;this.apiPrefixes=Array.isArray(prefix)?prefix:[prefix]}beforeMount({app}){if(!existsSync(this.clientDir)){log.warn(`SPA client directory not found: ${this.clientDir}`),log.warn(`Build your frontend first, or set clientDir to the correct path.`);return}let indexPath=join(this.clientDir,`index.html`);if(existsSync(indexPath))this.indexHtml=readFileSync(indexPath,`utf-8`);else{log.warn(`index.html not found in ${this.clientDir}`);return}try{let express=__require(`express`),staticOpts={};this.cacheControl&&(staticOpts.setHeaders=(res,filePath)=>{filePath.endsWith(`.html`)?res.setHeader(`Cache-Control`,this.indexCacheControl):res.setHeader(`Cache-Control`,this.cacheControl)}),app.use(express.static(this.clientDir,staticOpts))}catch{log.error(`express.static not available — SPA static file serving disabled`);return}log.debug(`Serving SPA from ${this.clientDir}`)}beforeStart({app}){this.indexHtml&&app.use((req,res,next)=>{for(let prefix of this.apiPrefixes)if(req.path.startsWith(prefix))return next();for(let path of this.excludePaths)if(req.path.startsWith(path))return next();if(req.path.includes(`.`)||req.method!==`GET`)return next();res.setHeader(`Content-Type`,`text/html`),res.setHeader(`Cache-Control`,this.indexCacheControl),res.send(this.indexHtml)})}};export{SpaAdapter};
11
+ import{h as __require}from"../../container-BvRsVif5.mjs";import{Logger}from"../../core/logger.mjs";import"../../core-BQHQsimv.mjs";import{existsSync,readFileSync}from"node:fs";import{join,resolve}from"node:path";const log=Logger.for(`SpaAdapter`);var SpaAdapter=class{options;name=`SpaAdapter`;clientDir;apiPrefixes;excludePaths;cacheControl;indexCacheControl;indexHtml=null;constructor(options={}){this.options=options,this.clientDir=resolve(options.clientDir??`dist/client`),this.excludePaths=options.exclude??[],this.cacheControl=options.cacheControl??`public, max-age=31536000, immutable`,this.indexCacheControl=options.indexCacheControl??`no-cache`;let prefix=options.apiPrefix??`/api`;this.apiPrefixes=Array.isArray(prefix)?prefix:[prefix]}beforeMount({app}){if(!existsSync(this.clientDir)){log.warn(`SPA client directory not found: ${this.clientDir}`),log.warn(`Build your frontend first, or set clientDir to the correct path.`);return}let indexPath=join(this.clientDir,`index.html`);if(existsSync(indexPath))this.indexHtml=readFileSync(indexPath,`utf-8`);else{log.warn(`index.html not found in ${this.clientDir}`);return}try{let express=__require(`express`),staticOpts={};this.cacheControl&&(staticOpts.setHeaders=(res,filePath)=>{filePath.endsWith(`.html`)?res.setHeader(`Cache-Control`,this.indexCacheControl):res.setHeader(`Cache-Control`,this.cacheControl)}),app.use(express.static(this.clientDir,staticOpts))}catch{log.error(`express.static not available — SPA static file serving disabled`);return}log.debug(`Serving SPA from ${this.clientDir}`)}beforeStart({app}){this.indexHtml&&app.use((req,res,next)=>{for(let prefix of this.apiPrefixes)if(req.path.startsWith(prefix))return next();for(let path of this.excludePaths)if(req.path.startsWith(path))return next();if(req.path.includes(`.`)||req.method!==`GET`)return next();res.setHeader(`Content-Type`,`text/html`),res.setHeader(`Cache-Control`,this.indexCacheControl),res.send(this.indexHtml)})}};export{SpaAdapter};
12
12
  //# sourceMappingURL=spa.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"spa.mjs","names":[],"sources":["../../../src/http/middleware/spa.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { Logger, type AppAdapter, type AdapterContext } from '../../core'\n\nconst log = Logger.for('SpaAdapter')\n\nexport interface SpaAdapterOptions {\n /**\n * Directory containing the built SPA files (index.html, assets, etc.)\n * Default: 'dist/client'\n *\n * @example\n * ```ts\n * // Vue: 'dist'\n * // React (Vite): 'dist'\n * // React (CRA): 'build'\n * // Svelte: 'build'\n * // Angular: 'dist/my-app/browser'\n * ```\n */\n clientDir?: string\n\n /**\n * URL prefix for API routes. SPA fallback only applies to\n * non-API routes (routes NOT starting with this prefix).\n * Default: '/api'\n *\n * Set to an array for multiple prefixes:\n * ```ts\n * apiPrefix: ['/api', '/graphql', '/_debug']\n * ```\n */\n apiPrefix?: string | string[]\n\n /**\n * Additional paths to exclude from SPA fallback.\n * These paths will NOT serve index.html.\n *\n * @example\n * ```ts\n * exclude: ['/health', '/metrics', '/ws']\n * ```\n */\n exclude?: string[]\n\n /**\n * Cache-Control header for static assets (default: 'public, max-age=31536000, immutable')\n * Set to false to disable caching headers.\n */\n cacheControl?: string | false\n\n /**\n * Cache-Control for index.html (default: 'no-cache')\n * index.html should not be cached to ensure clients get fresh builds.\n */\n indexCacheControl?: string\n}\n\n/**\n * SPA adapter — serve a Vue, React, Svelte, or Angular build alongside\n * your KickJS API. API routes are handled by controllers; everything else\n * falls back to index.html for client-side routing.\n *\n * @example\n * ```ts\n * import { SpaAdapter } from '@forinda/kickjs/spa'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new SpaAdapter({\n * clientDir: 'dist/client', // or 'build', 'dist', etc.\n * apiPrefix: '/api',\n * }),\n * ],\n * })\n * ```\n *\n * File structure:\n * ```\n * dist/\n * client/ ← SPA build output\n * index.html\n * assets/\n * app.js\n * style.css\n * server/ ← KickJS server\n * ```\n */\nexport class SpaAdapter implements AppAdapter {\n name = 'SpaAdapter'\n private clientDir: string\n private apiPrefixes: string[]\n private excludePaths: string[]\n private cacheControl: string | false\n private indexCacheControl: string\n private indexHtml: string | null = null\n\n constructor(private options: SpaAdapterOptions = {}) {\n this.clientDir = resolve(options.clientDir ?? 'dist/client')\n this.excludePaths = options.exclude ?? []\n this.cacheControl = options.cacheControl ?? 'public, max-age=31536000, immutable'\n this.indexCacheControl = options.indexCacheControl ?? 'no-cache'\n\n const prefix = options.apiPrefix ?? '/api'\n this.apiPrefixes = Array.isArray(prefix) ? prefix : [prefix]\n }\n\n beforeMount({ app }: AdapterContext): void {\n if (!existsSync(this.clientDir)) {\n log.warn(`SPA client directory not found: ${this.clientDir}`)\n log.warn('Build your frontend first, or set clientDir to the correct path.')\n return\n }\n\n // Read index.html into memory\n const indexPath = join(this.clientDir, 'index.html')\n if (existsSync(indexPath)) {\n this.indexHtml = readFileSync(indexPath, 'utf-8')\n } else {\n log.warn(`index.html not found in ${this.clientDir}`)\n return\n }\n\n // Serve static files with cache headers\n try {\n // Dynamic import to avoid hard dependency on express.static\n const express = require('express')\n const staticOpts: any = {}\n\n if (this.cacheControl) {\n staticOpts.setHeaders = (res: any, filePath: string) => {\n if (filePath.endsWith('.html')) {\n res.setHeader('Cache-Control', this.indexCacheControl)\n } else {\n res.setHeader('Cache-Control', this.cacheControl as string)\n }\n }\n }\n\n app.use(express.static(this.clientDir, staticOpts))\n } catch {\n log.error('express.static not available — SPA static file serving disabled')\n return\n }\n\n log.debug(`Serving SPA from ${this.clientDir}`)\n }\n\n beforeStart({ app }: AdapterContext): void {\n if (!this.indexHtml) return\n\n // SPA fallback: serve index.html for all non-API, non-file routes\n app.use((req: any, res: any, next: any) => {\n // Skip API routes\n for (const prefix of this.apiPrefixes) {\n if (req.path.startsWith(prefix)) return next()\n }\n\n // Skip excluded paths\n for (const path of this.excludePaths) {\n if (req.path.startsWith(path)) return next()\n }\n\n // Skip requests for files (have an extension)\n if (req.path.includes('.')) return next()\n\n // Skip non-GET requests\n if (req.method !== 'GET') return next()\n\n // Serve index.html\n res.setHeader('Content-Type', 'text/html')\n res.setHeader('Cache-Control', this.indexCacheControl)\n res.send(this.indexHtml)\n })\n }\n}\n"],"mappings":";;;;;;;;;;oNAIA,MAAM,IAAM,OAAO,IAAI,aAAa,CAqFpC,IAAa,WAAb,KAA8C,CAC5C,KAAO,aACP,UACA,YACA,aACA,aACA,kBACA,UAAmC,KAEnC,YAAY,QAAqC,EAAE,CAAE,CAAjC,KAAA,QAAA,QAClB,KAAK,UAAY,QAAQ,QAAQ,WAAa,cAAc,CAC5D,KAAK,aAAe,QAAQ,SAAW,EAAE,CACzC,KAAK,aAAe,QAAQ,cAAgB,sCAC5C,KAAK,kBAAoB,QAAQ,mBAAqB,WAEtD,IAAM,OAAS,QAAQ,WAAa,OACpC,KAAK,YAAc,MAAM,QAAQ,OAAO,CAAG,OAAS,CAAC,OAAO,CAG9D,YAAY,CAAE,KAA6B,CACzC,GAAI,CAAC,WAAW,KAAK,UAAU,CAAE,CAC/B,IAAI,KAAK,mCAAmC,KAAK,YAAY,CAC7D,IAAI,KAAK,mEAAmE,CAC5E,OAIF,IAAM,UAAY,KAAK,KAAK,UAAW,aAAa,CACpD,GAAI,WAAW,UAAU,CACvB,KAAK,UAAY,aAAa,UAAW,QAAQ,KAC5C,CACL,IAAI,KAAK,2BAA2B,KAAK,YAAY,CACrD,OAIF,GAAI,CAEF,IAAM,QAAA,UAAkB,UAAU,CAC5B,WAAkB,EAAE,CAEtB,KAAK,eACP,WAAW,YAAc,IAAU,WAAqB,CAClD,SAAS,SAAS,QAAQ,CAC5B,IAAI,UAAU,gBAAiB,KAAK,kBAAkB,CAEtD,IAAI,UAAU,gBAAiB,KAAK,aAAuB,GAKjE,IAAI,IAAI,QAAQ,OAAO,KAAK,UAAW,WAAW,CAAC,MAC7C,CACN,IAAI,MAAM,kEAAkE,CAC5E,OAGF,IAAI,MAAM,oBAAoB,KAAK,YAAY,CAGjD,YAAY,CAAE,KAA6B,CACpC,KAAK,WAGV,IAAI,KAAK,IAAU,IAAU,OAAc,CAEzC,IAAK,IAAM,UAAU,KAAK,YACxB,GAAI,IAAI,KAAK,WAAW,OAAO,CAAE,OAAO,MAAM,CAIhD,IAAK,IAAM,QAAQ,KAAK,aACtB,GAAI,IAAI,KAAK,WAAW,KAAK,CAAE,OAAO,MAAM,CAO9C,GAHI,IAAI,KAAK,SAAS,IAAI,EAGtB,IAAI,SAAW,MAAO,OAAO,MAAM,CAGvC,IAAI,UAAU,eAAgB,YAAY,CAC1C,IAAI,UAAU,gBAAiB,KAAK,kBAAkB,CACtD,IAAI,KAAK,KAAK,UAAU,EACxB"}
1
+ {"version":3,"file":"spa.mjs","names":[],"sources":["../../../src/http/middleware/spa.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { Logger, type AppAdapter, type AdapterContext } from '../../core'\n\nconst log = Logger.for('SpaAdapter')\n\nexport interface SpaAdapterOptions {\n /**\n * Directory containing the built SPA files (index.html, assets, etc.)\n * Default: 'dist/client'\n *\n * @example\n * ```ts\n * // Vue: 'dist'\n * // React (Vite): 'dist'\n * // React (CRA): 'build'\n * // Svelte: 'build'\n * // Angular: 'dist/my-app/browser'\n * ```\n */\n clientDir?: string\n\n /**\n * URL prefix for API routes. SPA fallback only applies to\n * non-API routes (routes NOT starting with this prefix).\n * Default: '/api'\n *\n * Set to an array for multiple prefixes:\n * ```ts\n * apiPrefix: ['/api', '/graphql', '/_debug']\n * ```\n */\n apiPrefix?: string | string[]\n\n /**\n * Additional paths to exclude from SPA fallback.\n * These paths will NOT serve index.html.\n *\n * @example\n * ```ts\n * exclude: ['/health', '/metrics', '/ws']\n * ```\n */\n exclude?: string[]\n\n /**\n * Cache-Control header for static assets (default: 'public, max-age=31536000, immutable')\n * Set to false to disable caching headers.\n */\n cacheControl?: string | false\n\n /**\n * Cache-Control for index.html (default: 'no-cache')\n * index.html should not be cached to ensure clients get fresh builds.\n */\n indexCacheControl?: string\n}\n\n/**\n * SPA adapter — serve a Vue, React, Svelte, or Angular build alongside\n * your KickJS API. API routes are handled by controllers; everything else\n * falls back to index.html for client-side routing.\n *\n * @example\n * ```ts\n * import { SpaAdapter } from '@forinda/kickjs/spa'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new SpaAdapter({\n * clientDir: 'dist/client', // or 'build', 'dist', etc.\n * apiPrefix: '/api',\n * }),\n * ],\n * })\n * ```\n *\n * File structure:\n * ```\n * dist/\n * client/ ← SPA build output\n * index.html\n * assets/\n * app.js\n * style.css\n * server/ ← KickJS server\n * ```\n */\nexport class SpaAdapter implements AppAdapter {\n name = 'SpaAdapter'\n private clientDir: string\n private apiPrefixes: string[]\n private excludePaths: string[]\n private cacheControl: string | false\n private indexCacheControl: string\n private indexHtml: string | null = null\n\n constructor(private options: SpaAdapterOptions = {}) {\n this.clientDir = resolve(options.clientDir ?? 'dist/client')\n this.excludePaths = options.exclude ?? []\n this.cacheControl = options.cacheControl ?? 'public, max-age=31536000, immutable'\n this.indexCacheControl = options.indexCacheControl ?? 'no-cache'\n\n const prefix = options.apiPrefix ?? '/api'\n this.apiPrefixes = Array.isArray(prefix) ? prefix : [prefix]\n }\n\n beforeMount({ app }: AdapterContext): void {\n if (!existsSync(this.clientDir)) {\n log.warn(`SPA client directory not found: ${this.clientDir}`)\n log.warn('Build your frontend first, or set clientDir to the correct path.')\n return\n }\n\n // Read index.html into memory\n const indexPath = join(this.clientDir, 'index.html')\n if (existsSync(indexPath)) {\n this.indexHtml = readFileSync(indexPath, 'utf-8')\n } else {\n log.warn(`index.html not found in ${this.clientDir}`)\n return\n }\n\n // Serve static files with cache headers\n try {\n // Dynamic import to avoid hard dependency on express.static\n const express = require('express')\n const staticOpts: any = {}\n\n if (this.cacheControl) {\n staticOpts.setHeaders = (res: any, filePath: string) => {\n if (filePath.endsWith('.html')) {\n res.setHeader('Cache-Control', this.indexCacheControl)\n } else {\n res.setHeader('Cache-Control', this.cacheControl as string)\n }\n }\n }\n\n app.use(express.static(this.clientDir, staticOpts))\n } catch {\n log.error('express.static not available — SPA static file serving disabled')\n return\n }\n\n log.debug(`Serving SPA from ${this.clientDir}`)\n }\n\n beforeStart({ app }: AdapterContext): void {\n if (!this.indexHtml) return\n\n // SPA fallback: serve index.html for all non-API, non-file routes\n app.use((req: any, res: any, next: any) => {\n // Skip API routes\n for (const prefix of this.apiPrefixes) {\n if (req.path.startsWith(prefix)) return next()\n }\n\n // Skip excluded paths\n for (const path of this.excludePaths) {\n if (req.path.startsWith(path)) return next()\n }\n\n // Skip requests for files (have an extension)\n if (req.path.includes('.')) return next()\n\n // Skip non-GET requests\n if (req.method !== 'GET') return next()\n\n // Serve index.html\n res.setHeader('Content-Type', 'text/html')\n res.setHeader('Cache-Control', this.indexCacheControl)\n res.send(this.indexHtml)\n })\n }\n}\n"],"mappings":";;;;;;;;;;oNAIA,MAAM,IAAM,OAAO,IAAI,YAAY,EAqFnC,IAAa,WAAb,KAA8C,CASxB,QARpB,KAAO,aACP,UACA,YACA,aACA,aACA,kBACA,UAAmC,KAEnC,YAAY,QAAqC,CAAC,EAAG,CAAjC,KAAA,QAAA,QAClB,KAAK,UAAY,QAAQ,QAAQ,WAAa,aAAa,EAC3D,KAAK,aAAe,QAAQ,SAAW,CAAC,EACxC,KAAK,aAAe,QAAQ,cAAgB,sCAC5C,KAAK,kBAAoB,QAAQ,mBAAqB,WAEtD,IAAM,OAAS,QAAQ,WAAa,OACpC,KAAK,YAAc,MAAM,QAAQ,MAAM,EAAI,OAAS,CAAC,MAAM,CAC7D,CAEA,YAAY,CAAE,KAA6B,CACzC,GAAI,CAAC,WAAW,KAAK,SAAS,EAAG,CAC/B,IAAI,KAAK,mCAAmC,KAAK,WAAW,EAC5D,IAAI,KAAK,kEAAkE,EAC3E,MACF,CAGA,IAAM,UAAY,KAAK,KAAK,UAAW,YAAY,EACnD,GAAI,WAAW,SAAS,EACtB,KAAK,UAAY,aAAa,UAAW,OAAO,MAC3C,CACL,IAAI,KAAK,2BAA2B,KAAK,WAAW,EACpD,MACF,CAGA,GAAI,CAEF,IAAM,QAAA,UAAkB,SAAS,EAC3B,WAAkB,CAAC,EAErB,KAAK,eACP,WAAW,YAAc,IAAU,WAAqB,CAClD,SAAS,SAAS,OAAO,EAC3B,IAAI,UAAU,gBAAiB,KAAK,iBAAiB,EAErD,IAAI,UAAU,gBAAiB,KAAK,YAAsB,CAE9D,GAGF,IAAI,IAAI,QAAQ,OAAO,KAAK,UAAW,UAAU,CAAC,CACpD,MAAQ,CACN,IAAI,MAAM,iEAAiE,EAC3E,MACF,CAEA,IAAI,MAAM,oBAAoB,KAAK,WAAW,CAChD,CAEA,YAAY,CAAE,KAA6B,CACpC,KAAK,WAGV,IAAI,KAAK,IAAU,IAAU,OAAc,CAEzC,IAAK,IAAM,UAAU,KAAK,YACxB,GAAI,IAAI,KAAK,WAAW,MAAM,EAAG,OAAO,KAAK,EAI/C,IAAK,IAAM,QAAQ,KAAK,aACtB,GAAI,IAAI,KAAK,WAAW,IAAI,EAAG,OAAO,KAAK,EAO7C,GAHI,IAAI,KAAK,SAAS,GAAG,GAGrB,IAAI,SAAW,MAAO,OAAO,KAAK,EAGtC,IAAI,UAAU,eAAgB,WAAW,EACzC,IAAI,UAAU,gBAAiB,KAAK,iBAAiB,EACrD,IAAI,KAAK,KAAK,SAAS,CACzB,CAAC,CACH,CACF"}
@@ -1,3 +1,3 @@
1
1
 
2
- import { a as upload, i as resolveMimeTypes, n as buildUploadMiddleware, r as cleanupFiles, t as UploadOptions } from "../../upload-DqRwWhX0.mjs";
2
+ import { a as upload, i as resolveMimeTypes, n as buildUploadMiddleware, r as cleanupFiles, t as UploadOptions } from "../../upload-5goxbO9A.mjs";
3
3
  export { UploadOptions, buildUploadMiddleware, cleanupFiles, resolveMimeTypes, upload };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.7.0
2
+ * @forinda/kickjs v5.8.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *