@warlock.js/core 5.14.0 → 5.16.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 (46) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/esm/application/application-config-types.d.mts +9 -0
  3. package/esm/application/application-config-types.d.mts.map +1 -1
  4. package/esm/application/index.d.mts +2 -1
  5. package/esm/application/index.mjs +1 -0
  6. package/esm/application/public-url.d.mts +20 -0
  7. package/esm/application/public-url.d.mts.map +1 -0
  8. package/esm/application/public-url.mjs +26 -0
  9. package/esm/application/public-url.mjs.map +1 -0
  10. package/esm/generations/features/index.mjs +2 -0
  11. package/esm/generations/features/index.mjs.map +1 -1
  12. package/esm/generations/features/shared/merge-web-sitemap-config.mjs +118 -0
  13. package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -0
  14. package/esm/generations/features/sitemap.feature.mjs +77 -0
  15. package/esm/generations/features/sitemap.feature.mjs.map +1 -0
  16. package/esm/http/errors/errors.d.mts +16 -1
  17. package/esm/http/errors/errors.d.mts.map +1 -1
  18. package/esm/http/errors/errors.mjs +19 -1
  19. package/esm/http/errors/errors.mjs.map +1 -1
  20. package/esm/http/index.d.mts +2 -1
  21. package/esm/http/index.mjs +1 -1
  22. package/esm/http/parse-urlencoded-body.mjs +27 -0
  23. package/esm/http/parse-urlencoded-body.mjs.map +1 -0
  24. package/esm/http/plugins.d.mts.map +1 -1
  25. package/esm/http/plugins.mjs +8 -0
  26. package/esm/http/plugins.mjs.map +1 -1
  27. package/esm/http/request.d.mts +8 -0
  28. package/esm/http/request.d.mts.map +1 -1
  29. package/esm/http/request.mjs +13 -1
  30. package/esm/http/request.mjs.map +1 -1
  31. package/esm/http/response.d.mts +9 -2
  32. package/esm/http/response.d.mts.map +1 -1
  33. package/esm/http/response.mjs +12 -4
  34. package/esm/http/response.mjs.map +1 -1
  35. package/esm/http/xmlable.d.mts +14 -0
  36. package/esm/http/xmlable.d.mts.map +1 -0
  37. package/esm/index.d.mts +4 -2
  38. package/esm/index.mjs +3 -2
  39. package/llms-full.txt +74 -3
  40. package/llms.txt +2 -2
  41. package/package.json +11 -11
  42. package/skills/configure-app/SKILL.md +29 -2
  43. package/skills/create-controller/SKILL.md +14 -0
  44. package/skills/health-checks/SKILL.md +8 -0
  45. package/skills/send-response/SKILL.md +17 -1
  46. package/skills/use-localization/SKILL.md +6 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  > ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
8
8
 
9
+ ## 5.16.0 - 2026-09-18
10
+
11
+ ### Added
12
+
13
+ - Core now parses `application/x-www-form-urlencoded` request bodies, so plain HTML forms and OAuth `form_post` callbacks (Apple) reach their routes instead of failing with `FST_ERR_CTP_INVALID_MEDIA_TYPE`. These bodies have the same `http.bodyLimit` as JSON. A key sent more than once becomes an array.
14
+ - `response.xml()` accepts a raw string or any value with a `toXML(): string` method (such as a `@warlock.js/sitemap` `Sitemap`), and sends `application/xml`.
15
+
16
+ ### Changed
17
+
18
+ - `warlock add sitemap` now merges a disabled `sitemap` section into `src/config/web.ts`, and creates that file when it is missing. It no longer writes `src/config/sitemap.ts` or registers `sitemapConnector()`. The merge edits only a `sitemap` key directly on the exported config object, and the result is re-parsed before it is written.
19
+
20
+ ## 5.15.0 - 2026-09-18
21
+
22
+ ### Added
23
+
24
+ - `warlock add sitemap` installs `@warlock.js/sitemap`, writes `src/config/sitemap.ts`, and registers `sitemapConnector()` in `warlock.config.ts`. The generated config ships **disabled**: a sitemap needs the application public origin and a generated app cannot know it, so the block explains the two steps to turn it on rather than producing an app that refuses to boot.
25
+ - `app.publicUrl` config key, with a `PUBLIC_APP_URL` environment fallback: the one absolute-URL source for every consumer that needs an origin. It never falls back to a request-derived host — an absolute URL built from the wrong host is worse than a boot that refuses to start, because nothing downstream reports it.
26
+
27
+ ### Changed
28
+
29
+ - **BREAKING (fail-loud):** `request.cookie(name)` and `request.hasCookie(name)` now throw `CookieJarUnavailableError` when `@fastify/cookie` is not registered on the Fastify instance, instead of returning `undefined` / `false`. An unregistered plugin is a configuration fault, and it was previously indistinguishable from "the caller sent no such cookie" — under `authMiddleware([], "cookie:token")` it surfaced as a permanent, unexplained 401. Apps built on `createHttpApplication` are unaffected: core registers the plugin before anything mounts. The exposure is a host that mounts a guarded surface on its own Fastify instance. `request.cookies` stays lenient and still returns `{}`, so the framework's own opportunistic reads — locale resolution among them — are unchanged.
30
+
9
31
  ## 5.14.0 - 2026-09-17
10
32
 
11
33
  ### Added
@@ -16,6 +16,15 @@ type AppConfigurations = {
16
16
  * @default localhost:
17
17
  */
18
18
  baseUrl?: string;
19
+ /**
20
+ * The application's public origin — the absolute URL other consumers (the
21
+ * sitemap route, canonical links, OG tags, absolute URLs in mail) build
22
+ * links against. Optional in general; falls back to the `PUBLIC_APP_URL`
23
+ * env var (see {@link getPublicUrl}). A consumer that requires it (e.g. the
24
+ * sitemap route) fails boot loudly, naming both, when neither is set — it
25
+ * never falls back to a request-derived origin.
26
+ */
27
+ publicUrl?: string;
19
28
  /**
20
29
  * Application timezone
21
30
  */
@@ -1 +1 @@
1
- {"version":3,"file":"application-config-types.d.mts","names":[],"sources":["../../../../../../../core/src/application/application-config-types.ts"],"mappings":";KAAY,iBAAA;EAAA;;;EAIV,OAAA;EAAA;;;;;EAMA,UAAA;EAcW;;;;;EARX,OAAA;;;;EAIA,QAAA;;;;EAIA,WAAA;AAAA"}
1
+ {"version":3,"file":"application-config-types.d.mts","names":[],"sources":["../../../../../../../core/src/application/application-config-types.ts"],"mappings":";KAAY,iBAAA;EAAA;;;EAIV,OAAA;EAAA;;;;;EAMA,UAAA;EAuBW;AAAA;;;;EAjBX,OAAA;;;;;;;;;EASA,SAAA;;;;EAIA,QAAA;;;;EAIA,WAAA;AAAA"}
@@ -1,4 +1,5 @@
1
1
  import { app } from "./app.mjs";
2
2
  import { Application, BootContext, BootListener, BootValidator, ShutdownListener } from "./application.mjs";
3
3
  import { BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BootSignal, BootSignalType, isBootSignal, sendBootSignal } from "./boot-signal.mjs";
4
- import { AppConfigurations } from "./application-config-types.mjs";
4
+ import { AppConfigurations } from "./application-config-types.mjs";
5
+ import { getPublicUrl } from "./public-url.mjs";
@@ -1,5 +1,6 @@
1
1
  import { BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, isBootSignal, sendBootSignal } from "./boot-signal.mjs";
2
2
  import { Application } from "./application.mjs";
3
3
  import { app } from "./app.mjs";
4
+ import { getPublicUrl } from "./public-url.mjs";
4
5
 
5
6
  export { };
@@ -0,0 +1,20 @@
1
+ //#region ../core/src/application/public-url.d.ts
2
+ /**
3
+ * The application's public origin — the ONE absolute-URL source every
4
+ * consumer that needs one (the sitemap route, canonical links, OG tags,
5
+ * absolute URLs in mail) reads instead of each keeping its own copy.
6
+ *
7
+ * `app.publicUrl` wins over the `PUBLIC_APP_URL` env fallback. Deliberately
8
+ * does not fall back further to a request-derived origin: a sitemap (or any
9
+ * other absolute URL) served from the wrong host is worse than a boot that
10
+ * refuses to start, because nothing downstream ever tells you it was wrong.
11
+ *
12
+ * Optional in general — most apps have no consumer that needs it yet.
13
+ * Returns `undefined` rather than throwing; a consumer that requires the
14
+ * value (e.g. the sitemap route, at boot) is responsible for failing loudly
15
+ * itself, naming both `app.publicUrl` and `PUBLIC_APP_URL`.
16
+ */
17
+ declare function getPublicUrl(): string | undefined;
18
+ //#endregion
19
+ export { getPublicUrl };
20
+ //# sourceMappingURL=public-url.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-url.d.mts","names":[],"sources":["../../../../../../../core/src/application/public-url.ts"],"mappings":";;AAiBA;;;;AAA4B;;;;;;;;;;iBAAZ,YAAA"}
@@ -0,0 +1,26 @@
1
+ import { config } from "../config/config-getter.mjs";
2
+ import "../config/index.mjs";
3
+
4
+ //#region ../core/src/application/public-url.ts
5
+ /**
6
+ * The application's public origin — the ONE absolute-URL source every
7
+ * consumer that needs one (the sitemap route, canonical links, OG tags,
8
+ * absolute URLs in mail) reads instead of each keeping its own copy.
9
+ *
10
+ * `app.publicUrl` wins over the `PUBLIC_APP_URL` env fallback. Deliberately
11
+ * does not fall back further to a request-derived origin: a sitemap (or any
12
+ * other absolute URL) served from the wrong host is worse than a boot that
13
+ * refuses to start, because nothing downstream ever tells you it was wrong.
14
+ *
15
+ * Optional in general — most apps have no consumer that needs it yet.
16
+ * Returns `undefined` rather than throwing; a consumer that requires the
17
+ * value (e.g. the sitemap route, at boot) is responsible for failing loudly
18
+ * itself, naming both `app.publicUrl` and `PUBLIC_APP_URL`.
19
+ */
20
+ function getPublicUrl() {
21
+ return config.key("app.publicUrl") || process.env.PUBLIC_APP_URL || void 0;
22
+ }
23
+
24
+ //#endregion
25
+ export { getPublicUrl };
26
+ //# sourceMappingURL=public-url.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-url.mjs","names":[],"sources":["../../../../../../../core/src/application/public-url.ts"],"sourcesContent":["import { config } from \"../config\";\n\n/**\n * The application's public origin — the ONE absolute-URL source every\n * consumer that needs one (the sitemap route, canonical links, OG tags,\n * absolute URLs in mail) reads instead of each keeping its own copy.\n *\n * `app.publicUrl` wins over the `PUBLIC_APP_URL` env fallback. Deliberately\n * does not fall back further to a request-derived origin: a sitemap (or any\n * other absolute URL) served from the wrong host is worse than a boot that\n * refuses to start, because nothing downstream ever tells you it was wrong.\n *\n * Optional in general — most apps have no consumer that needs it yet.\n * Returns `undefined` rather than throwing; a consumer that requires the\n * value (e.g. the sitemap route, at boot) is responsible for failing loudly\n * itself, naming both `app.publicUrl` and `PUBLIC_APP_URL`.\n */\nexport function getPublicUrl(): string | undefined {\n return config.key<string | undefined>(\"app.publicUrl\") || process.env.PUBLIC_APP_URL || undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAAmC;CACjD,OAAO,OAAO,IAAwB,eAAe,KAAK,QAAQ,IAAI,kBAAkB;AAC1F"}
@@ -26,6 +26,7 @@ import { s3Feature } from "./s3.feature.mjs";
26
26
  import { schedulerFeature } from "./scheduler.feature.mjs";
27
27
  import { sesFeature } from "./ses.feature.mjs";
28
28
  import { shadcnFeature } from "./shadcn.feature.mjs";
29
+ import { sitemapFeature } from "./sitemap.feature.mjs";
29
30
  import { socketFeature } from "./socket.feature.mjs";
30
31
  import { tailwindFeature } from "./tailwind.feature.mjs";
31
32
  import { testFeature } from "./test.feature.mjs";
@@ -56,6 +57,7 @@ const featuresMap = {
56
57
  web: webFeature,
57
58
  tailwind: tailwindFeature,
58
59
  shadcn: shadcnFeature,
60
+ sitemap: sitemapFeature,
59
61
  herald: heraldFeature,
60
62
  queue: queueFeature,
61
63
  "bull-board": bullBoardFeature,
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\r\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\r\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\r\nimport { aiGoogleFeature } from \"./ai-google.feature\";\r\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\r\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\r\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\r\nimport { aiToolsFeature } from \"./ai-tools.feature\";\r\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\r\nimport { aiFeature } from \"./ai.feature\";\r\nimport { authGoogleFeature } from \"./auth-google.feature\";\r\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\r\nimport { bullBoardFeature } from \"./bull-board.feature\";\r\nimport { heraldFeature } from \"./herald.feature\";\r\nimport { imageFeature } from \"./image.feature\";\r\nimport { mailFeature } from \"./mail.feature\";\r\nimport { mongodbFeature } from \"./mongodb.feature\";\r\nimport { mysqlFeature } from \"./mysql.feature\";\r\nimport { notificationsFeature } from \"./notifications.feature\";\r\nimport { postgresFeature } from \"./postgres.feature\";\r\nimport { queueFeature } from \"./queue.feature\";\r\nimport { reactEmailFeature } from \"./react-email.feature\";\r\nimport { reactFeature } from \"./react.feature\";\r\nimport { redisFeature } from \"./redis.feature\";\r\nimport { s3Feature } from \"./s3.feature\";\r\nimport { schedulerFeature } from \"./scheduler.feature\";\r\nimport { sesFeature } from \"./ses.feature\";\r\nimport { shadcnFeature } from \"./shadcn.feature\";\r\nimport { socketFeature } from \"./socket.feature\";\r\nimport { tailwindFeature } from \"./tailwind.feature\";\r\nimport { testFeature } from \"./test.feature\";\r\nimport type { FeatureDefinition } from \"./types\";\r\nimport { webFeature } from \"./web.feature\";\r\n\r\nexport type { FeatureDefinition } from \"./types\";\r\n\r\n/**\r\n * The feature registry `warlock add` dispatches against.\r\n *\r\n * This file is an INDEX, nothing more: every entry lives in its own module\r\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\r\n * the order `--list` prints and the order the \"not allowed\" error lists — so\r\n * add new features in the place they should appear, not alphabetically.\r\n */\r\nexport const featuresMap: Record<string, FeatureDefinition> = {\r\n \"react-email\": reactEmailFeature,\r\n react: reactFeature,\r\n image: imageFeature,\r\n mail: mailFeature,\r\n ses: sesFeature,\r\n mongodb: mongodbFeature,\r\n scheduler: schedulerFeature,\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: postgresFeature,\r\n mysql: mysqlFeature,\r\n redis: redisFeature,\r\n s3: s3Feature,\r\n test: testFeature,\r\n web: webFeature,\r\n // Directly after `web`, and only there: it `requires` it, it is useless\r\n // without it, and a reader scanning `--list` for the page stack should meet\r\n // the two together rather than find styling filed between queues and sockets.\r\n tailwind: tailwindFeature,\r\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\r\n // it `requires` it, it appends to the stylesheet that feature creates, and the\r\n // three of them are one stack a reader should meet in build order.\r\n shadcn: shadcnFeature,\r\n herald: heraldFeature,\r\n queue: queueFeature,\r\n // Directly after `queue`, for the same reason `tailwind` follows `web`: it\r\n // needs queue already installed and configured, and a reader scanning\r\n // `--list` for job-queue features should meet the two together.\r\n \"bull-board\": bullBoardFeature,\r\n socket: socketFeature,\r\n notifications: notificationsFeature,\r\n access: accessFeature,\r\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\r\n \"auth-google\": authGoogleFeature,\r\n \"auth-passkeys\": authPasskeysFeature,\r\n ai: aiFeature,\r\n \"ai-openai\": aiOpenaiFeature,\r\n \"ai-google\": aiGoogleFeature,\r\n \"ai-anthropic\": aiAnthropicFeature,\r\n \"ai-bedrock\": aiBedrockFeature,\r\n \"ai-ollama\": aiOllamaFeature,\r\n \"ai-tools\": aiToolsFeature,\r\n \"ai-panoptic\": aiPanopticFeature,\r\n \"ai-workspace\": aiWorkspaceFeature,\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,MAAa,cAAiD;CAC5D,eAAe;CACf,OAAO;CACP,OAAO;CACP,MAAM;CACN,KAAK;CACL,SAAS;CACT,WAAW;CAGX,UAAU;CACV,OAAO;CACP,OAAO;CACP,IAAI;CACJ,MAAM;CACN,KAAK;CAIL,UAAU;CAIV,QAAQ;CACR,QAAQ;CACR,OAAO;CAIP,cAAc;CACd,QAAQ;CACR,eAAe;CACf,QAAQ;CAER,eAAe;CACf,iBAAiB;CACjB,IAAI;CACJ,aAAa;CACb,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,aAAa;CACb,YAAY;CACZ,eAAe;CACf,gBAAgB;AAClB"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\r\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\r\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\r\nimport { aiGoogleFeature } from \"./ai-google.feature\";\r\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\r\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\r\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\r\nimport { aiToolsFeature } from \"./ai-tools.feature\";\r\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\r\nimport { aiFeature } from \"./ai.feature\";\r\nimport { authGoogleFeature } from \"./auth-google.feature\";\r\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\r\nimport { bullBoardFeature } from \"./bull-board.feature\";\r\nimport { heraldFeature } from \"./herald.feature\";\r\nimport { imageFeature } from \"./image.feature\";\r\nimport { mailFeature } from \"./mail.feature\";\r\nimport { mongodbFeature } from \"./mongodb.feature\";\r\nimport { mysqlFeature } from \"./mysql.feature\";\r\nimport { notificationsFeature } from \"./notifications.feature\";\r\nimport { postgresFeature } from \"./postgres.feature\";\r\nimport { queueFeature } from \"./queue.feature\";\r\nimport { reactEmailFeature } from \"./react-email.feature\";\r\nimport { reactFeature } from \"./react.feature\";\r\nimport { redisFeature } from \"./redis.feature\";\r\nimport { s3Feature } from \"./s3.feature\";\r\nimport { schedulerFeature } from \"./scheduler.feature\";\r\nimport { sesFeature } from \"./ses.feature\";\r\nimport { shadcnFeature } from \"./shadcn.feature\";\r\nimport { sitemapFeature } from \"./sitemap.feature\";\r\nimport { socketFeature } from \"./socket.feature\";\r\nimport { tailwindFeature } from \"./tailwind.feature\";\r\nimport { testFeature } from \"./test.feature\";\r\nimport type { FeatureDefinition } from \"./types\";\r\nimport { webFeature } from \"./web.feature\";\r\n\r\nexport type { FeatureDefinition } from \"./types\";\r\n\r\n/**\r\n * The feature registry `warlock add` dispatches against.\r\n *\r\n * This file is an INDEX, nothing more: every entry lives in its own module\r\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\r\n * the order `--list` prints and the order the \"not allowed\" error lists — so\r\n * add new features in the place they should appear, not alphabetically.\r\n */\r\nexport const featuresMap: Record<string, FeatureDefinition> = {\r\n \"react-email\": reactEmailFeature,\r\n react: reactFeature,\r\n image: imageFeature,\r\n mail: mailFeature,\r\n ses: sesFeature,\r\n mongodb: mongodbFeature,\r\n scheduler: schedulerFeature,\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: postgresFeature,\r\n mysql: mysqlFeature,\r\n redis: redisFeature,\r\n s3: s3Feature,\r\n test: testFeature,\r\n web: webFeature,\r\n // Directly after `web`, and only there: it `requires` it, it is useless\r\n // without it, and a reader scanning `--list` for the page stack should meet\r\n // the two together rather than find styling filed between queues and sockets.\r\n tailwind: tailwindFeature,\r\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\r\n // it `requires` it, it appends to the stylesheet that feature creates, and the\r\n // three of them are one stack a reader should meet in build order.\r\n shadcn: shadcnFeature,\r\n // Directly after the web/tailwind/shadcn stack, for the same reason: it\r\n // `requires` web (it reads the page registry `listRoutablePages()`\r\n // exposes) and a reader scanning `--list` for the page stack should meet it\r\n // there rather than filed between queues and sockets.\r\n sitemap: sitemapFeature,\r\n herald: heraldFeature,\r\n queue: queueFeature,\r\n // Directly after `queue`, for the same reason `tailwind` follows `web`: it\r\n // needs queue already installed and configured, and a reader scanning\r\n // `--list` for job-queue features should meet the two together.\r\n \"bull-board\": bullBoardFeature,\r\n socket: socketFeature,\r\n notifications: notificationsFeature,\r\n access: accessFeature,\r\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\r\n \"auth-google\": authGoogleFeature,\r\n \"auth-passkeys\": authPasskeysFeature,\r\n ai: aiFeature,\r\n \"ai-openai\": aiOpenaiFeature,\r\n \"ai-google\": aiGoogleFeature,\r\n \"ai-anthropic\": aiAnthropicFeature,\r\n \"ai-bedrock\": aiBedrockFeature,\r\n \"ai-ollama\": aiOllamaFeature,\r\n \"ai-tools\": aiToolsFeature,\r\n \"ai-panoptic\": aiPanopticFeature,\r\n \"ai-workspace\": aiWorkspaceFeature,\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,MAAa,cAAiD;CAC5D,eAAe;CACf,OAAO;CACP,OAAO;CACP,MAAM;CACN,KAAK;CACL,SAAS;CACT,WAAW;CAGX,UAAU;CACV,OAAO;CACP,OAAO;CACP,IAAI;CACJ,MAAM;CACN,KAAK;CAIL,UAAU;CAIV,QAAQ;CAKR,SAAS;CACT,QAAQ;CACR,OAAO;CAIP,cAAc;CACd,QAAQ;CACR,eAAe;CACf,QAAQ;CAER,eAAe;CACf,iBAAiB;CACjB,IAAI;CACJ,aAAa;CACb,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,aAAa;CACb,YAAY;CACZ,eAAe;CACf,gBAAgB;AAClB"}
@@ -0,0 +1,118 @@
1
+ import ts from "typescript";
2
+
3
+ //#region ../core/src/generations/features/shared/merge-web-sitemap-config.ts
4
+ /**
5
+ * Merge a disabled `sitemap` section into `src/config/web.ts` SOURCE TEXT.
6
+ *
7
+ * **String surgery, never parse-and-print** — same rationale as
8
+ * `insertConnectorEntry`: `web.ts` is app-owned and may carry any formatting
9
+ * or comments a parse-and-print would discard. The TypeScript AST is used to
10
+ * *find where to cut*, never to regenerate the file — the splice below still
11
+ * copies the original source's bytes verbatim.
12
+ *
13
+ * Recognises the exported config object two ways: an inline `export default
14
+ * {...}`, or `export default <identifier>;` pointing back at a `const
15
+ * <identifier> = {...}` (or `const <identifier>: SomeType = {...}`) declared
16
+ * earlier in the file — the shape this generator itself scaffolds, and the
17
+ * shape every other generated config file in this project uses. Any other
18
+ * shape (a function call, a re-export, no default export at all) returns
19
+ * `"unrecognised"` rather than guessing at where to cut — clobbering an
20
+ * app-owned file the merge could not actually understand is worse than
21
+ * refusing.
22
+ *
23
+ * A `sitemap` property already present anywhere in the source (by key, so one
24
+ * a human has since hand-edited is still found) is left unchanged — that is
25
+ * what makes a second `warlock add sitemap` a no-op instead of a duplicate
26
+ * block. Detection walks the AST rather than regex-matching `sitemap\s*:`, so
27
+ * a comment like `// sitemap: TODO` — text, not a property — does not count.
28
+ *
29
+ * Before returning a merge, the resulting text is re-parsed and checked for
30
+ * syntax errors; a merge that would not itself parse is refused (as
31
+ * `"unrecognised"`) instead of written, since a matched-but-corrupt splice is
32
+ * worse than doing nothing.
33
+ *
34
+ * @param source The config file's current text.
35
+ * @returns What happened, and the new text when there is any.
36
+ */
37
+ function mergeWebSitemapConfig(source) {
38
+ const sourceFile = ts.createSourceFile("web.ts", source, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
39
+ const objectLiteral = findExportedConfigObjectLiteral(sourceFile);
40
+ if (!objectLiteral) return { status: "unrecognised" };
41
+ if (hasTopLevelSitemapProperty(objectLiteral)) return { status: "already-present" };
42
+ const bodyStart = objectLiteral.getStart(sourceFile) + 1;
43
+ const closingBraceIndex = objectLiteral.getEnd() - 1;
44
+ const body = source.slice(bodyStart, closingBraceIndex);
45
+ const indentMatch = /\n([ \t]*)\S/.exec(body);
46
+ const indent = indentMatch ? indentMatch[1] : " ";
47
+ const block = `${indent}sitemap: {\n${indent}${indent}enabled: false,\n${indent}${indent}path: "/sitemap.xml",\n${indent}${indent}defaults: { changefreq: "weekly", priority: 0.5 },\n${indent}},\n`;
48
+ const { index: insertionPoint, needsComma } = lastPropertyEnd(source, closingBraceIndex);
49
+ const next = `${source.slice(0, insertionPoint)}${needsComma ? ",\n" : ""}${block}${source.slice(closingBraceIndex)}`;
50
+ if (hasSyntaxErrors(next)) return { status: "unrecognised" };
51
+ return {
52
+ status: "merged",
53
+ next
54
+ };
55
+ }
56
+ /** Whether TEXT fails to parse as TypeScript source. */
57
+ function hasSyntaxErrors(text) {
58
+ const { diagnostics } = ts.transpileModule(text, {
59
+ compilerOptions: {
60
+ module: ts.ModuleKind.ESNext,
61
+ target: ts.ScriptTarget.ESNext
62
+ },
63
+ reportDiagnostics: true
64
+ });
65
+ return Boolean(diagnostics && diagnostics.length > 0);
66
+ }
67
+ /**
68
+ * Whether the exported config object literal already has a `sitemap`
69
+ * property directly on it — a `sitemap` key nested under some other
70
+ * property (e.g. `seo: { sitemap: true }`) is a different feature's field
71
+ * and must not block adding the top-level one.
72
+ */
73
+ function hasTopLevelSitemapProperty(objectLiteral) {
74
+ return objectLiteral.properties.some((property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && propertyKeyName(property.name) === "sitemap");
75
+ }
76
+ /** The literal text of a property name node, when it is one of the plain shapes we care about. */
77
+ function propertyKeyName(name) {
78
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
79
+ }
80
+ /**
81
+ * Find the object literal a source file's default export ultimately points
82
+ * at, whether the export is inline or an identifier declared earlier in the
83
+ * file.
84
+ */
85
+ function findExportedConfigObjectLiteral(sourceFile) {
86
+ const exportAssignment = sourceFile.statements.find((statement) => ts.isExportAssignment(statement) && !statement.isExportEquals);
87
+ if (!exportAssignment) return void 0;
88
+ const { expression } = exportAssignment;
89
+ if (ts.isObjectLiteralExpression(expression)) return expression;
90
+ if (!ts.isIdentifier(expression)) return void 0;
91
+ const identifier = expression.text;
92
+ for (const statement of sourceFile.statements) {
93
+ if (!ts.isVariableStatement(statement)) continue;
94
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.name.text === identifier && declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) return declaration.initializer;
95
+ }
96
+ }
97
+ /**
98
+ * Where to insert a new property right before a closing brace, and whether
99
+ * the property immediately before it (if any) needs a trailing comma added —
100
+ * an object literal with two properties and no comma between them does not
101
+ * parse.
102
+ */
103
+ function lastPropertyEnd(source, closingBraceIndex) {
104
+ let cursor = closingBraceIndex - 1;
105
+ while (cursor >= 0 && /\s/.test(source.charAt(cursor))) cursor--;
106
+ if (cursor < 0 || source[cursor] === "{" || source[cursor] === ",") return {
107
+ index: closingBraceIndex,
108
+ needsComma: false
109
+ };
110
+ return {
111
+ index: cursor + 1,
112
+ needsComma: true
113
+ };
114
+ }
115
+
116
+ //#endregion
117
+ export { mergeWebSitemapConfig };
118
+ //# sourceMappingURL=merge-web-sitemap-config.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-web-sitemap-config.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/merge-web-sitemap-config.ts"],"sourcesContent":["import ts from \"typescript\";\n\n/** What {@link mergeWebSitemapConfig} did, or could not do, to the source it was given. */\nexport type WebSitemapMergeResult =\n { status: \"merged\"; next: string } | { status: \"already-present\" } | { status: \"unrecognised\" };\n\n/**\n * Merge a disabled `sitemap` section into `src/config/web.ts` SOURCE TEXT.\n *\n * **String surgery, never parse-and-print** — same rationale as\n * `insertConnectorEntry`: `web.ts` is app-owned and may carry any formatting\n * or comments a parse-and-print would discard. The TypeScript AST is used to\n * *find where to cut*, never to regenerate the file — the splice below still\n * copies the original source's bytes verbatim.\n *\n * Recognises the exported config object two ways: an inline `export default\n * {...}`, or `export default <identifier>;` pointing back at a `const\n * <identifier> = {...}` (or `const <identifier>: SomeType = {...}`) declared\n * earlier in the file — the shape this generator itself scaffolds, and the\n * shape every other generated config file in this project uses. Any other\n * shape (a function call, a re-export, no default export at all) returns\n * `\"unrecognised\"` rather than guessing at where to cut — clobbering an\n * app-owned file the merge could not actually understand is worse than\n * refusing.\n *\n * A `sitemap` property already present anywhere in the source (by key, so one\n * a human has since hand-edited is still found) is left unchanged — that is\n * what makes a second `warlock add sitemap` a no-op instead of a duplicate\n * block. Detection walks the AST rather than regex-matching `sitemap\\s*:`, so\n * a comment like `// sitemap: TODO` — text, not a property — does not count.\n *\n * Before returning a merge, the resulting text is re-parsed and checked for\n * syntax errors; a merge that would not itself parse is refused (as\n * `\"unrecognised\"`) instead of written, since a matched-but-corrupt splice is\n * worse than doing nothing.\n *\n * @param source The config file's current text.\n * @returns What happened, and the new text when there is any.\n */\nexport function mergeWebSitemapConfig(source: string): WebSitemapMergeResult {\n const sourceFile = ts.createSourceFile(\n \"web.ts\",\n source,\n ts.ScriptTarget.ESNext,\n /* setParentNodes */ true,\n ts.ScriptKind.TS,\n );\n\n const objectLiteral = findExportedConfigObjectLiteral(sourceFile);\n\n if (!objectLiteral) {\n return { status: \"unrecognised\" };\n }\n\n if (hasTopLevelSitemapProperty(objectLiteral)) {\n return { status: \"already-present\" };\n }\n\n const bodyStart = objectLiteral.getStart(sourceFile) + 1;\n const closingBraceIndex = objectLiteral.getEnd() - 1;\n const body = source.slice(bodyStart, closingBraceIndex);\n\n const indentMatch = /\\n([ \\t]*)\\S/.exec(body);\n const indent = indentMatch ? indentMatch[1] : \" \";\n\n const block =\n `${indent}sitemap: {\\n` +\n `${indent}${indent}enabled: false,\\n` +\n `${indent}${indent}path: \"/sitemap.xml\",\\n` +\n `${indent}${indent}defaults: { changefreq: \"weekly\", priority: 0.5 },\\n` +\n `${indent}},\\n`;\n\n const { index: insertionPoint, needsComma } = lastPropertyEnd(source, closingBraceIndex);\n\n const next =\n `${source.slice(0, insertionPoint)}` +\n `${needsComma ? \",\\n\" : \"\"}` +\n `${block}` +\n `${source.slice(closingBraceIndex)}`;\n\n if (hasSyntaxErrors(next)) {\n return { status: \"unrecognised\" };\n }\n\n return { status: \"merged\", next };\n}\n\n/** Whether TEXT fails to parse as TypeScript source. */\nfunction hasSyntaxErrors(text: string): boolean {\n const { diagnostics } = ts.transpileModule(text, {\n compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext },\n reportDiagnostics: true,\n });\n\n return Boolean(diagnostics && diagnostics.length > 0);\n}\n\n/**\n * Whether the exported config object literal already has a `sitemap`\n * property directly on it — a `sitemap` key nested under some other\n * property (e.g. `seo: { sitemap: true }`) is a different feature's field\n * and must not block adding the top-level one.\n */\nfunction hasTopLevelSitemapProperty(objectLiteral: ts.ObjectLiteralExpression): boolean {\n return objectLiteral.properties.some(\n (property) =>\n (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&\n propertyKeyName(property.name) === \"sitemap\",\n );\n}\n\n/** The literal text of a property name node, when it is one of the plain shapes we care about. */\nfunction propertyKeyName(name: ts.PropertyName): string | undefined {\n if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {\n return name.text;\n }\n\n return undefined;\n}\n\n/**\n * Find the object literal a source file's default export ultimately points\n * at, whether the export is inline or an identifier declared earlier in the\n * file.\n */\nfunction findExportedConfigObjectLiteral(\n sourceFile: ts.SourceFile,\n): ts.ObjectLiteralExpression | undefined {\n const exportAssignment = sourceFile.statements.find(\n (statement): statement is ts.ExportAssignment =>\n ts.isExportAssignment(statement) && !statement.isExportEquals,\n );\n\n if (!exportAssignment) return undefined;\n\n const { expression } = exportAssignment;\n\n if (ts.isObjectLiteralExpression(expression)) {\n return expression;\n }\n\n if (!ts.isIdentifier(expression)) return undefined;\n\n const identifier = expression.text;\n\n for (const statement of sourceFile.statements) {\n if (!ts.isVariableStatement(statement)) continue;\n\n for (const declaration of statement.declarationList.declarations) {\n if (\n ts.isIdentifier(declaration.name) &&\n declaration.name.text === identifier &&\n declaration.initializer &&\n ts.isObjectLiteralExpression(declaration.initializer)\n ) {\n return declaration.initializer;\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Where to insert a new property right before a closing brace, and whether\n * the property immediately before it (if any) needs a trailing comma added —\n * an object literal with two properties and no comma between them does not\n * parse.\n */\nfunction lastPropertyEnd(\n source: string,\n closingBraceIndex: number,\n): { index: number; needsComma: boolean } {\n let cursor = closingBraceIndex - 1;\n\n while (cursor >= 0 && /\\s/.test(source.charAt(cursor))) {\n cursor--;\n }\n\n if (cursor < 0 || source[cursor] === \"{\" || source[cursor] === \",\") {\n return { index: closingBraceIndex, needsComma: false };\n }\n\n return { index: cursor + 1, needsComma: true };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,sBAAsB,QAAuC;CAC3E,MAAM,aAAa,GAAG,iBACpB,UACA,QACA,GAAG,aAAa,QACK,MACrB,GAAG,WAAW,EAChB;CAEA,MAAM,gBAAgB,gCAAgC,UAAU;CAEhE,IAAI,CAAC,eACH,OAAO,EAAE,QAAQ,eAAe;CAGlC,IAAI,2BAA2B,aAAa,GAC1C,OAAO,EAAE,QAAQ,kBAAkB;CAGrC,MAAM,YAAY,cAAc,SAAS,UAAU,IAAI;CACvD,MAAM,oBAAoB,cAAc,OAAO,IAAI;CACnD,MAAM,OAAO,OAAO,MAAM,WAAW,iBAAiB;CAEtD,MAAM,cAAc,eAAe,KAAK,IAAI;CAC5C,MAAM,SAAS,cAAc,YAAY,KAAK;CAE9C,MAAM,QACJ,GAAG,OAAO,cACP,SAAS,OAAO,mBAChB,SAAS,OAAO,yBAChB,SAAS,OAAO,sDAChB,OAAO;CAEZ,MAAM,EAAE,OAAO,gBAAgB,eAAe,gBAAgB,QAAQ,iBAAiB;CAEvF,MAAM,OACJ,GAAG,OAAO,MAAM,GAAG,cAAc,IAC9B,aAAa,QAAQ,KACrB,QACA,OAAO,MAAM,iBAAiB;CAEnC,IAAI,gBAAgB,IAAI,GACtB,OAAO,EAAE,QAAQ,eAAe;CAGlC,OAAO;EAAE,QAAQ;EAAU;CAAK;AAClC;;AAGA,SAAS,gBAAgB,MAAuB;CAC9C,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,MAAM;EAC/C,iBAAiB;GAAE,QAAQ,GAAG,WAAW;GAAQ,QAAQ,GAAG,aAAa;EAAO;EAChF,mBAAmB;CACrB,CAAC;CAED,OAAO,QAAQ,eAAe,YAAY,SAAS,CAAC;AACtD;;;;;;;AAQA,SAAS,2BAA2B,eAAoD;CACtF,OAAO,cAAc,WAAW,MAC7B,cACE,GAAG,qBAAqB,QAAQ,KAAK,GAAG,8BAA8B,QAAQ,MAC/E,gBAAgB,SAAS,IAAI,MAAM,SACvC;AACF;;AAGA,SAAS,gBAAgB,MAA2C;CAClE,IAAI,GAAG,aAAa,IAAI,KAAK,GAAG,gBAAgB,IAAI,KAAK,GAAG,iBAAiB,IAAI,GAC/E,OAAO,KAAK;AAIhB;;;;;;AAOA,SAAS,gCACP,YACwC;CACxC,MAAM,mBAAmB,WAAW,WAAW,MAC5C,cACC,GAAG,mBAAmB,SAAS,KAAK,CAAC,UAAU,cACnD;CAEA,IAAI,CAAC,kBAAkB,OAAO;CAE9B,MAAM,EAAE,eAAe;CAEvB,IAAI,GAAG,0BAA0B,UAAU,GACzC,OAAO;CAGT,IAAI,CAAC,GAAG,aAAa,UAAU,GAAG,OAAO;CAEzC,MAAM,aAAa,WAAW;CAE9B,KAAK,MAAM,aAAa,WAAW,YAAY;EAC7C,IAAI,CAAC,GAAG,oBAAoB,SAAS,GAAG;EAExC,KAAK,MAAM,eAAe,UAAU,gBAAgB,cAClD,IACE,GAAG,aAAa,YAAY,IAAI,KAChC,YAAY,KAAK,SAAS,cAC1B,YAAY,eACZ,GAAG,0BAA0B,YAAY,WAAW,GAEpD,OAAO,YAAY;CAGzB;AAGF;;;;;;;AAQA,SAAS,gBACP,QACA,mBACwC;CACxC,IAAI,SAAS,oBAAoB;CAEjC,OAAO,UAAU,KAAK,KAAK,KAAK,OAAO,OAAO,MAAM,CAAC,GACnD;CAGF,IAAI,SAAS,KAAK,OAAO,YAAY,OAAO,OAAO,YAAY,KAC7D,OAAO;EAAE,OAAO;EAAmB,YAAY;CAAM;CAGvD,OAAO;EAAE,OAAO,SAAS;EAAG,YAAY;CAAK;AAC/C"}
@@ -0,0 +1,77 @@
1
+ import { srcPath } from "../../utils/paths.mjs";
2
+ import "../../utils/index.mjs";
3
+ import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
4
+ import { mergeWebSitemapConfig } from "./shared/merge-web-sitemap-config.mjs";
5
+ import { colors } from "@mongez/copper";
6
+ import { fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
7
+
8
+ //#region ../core/src/generations/features/sitemap.feature.ts
9
+ const NEXT_STEPS = "Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment variable, then flip `sitemap.enabled` to true in src/config/web.ts.";
10
+ /**
11
+ * Ships DISABLED because a sitemap needs this application's public origin and
12
+ * a freshly generated app has no way to know it. Two steps to turn it on:
13
+ *
14
+ * 1. set `app.publicUrl` in src/config/app.ts, or the PUBLIC_APP_URL
15
+ * environment variable;
16
+ * 2. flip `sitemap.enabled` to true here.
17
+ *
18
+ * With it enabled and no origin configured, generation REFUSES rather than
19
+ * serving absolute URLs built from a guessed host — a sitemap pointing at
20
+ * the wrong domain is worse than one that never starts, because nothing
21
+ * downstream reports it.
22
+ */
23
+ const freshWebConfigStub = `import type { WebSitemapConfig } from "@warlock.js/web/sitemap";
24
+
25
+ const webConfig: { sitemap: WebSitemapConfig } = {
26
+ sitemap: {
27
+ enabled: false,
28
+ path: "/sitemap.xml",
29
+ defaults: { changefreq: "weekly", priority: 0.5 },
30
+ },
31
+ };
32
+
33
+ export default webConfig;
34
+ `;
35
+ /**
36
+ * `warlock add sitemap` writes its policy into the app's \`src/config/web.ts\`,
37
+ * under the \`sitemap\` key — there is no \`src/config/sitemap.ts\` and no
38
+ * standalone connector to register; \`@warlock.js/web\` (already installed via
39
+ * the \`web\` requirement) reads \`web.sitemap\` itself.
40
+ *
41
+ * Creates \`web.ts\` when the app does not have one yet; otherwise merges a
42
+ * \`sitemap\` section into whatever is already there rather than clobbering it.
43
+ */
44
+ async function installSitemapConfig() {
45
+ const configPath = srcPath("config/web.ts");
46
+ if (!await fileExistsAsync(configPath)) {
47
+ await putFileAsync(configPath, freshWebConfigStub);
48
+ console.log(`${colors.green("✓")} Created src/config/web.ts with a disabled sitemap section`);
49
+ console.log(NEXT_STEPS);
50
+ return;
51
+ }
52
+ const merge = mergeWebSitemapConfig(await getFileAsync(configPath));
53
+ if (merge.status === "already-present") {
54
+ console.log(`${colors.yellowBright("sitemap")} already configured in src/config/web.ts, skipping...`);
55
+ return;
56
+ }
57
+ if (merge.status === "unrecognised") {
58
+ console.log(`${colors.redBright("✗")} Could not safely add a \`sitemap\` section to ${colors.yellowBright("src/config/web.ts")} — its exported config object was not recognised, so nothing was written. Add it yourself:
59
+ sitemap: { enabled: false, path: "/sitemap.xml", defaults: { changefreq: "weekly", priority: 0.5 } },`);
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ await putFileAsync(configPath, merge.next);
64
+ console.log(`${colors.green("✓")} Added a disabled \`sitemap\` section to src/config/web.ts`);
65
+ console.log(NEXT_STEPS);
66
+ }
67
+ /** `warlock add sitemap` — runtime sitemap.xml generation, backed by the page registry. */
68
+ const sitemapFeature = {
69
+ description: "Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. Adds a disabled `sitemap` section to src/config/web.ts, creating the file if it doesn't exist yet. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.",
70
+ requires: ["web"],
71
+ dependencies: { "@warlock.js/sitemap": INSTALLED_WARLOCK_VERSION },
72
+ onExecuting: installSitemapConfig
73
+ };
74
+
75
+ //#endregion
76
+ export { sitemapFeature };
77
+ //# sourceMappingURL=sitemap.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/sitemap.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { srcPath } from \"../../utils\";\nimport { mergeWebSitemapConfig } from \"./shared/merge-web-sitemap-config\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\nconst NEXT_STEPS =\n \"Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment \" +\n \"variable, then flip `sitemap.enabled` to true in src/config/web.ts.\";\n\n/**\n * Ships DISABLED because a sitemap needs this application's public origin and\n * a freshly generated app has no way to know it. Two steps to turn it on:\n *\n * 1. set `app.publicUrl` in src/config/app.ts, or the PUBLIC_APP_URL\n * environment variable;\n * 2. flip `sitemap.enabled` to true here.\n *\n * With it enabled and no origin configured, generation REFUSES rather than\n * serving absolute URLs built from a guessed host — a sitemap pointing at\n * the wrong domain is worse than one that never starts, because nothing\n * downstream reports it.\n */\nconst freshWebConfigStub = `import type { WebSitemapConfig } from \"@warlock.js/web/sitemap\";\n\nconst webConfig: { sitemap: WebSitemapConfig } = {\n sitemap: {\n enabled: false,\n path: \"/sitemap.xml\",\n defaults: { changefreq: \"weekly\", priority: 0.5 },\n },\n};\n\nexport default webConfig;\n`;\n\n/**\n * `warlock add sitemap` writes its policy into the app's \\`src/config/web.ts\\`,\n * under the \\`sitemap\\` key — there is no \\`src/config/sitemap.ts\\` and no\n * standalone connector to register; \\`@warlock.js/web\\` (already installed via\n * the \\`web\\` requirement) reads \\`web.sitemap\\` itself.\n *\n * Creates \\`web.ts\\` when the app does not have one yet; otherwise merges a\n * \\`sitemap\\` section into whatever is already there rather than clobbering it.\n */\nasync function installSitemapConfig(): Promise<void> {\n const configPath = srcPath(\"config/web.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n await putFileAsync(configPath, freshWebConfigStub);\n console.log(`${colors.green(\"✓\")} Created src/config/web.ts with a disabled sitemap section`);\n console.log(NEXT_STEPS);\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n const merge = mergeWebSitemapConfig(current);\n\n if (merge.status === \"already-present\") {\n console.log(\n `${colors.yellowBright(\"sitemap\")} already configured in src/config/web.ts, skipping...`,\n );\n\n return;\n }\n\n if (merge.status === \"unrecognised\") {\n console.log(\n `${colors.redBright(\"✗\")} Could not safely add a \\`sitemap\\` section to ` +\n `${colors.yellowBright(\"src/config/web.ts\")} — its exported config object was not ` +\n \"recognised, so nothing was written. Add it yourself:\\n\" +\n ' sitemap: { enabled: false, path: \"/sitemap.xml\", defaults: { changefreq: \"weekly\", priority: 0.5 } },',\n );\n\n process.exitCode = 1;\n\n return;\n }\n\n await putFileAsync(configPath, merge.next);\n console.log(`${colors.green(\"✓\")} Added a disabled \\`sitemap\\` section to src/config/web.ts`);\n console.log(NEXT_STEPS);\n}\n\n/** `warlock add sitemap` — runtime sitemap.xml generation, backed by the page registry. */\nexport const sitemapFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. \" +\n \"Adds a disabled `sitemap` section to src/config/web.ts, creating the file if it doesn't \" +\n \"exist yet. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.\",\n requires: [\"web\"],\n dependencies: {\n \"@warlock.js/sitemap\": INSTALLED_WARLOCK_VERSION,\n },\n onExecuting: installSitemapConfig,\n};\n"],"mappings":";;;;;;;;AAMA,MAAM,aACJ;;;;;;;;;;;;;;AAgBF,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;AAsB3B,eAAe,uBAAsC;CACnD,MAAM,aAAa,QAAQ,eAAe;CAE1C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,MAAM,aAAa,YAAY,kBAAkB;EACjD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2DAA2D;EAC5F,QAAQ,IAAI,UAAU;EAEtB;CACF;CAGA,MAAM,QAAQ,sBAAsB,MADd,aAAa,UAAU,CACF;CAE3C,IAAI,MAAM,WAAW,mBAAmB;EACtC,QAAQ,IACN,GAAG,OAAO,aAAa,SAAS,EAAE,sDACpC;EAEA;CACF;CAEA,IAAI,MAAM,WAAW,gBAAgB;EACnC,QAAQ,IACN,GAAG,OAAO,UAAU,GAAG,EAAE,iDACpB,OAAO,aAAa,mBAAmB,EAAE;wGAGhD;EAEA,QAAQ,WAAW;EAEnB;CACF;CAEA,MAAM,aAAa,YAAY,MAAM,IAAI;CACzC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2DAA2D;CAC5F,QAAQ,IAAI,UAAU;AACxB;;AAGA,MAAa,iBAAoC;CAC/C,aACE;CAGF,UAAU,CAAC,KAAK;CAChB,cAAc,EACZ,uBAAuB,0BACzB;CACA,aAAa;AACf"}
@@ -37,6 +37,21 @@ declare class NotAllowedError extends HttpError {
37
37
  payload?: any | undefined;
38
38
  constructor(message: string, payload?: any | undefined);
39
39
  }
40
+ /**
41
+ * Thrown by `Request.prototype.cookie` / `Request.prototype.hasCookie` when
42
+ * `@fastify/cookie` was never registered on this Fastify instance, so
43
+ * `baseRequest.cookies` is `undefined` rather than an (possibly empty) object.
44
+ *
45
+ * A by-name cookie read is a deliberate assertion by the caller — "this
46
+ * cookie should be readable here" — so an unavailable jar is a configuration
47
+ * fault, not an absent cookie, and must not be swallowed into a default
48
+ * value or a silent `false`. Contrast `Request.prototype.cookies`, which
49
+ * stays lenient because the framework's own opportunistic reads (e.g.
50
+ * locale resolution) must not throw on a request that simply has no jar.
51
+ */
52
+ declare class CookieJarUnavailableError extends Error {
53
+ constructor(cookieName: string);
54
+ }
40
55
  /**
41
56
  * Thrown by `Request.prototype.user` in development to catch a call site
42
57
  * still reading the removed `request.user` getter/setter after the 5.12.0
@@ -54,5 +69,5 @@ declare class RequestUserMovedError extends Error {
54
69
  constructor();
55
70
  }
56
71
  //#endregion
57
- export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
72
+ export { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
58
73
  //# sourceMappingURL=errors.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"mappings":";cAAa,SAAA,SAAkB,KAAK;EAEzB,MAAA;EACA,OAAA;EACA,OAAA;cAFA,MAAA,UACA,OAAA,UACA,OAAA;AAAA;AAAA,cAOE,qBAAA,SAA8B,SAAS;EAGzC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,iBAAA,SAA0B,SAAS;EAGrC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,cAAA,SAAuB,SAAS;EAGlC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,WAAA,SAAoB,SAAS;EAG/B,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,aAAA,SAAsB,SAAS;EAGjC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,kBAAA,SAA2B,SAAS;EAGtC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;;;;;;;;;;AAlDa;AAOxB;;;cA+Da,qBAAA,SAA8B,KAAK;EAAL,WAAA;AAAA"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"mappings":";cAAa,SAAA,SAAkB,KAAK;EAEzB,MAAA;EACA,OAAA;EACA,OAAA;cAFA,MAAA,UACA,OAAA,UACA,OAAA;AAAA;AAAA,cAOE,qBAAA,SAA8B,SAAS;EAGzC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,iBAAA,SAA0B,SAAS;EAGrC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,cAAA,SAAuB,SAAS;EAGlC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,WAAA,SAAoB,SAAS;EAG/B,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,aAAA,SAAsB,SAAS;EAGjC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,kBAAA,SAA2B,SAAS;EAGtC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;;;;;;;;;;AAlDa;AAOxB;;cA8Da,yBAAA,SAAkC,KAAK;cAC/B,UAAA;AAAA;;;;;;AA5DG;AAOxB;;;;;;;cA6Ea,qBAAA,SAA8B,KAAK;EAAL,WAAA;AAAA"}
@@ -65,6 +65,24 @@ var NotAllowedError = class extends HttpError {
65
65
  }
66
66
  };
67
67
  /**
68
+ * Thrown by `Request.prototype.cookie` / `Request.prototype.hasCookie` when
69
+ * `@fastify/cookie` was never registered on this Fastify instance, so
70
+ * `baseRequest.cookies` is `undefined` rather than an (possibly empty) object.
71
+ *
72
+ * A by-name cookie read is a deliberate assertion by the caller — "this
73
+ * cookie should be readable here" — so an unavailable jar is a configuration
74
+ * fault, not an absent cookie, and must not be swallowed into a default
75
+ * value or a silent `false`. Contrast `Request.prototype.cookies`, which
76
+ * stays lenient because the framework's own opportunistic reads (e.g.
77
+ * locale resolution) must not throw on a request that simply has no jar.
78
+ */
79
+ var CookieJarUnavailableError = class extends Error {
80
+ constructor(cookieName) {
81
+ super(`Cannot read cookie "${cookieName}": the cookie jar is unavailable because @fastify/cookie is not registered on this Fastify instance. Register the plugin (see core's http/plugins.ts) before reading cookies by name.`);
82
+ this.name = "CookieJarUnavailableError";
83
+ }
84
+ };
85
+ /**
68
86
  * Thrown by `Request.prototype.user` in development to catch a call site
69
87
  * still reading the removed `request.user` getter/setter after the 5.12.0
70
88
  * move: the authenticated user now lives at `request.locals.user`, written
@@ -85,5 +103,5 @@ var RequestUserMovedError = class extends Error {
85
103
  };
86
104
 
87
105
  //#endregion
88
- export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
106
+ export { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
89
107
  //# sourceMappingURL=errors.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"sourcesContent":["export class HttpError extends Error {\n public constructor(\n public status: number,\n public message: string,\n public payload?: any,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\nexport class ResourceNotFoundError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(404, message, payload);\n this.name = \"ResourceNotFoundError\";\n }\n}\n\nexport class UnAuthorizedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(401, message, payload);\n this.name = \"UnAuthorizedError\";\n }\n}\n\nexport class ForbiddenError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(403, message, payload);\n this.name = \"ForbiddenError\";\n }\n}\n\nexport class BadRequestError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(400, message, payload);\n this.name = \"BadRequestError\";\n }\n}\n\nexport class ServerError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(500, message, payload);\n this.name = \"ServerError\";\n }\n}\n\nexport class ConflictError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(409, message, payload);\n this.name = \"ConflictError\";\n }\n}\n\nexport class NotAcceptableError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(406, message, payload);\n this.name = \"NotAcceptableError\";\n }\n}\n\nexport class NotAllowedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(405, message, payload);\n this.name = \"NotAllowedError\";\n }\n}\n\n/**\n * Thrown by `Request.prototype.user` in development to catch a call site\n * still reading the removed `request.user` getter/setter after the 5.12.0\n * move: the authenticated user now lives at `request.locals.user`, written\n * by `@warlock.js/auth`'s middleware.\n *\n * Development-only diagnostic, not a runtime contract other code should\n * catch — the getter itself is typed `never`, so a caller that still\n * compiles against `request.user` only does so via `any`/an outdated type.\n * Kept in core (not auth) because `request.user` is a core `Request`\n * accessor and core cannot depend on `@warlock.js/auth` to react to it.\n * Slated for removal one release after 5.12.0 — see the CHANGELOG.\n */\nexport class RequestUserMovedError extends Error {\n public constructor() {\n super(\n \"request.user has been removed. The authenticated user now lives at \" +\n \"request.locals.user, set by @warlock.js/auth's middleware.\",\n );\n this.name = \"RequestUserMovedError\";\n }\n}\n"],"mappings":";AAAA,IAAa,YAAb,cAA+B,MAAM;CACnC,AAAO,YACL,AAAO,QACP,AAAO,SACP,AAAO,SACP;EACA,MAAM,OAAO;EAJN;EACA;EACA;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,wBAAb,cAA2C,UAAU;CACnD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,oBAAb,cAAuC,UAAU;CAC/C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,iBAAb,cAAoC,UAAU;CAC5C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,cAAb,cAAiC,UAAU;CACzC,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,gBAAb,cAAmC,UAAU;CAC3C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,qBAAb,cAAwC,UAAU;CAChD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;AAeA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,AAAO,cAAc;EACnB,MACE,+HAEF;EACA,KAAK,OAAO;CACd;AACF"}
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"sourcesContent":["export class HttpError extends Error {\n public constructor(\n public status: number,\n public message: string,\n public payload?: any,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\nexport class ResourceNotFoundError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(404, message, payload);\n this.name = \"ResourceNotFoundError\";\n }\n}\n\nexport class UnAuthorizedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(401, message, payload);\n this.name = \"UnAuthorizedError\";\n }\n}\n\nexport class ForbiddenError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(403, message, payload);\n this.name = \"ForbiddenError\";\n }\n}\n\nexport class BadRequestError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(400, message, payload);\n this.name = \"BadRequestError\";\n }\n}\n\nexport class ServerError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(500, message, payload);\n this.name = \"ServerError\";\n }\n}\n\nexport class ConflictError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(409, message, payload);\n this.name = \"ConflictError\";\n }\n}\n\nexport class NotAcceptableError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(406, message, payload);\n this.name = \"NotAcceptableError\";\n }\n}\n\nexport class NotAllowedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(405, message, payload);\n this.name = \"NotAllowedError\";\n }\n}\n\n/**\n * Thrown by `Request.prototype.cookie` / `Request.prototype.hasCookie` when\n * `@fastify/cookie` was never registered on this Fastify instance, so\n * `baseRequest.cookies` is `undefined` rather than an (possibly empty) object.\n *\n * A by-name cookie read is a deliberate assertion by the caller — \"this\n * cookie should be readable here\" — so an unavailable jar is a configuration\n * fault, not an absent cookie, and must not be swallowed into a default\n * value or a silent `false`. Contrast `Request.prototype.cookies`, which\n * stays lenient because the framework's own opportunistic reads (e.g.\n * locale resolution) must not throw on a request that simply has no jar.\n */\nexport class CookieJarUnavailableError extends Error {\n public constructor(cookieName: string) {\n super(\n `Cannot read cookie \"${cookieName}\": the cookie jar is unavailable because ` +\n \"@fastify/cookie is not registered on this Fastify instance. \" +\n \"Register the plugin (see core's http/plugins.ts) before reading cookies by name.\",\n );\n\n this.name = \"CookieJarUnavailableError\";\n }\n}\n\n/**\n * Thrown by `Request.prototype.user` in development to catch a call site\n * still reading the removed `request.user` getter/setter after the 5.12.0\n * move: the authenticated user now lives at `request.locals.user`, written\n * by `@warlock.js/auth`'s middleware.\n *\n * Development-only diagnostic, not a runtime contract other code should\n * catch — the getter itself is typed `never`, so a caller that still\n * compiles against `request.user` only does so via `any`/an outdated type.\n * Kept in core (not auth) because `request.user` is a core `Request`\n * accessor and core cannot depend on `@warlock.js/auth` to react to it.\n * Slated for removal one release after 5.12.0 — see the CHANGELOG.\n */\nexport class RequestUserMovedError extends Error {\n public constructor() {\n super(\n \"request.user has been removed. The authenticated user now lives at \" +\n \"request.locals.user, set by @warlock.js/auth's middleware.\",\n );\n this.name = \"RequestUserMovedError\";\n }\n}\n"],"mappings":";AAAA,IAAa,YAAb,cAA+B,MAAM;CACnC,AAAO,YACL,AAAO,QACP,AAAO,SACP,AAAO,SACP;EACA,MAAM,OAAO;EAJN;EACA;EACA;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,wBAAb,cAA2C,UAAU;CACnD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,oBAAb,cAAuC,UAAU;CAC/C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,iBAAb,cAAoC,UAAU;CAC5C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,cAAb,cAAiC,UAAU;CACzC,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,gBAAb,cAAmC,UAAU;CAC3C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,qBAAb,cAAwC,UAAU;CAChD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;AAcA,IAAa,4BAAb,cAA+C,MAAM;CACnD,AAAO,YAAY,YAAoB;EACrC,MACE,uBAAuB,WAAW,sLAGpC;EAEA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;AAeA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,AAAO,cAAc;EACnB,MACE,+HAEF;EACA,KAAK,OAAO;CACd;AACF"}
@@ -1,4 +1,5 @@
1
1
  import { PipeableReactStream, StreamReactResponseOptions, streamReactResponse } from "./stream-react-response.mjs";
2
+ import { XMLable } from "./xmlable.mjs";
2
3
  import { CookieOptions, Response, ResponseStatus, SendBufferOptions, SendFileOptions } from "./response.mjs";
3
4
  import { FileNamingStrategy, ImageTransformCallback, ImageTransformConfig, PrefixConfig, PrefixOptions, SaveAsOptions, SaveOptions, UploadedFileImageOptions, UploadsConfigurations } from "./uploads-types.mjs";
4
5
  import { FileValidationOptions, UploadedFile, UploadedFileJson } from "./uploaded-file.mjs";
@@ -14,7 +15,7 @@ import { logResponse, wrapResponseInDataKey } from "./events.mjs";
14
15
  import { HealthCheck, HealthStatus, health } from "./health.mjs";
15
16
  import { RequestController } from "./request-controller.mjs";
16
17
  import { UPLOADS_DEFAULTS, uploadsConfig } from "./uploads-config.mjs";
17
- import { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
18
+ import { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
18
19
  import { CacheMiddlewareOptions } from "./middleware/cache-response-middleware.mjs";
19
20
  import { ConcurrencyLimitOptions } from "./middleware/concurrency-limit.middleware.mjs";
20
21
  import { IdempotencyOptions } from "./middleware/idempotency.middleware.mjs";
@@ -1,6 +1,6 @@
1
1
  import { requestContext, useCurrentUser, useRequest, useRequestStore } from "./context/request-context.mjs";
2
2
  import { DEFAULT_CSP_DIRECTIVES, InvalidCspDirectiveError, applyCspHeader, buildCspHeaderValue, mergeCspDirectives, resolveCspConfig, serializeCspDirectives, validateCspConfigAtBoot, validateCspDirectives } from "./csp.mjs";
3
- import { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
3
+ import { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
4
4
  import { deriveTraceId, parseTraceparentTraceId } from "./tracing/trace-id.mjs";
5
5
  import { buildTracingContext, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, isTracingEnabled, resetTracingConfigForTests, resolveTracingConfig } from "./tracing/tracing-dispatcher.mjs";
6
6
  import "./tracing/index.mjs";
@@ -0,0 +1,27 @@
1
+ //#region ../core/src/http/parse-urlencoded-body.ts
2
+ /**
3
+ * Decode an `application/x-www-form-urlencoded` request body into a plain
4
+ * object, the shape `request.ts`'s `parseBody` already expects from JSON and
5
+ * multipart bodies.
6
+ *
7
+ * Repeated keys become arrays (`code=a&code=b` -> `{ code: ["a", "b"] }`);
8
+ * every other key stays a single string. Bracket-notation keys (`a[b]=1`) are
9
+ * NOT expanded here — they are handed through as literal keys and, like any
10
+ * JSON or query-string key of that shape, are expanded later by the shared
11
+ * `parseBody` nesting logic in `request.ts`. This function's only job is the
12
+ * urlencoded -> flat-object step; it does not duplicate that logic.
13
+ */
14
+ function parseUrlencodedBody(raw) {
15
+ const params = new URLSearchParams(raw);
16
+ const body = {};
17
+ for (const key of params.keys()) {
18
+ if (Object.prototype.hasOwnProperty.call(body, key)) continue;
19
+ const values = params.getAll(key);
20
+ body[key] = values.length > 1 ? values : values[0] ?? "";
21
+ }
22
+ return body;
23
+ }
24
+
25
+ //#endregion
26
+ export { parseUrlencodedBody };
27
+ //# sourceMappingURL=parse-urlencoded-body.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-urlencoded-body.mjs","names":[],"sources":["../../../../../../../core/src/http/parse-urlencoded-body.ts"],"sourcesContent":["/**\n * Decode an `application/x-www-form-urlencoded` request body into a plain\n * object, the shape `request.ts`'s `parseBody` already expects from JSON and\n * multipart bodies.\n *\n * Repeated keys become arrays (`code=a&code=b` -> `{ code: [\"a\", \"b\"] }`);\n * every other key stays a single string. Bracket-notation keys (`a[b]=1`) are\n * NOT expanded here — they are handed through as literal keys and, like any\n * JSON or query-string key of that shape, are expanded later by the shared\n * `parseBody` nesting logic in `request.ts`. This function's only job is the\n * urlencoded -> flat-object step; it does not duplicate that logic.\n */\nexport function parseUrlencodedBody(raw: string): Record<string, string | string[]> {\n const params = new URLSearchParams(raw);\n const body: Record<string, string | string[]> = {};\n\n for (const key of params.keys()) {\n if (Object.prototype.hasOwnProperty.call(body, key)) continue;\n\n const values = params.getAll(key);\n\n body[key] = values.length > 1 ? values : (values[0] ?? \"\");\n }\n\n return body;\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,oBAAoB,KAAgD;CAClF,MAAM,SAAS,IAAI,gBAAgB,GAAG;CACtC,MAAM,OAA0C,CAAC;CAEjD,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG;EAC/B,IAAI,OAAO,UAAU,eAAe,KAAK,MAAM,GAAG,GAAG;EAErD,MAAM,SAAS,OAAO,OAAO,GAAG;EAEhC,KAAK,OAAO,OAAO,SAAS,IAAI,SAAU,OAAO,MAAM;CACzD;CAEA,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"plugins.d.mts","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"mappings":";;;iBAMsB,mBAAA,CAAoB,MAAA,EAAQ,eAAA,GAAe,OAAA"}
1
+ {"version":3,"file":"plugins.d.mts","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"mappings":";;;iBAQsB,mBAAA,CAAoB,MAAA,EAAQ,eAAA,GAAe,OAAA"}
@@ -1,6 +1,7 @@
1
1
  import { rootPath } from "../utils/paths.mjs";
2
2
  import "../utils/index.mjs";
3
3
  import { buildCorsOptions } from "./build-cors-options.mjs";
4
+ import { parseUrlencodedBody } from "./parse-urlencoded-body.mjs";
4
5
  import config from "@mongez/config";
5
6
  import fastifyMultipart from "@fastify/multipart";
6
7
 
@@ -15,6 +16,13 @@ async function registerHttpPlugins(server) {
15
16
  attachFieldsToBody: true,
16
17
  limits: { fileSize: config.get("http.fileUploadLimit", 10 * 1024 * 1024) }
17
18
  });
19
+ server.addContentTypeParser("application/x-www-form-urlencoded", { parseAs: "string" }, (_request, body, done) => {
20
+ try {
21
+ done(null, parseUrlencodedBody(body));
22
+ } catch (error) {
23
+ done(error, void 0);
24
+ }
25
+ });
18
26
  server.register(import("@fastify/static"), {
19
27
  root: config.get("storage.publicRoot", rootPath("public")),
20
28
  prefix: config.get("storage.publicPrefix", "/public/")
@@ -1 +1 @@
1
- {"version":3,"file":"plugins.mjs","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"sourcesContent":["import fastifyMultipart from \"@fastify/multipart\";\nimport config from \"@mongez/config\";\nimport { rootPath } from \"../utils\";\nimport { buildCorsOptions } from \"./build-cors-options\";\nimport type { FastifyInstance } from \"./server\";\n\nexport async function registerHttpPlugins(server: FastifyInstance) {\n // 👇🏻 register rate-limit plugin\n server.register(import(\"@fastify/rate-limit\"), {\n // max requests per time window\n max: config.get(\"http.rateLimit.max\", 60),\n // maximum time that is will allow max requests\n timeWindow: config.get(\"http.rateLimit.duration\", 60 * 1000),\n });\n\n // 👇🏻 register cors plugin\n server.register(import(\"@fastify/cors\"), buildCorsOptions());\n\n // 👇🏻 import multipart plugin\n server.register(fastifyMultipart, {\n attachFieldsToBody: true,\n limits: {\n // file size could be up to 10MB\n fileSize: config.get(\"http.fileUploadLimit\", 10 * 1024 * 1024),\n },\n });\n\n server.register(import(\"@fastify/static\"), {\n root: config.get(\"storage.publicRoot\", rootPath(\"public\")),\n prefix: config.get(\"storage.publicPrefix\", \"/public/\"),\n });\n\n // 👇🏻 register cookie plugin\n server.register(import(\"@fastify/cookie\"), {\n secret: config.get(\"http.cookies.secret\"), // Optional: allow signed cookies\n parseOptions: config.get(\"http.cookies.options\", {}),\n });\n}\n"],"mappings":";;;;;;;AAMA,eAAsB,oBAAoB,QAAyB;CAEjE,OAAO,SAAS,OAAO,wBAAwB;EAE7C,KAAK,OAAO,IAAI,sBAAsB,EAAE;EAExC,YAAY,OAAO,IAAI,2BAA2B,KAAK,GAAI;CAC7D,CAAC;CAGD,OAAO,SAAS,OAAO,kBAAkB,iBAAiB,CAAC;CAG3D,OAAO,SAAS,kBAAkB;EAChC,oBAAoB;EACpB,QAAQ,EAEN,UAAU,OAAO,IAAI,wBAAwB,KAAK,OAAO,IAAI,EAC/D;CACF,CAAC;CAED,OAAO,SAAS,OAAO,oBAAoB;EACzC,MAAM,OAAO,IAAI,sBAAsB,SAAS,QAAQ,CAAC;EACzD,QAAQ,OAAO,IAAI,wBAAwB,UAAU;CACvD,CAAC;CAGD,OAAO,SAAS,OAAO,oBAAoB;EACzC,QAAQ,OAAO,IAAI,qBAAqB;EACxC,cAAc,OAAO,IAAI,wBAAwB,CAAC,CAAC;CACrD,CAAC;AACH"}
1
+ {"version":3,"file":"plugins.mjs","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"sourcesContent":["import fastifyMultipart from \"@fastify/multipart\";\nimport config from \"@mongez/config\";\nimport type { FastifyRequest } from \"fastify\";\nimport { rootPath } from \"../utils\";\nimport { buildCorsOptions } from \"./build-cors-options\";\nimport { parseUrlencodedBody } from \"./parse-urlencoded-body\";\nimport type { FastifyInstance } from \"./server\";\n\nexport async function registerHttpPlugins(server: FastifyInstance) {\n // 👇🏻 register rate-limit plugin\n server.register(import(\"@fastify/rate-limit\"), {\n // max requests per time window\n max: config.get(\"http.rateLimit.max\", 60),\n // maximum time that is will allow max requests\n timeWindow: config.get(\"http.rateLimit.duration\", 60 * 1000),\n });\n\n // 👇🏻 register cors plugin\n server.register(import(\"@fastify/cors\"), buildCorsOptions());\n\n // 👇🏻 import multipart plugin\n server.register(fastifyMultipart, {\n attachFieldsToBody: true,\n limits: {\n // file size could be up to 10MB\n fileSize: config.get(\"http.fileUploadLimit\", 10 * 1024 * 1024),\n },\n });\n\n // 👇🏻 parse application/x-www-form-urlencoded bodies (Apple's OAuth\n // `form_post` callback, plain HTML forms). Fastify only parses JSON and\n // text natively, so without this every urlencoded POST is rejected with\n // FST_ERR_CTP_INVALID_MEDIA_TYPE before it reaches a route.\n //\n // No `bodyLimit` option here: Fastify falls back to the server's own\n // `bodyLimit` (`server.ts`, from `http.bodyLimit`) whenever a content-type\n // parser doesn't set one — the exact limit JSON bodies are already held to.\n // Reading `http.bodyLimit` again here would duplicate that lookup and risk\n // the two silently drifting.\n server.addContentTypeParser(\n \"application/x-www-form-urlencoded\",\n { parseAs: \"string\" },\n (_request: FastifyRequest, body: string, done: (err: Error | null, body?: any) => void) => {\n try {\n done(null, parseUrlencodedBody(body));\n } catch (error) {\n done(error as Error, undefined);\n }\n },\n );\n\n server.register(import(\"@fastify/static\"), {\n root: config.get(\"storage.publicRoot\", rootPath(\"public\")),\n prefix: config.get(\"storage.publicPrefix\", \"/public/\"),\n });\n\n // 👇🏻 register cookie plugin\n server.register(import(\"@fastify/cookie\"), {\n secret: config.get(\"http.cookies.secret\"), // Optional: allow signed cookies\n parseOptions: config.get(\"http.cookies.options\", {}),\n });\n}\n"],"mappings":";;;;;;;;AAQA,eAAsB,oBAAoB,QAAyB;CAEjE,OAAO,SAAS,OAAO,wBAAwB;EAE7C,KAAK,OAAO,IAAI,sBAAsB,EAAE;EAExC,YAAY,OAAO,IAAI,2BAA2B,KAAK,GAAI;CAC7D,CAAC;CAGD,OAAO,SAAS,OAAO,kBAAkB,iBAAiB,CAAC;CAG3D,OAAO,SAAS,kBAAkB;EAChC,oBAAoB;EACpB,QAAQ,EAEN,UAAU,OAAO,IAAI,wBAAwB,KAAK,OAAO,IAAI,EAC/D;CACF,CAAC;CAYD,OAAO,qBACL,qCACA,EAAE,SAAS,SAAS,IACnB,UAA0B,MAAc,SAAkD;EACzF,IAAI;GACF,KAAK,MAAM,oBAAoB,IAAI,CAAC;EACtC,SAAS,OAAO;GACd,KAAK,OAAgB,MAAS;EAChC;CACF,CACF;CAEA,OAAO,SAAS,OAAO,oBAAoB;EACzC,MAAM,OAAO,IAAI,sBAAsB,SAAS,QAAQ,CAAC;EACzD,QAAQ,OAAO,IAAI,wBAAwB,UAAU;CACvD,CAAC;CAGD,OAAO,SAAS,OAAO,oBAAoB;EACzC,QAAQ,OAAO,IAAI,qBAAqB;EACxC,cAAc,OAAO,IAAI,wBAAwB,CAAC,CAAC;CACrD,CAAC;AACH"}