@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/llms-full.txt CHANGED
@@ -964,7 +964,7 @@ Don't call `setBaseUrl` per request — it's process-global and races every othe
964
964
 
965
965
  ---
966
966
  name: configure-app
967
- description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, and the `config()` getter for runtime reads. Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
967
+ description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
968
968
  ---
969
969
 
970
970
  # Warlock — configure the app
@@ -1069,7 +1069,7 @@ same nonce reaches the page's `<script>` tags.
1069
1069
  | CLI commands (registered via `warlock <cmd>`) | `warlock.config.ts > cli` |
1070
1070
  | HTTP server tuning per env (port, host, retry) | `warlock.config.ts > server` |
1071
1071
  | HTTP runtime (CORS, cookies, rate limits, upload size) | `src/config/http.ts` |
1072
- | App identity (name, baseUrl, timezone, locales) | `src/config/app.ts` |
1072
+ | App identity (name, baseUrl, publicUrl, timezone, locales) | `src/config/app.ts` |
1073
1073
  | Subsystem configs (auth, mail, storage, cache, ai, …) | `src/config/<name>.ts` |
1074
1074
 
1075
1075
  Heuristic: if the setting changes how the framework **boots, builds, or scaffolds**, it goes in `warlock.config.ts`. If it changes how a **subsystem behaves at runtime**, it goes in `src/config/`.
@@ -1203,6 +1203,33 @@ const config = {
1203
1203
 
1204
1204
  Avoid scattering `process.env.NODE_ENV === "production"` checks — they don't get the same default-handling.
1205
1205
 
1206
+ ### `app.publicUrl` — the app's public origin (5.15.0)
1207
+
1208
+ ```ts title="src/config/app.ts"
1209
+ import type { AppConfigurations } from "@warlock.js/core";
1210
+
1211
+ const appConfigurations: AppConfigurations = {
1212
+ appName: "My App",
1213
+ publicUrl: "https://example.com",
1214
+ };
1215
+
1216
+ export default appConfigurations;
1217
+ ```
1218
+
1219
+ The one absolute-URL source every consumer that needs one — the sitemap
1220
+ route, canonical links, OG tags, absolute URLs in mail — reads instead of
1221
+ keeping its own copy. Optional in general (most apps have no consumer that
1222
+ needs it yet); read it with `getPublicUrl()`, which returns `app.publicUrl`,
1223
+ falling back to the `PUBLIC_APP_URL` env var, or `undefined` when neither is
1224
+ set.
1225
+
1226
+ `getPublicUrl()` never throws — it is a consumer's job to fail loudly when it
1227
+ requires the value. `@warlock.js/web`'s sitemap is the first such consumer:
1228
+ with `web.sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
1229
+ generation refuses (`MissingPublicUrlError`, naming both) rather than falling
1230
+ back to a request-derived host — a sitemap pointing at the wrong host is
1231
+ worse than none. See `@warlock.js/web/generate-sitemap/SKILL.md`.
1232
+
1206
1233
  ## Common patterns
1207
1234
 
1208
1235
  ### Adding a new subsystem
@@ -1282,6 +1309,20 @@ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-
1282
1309
 
1283
1310
  Prefer `request.validated()` once a schema is attached — it's typed.
1284
1311
 
1312
+ ### Request body content types
1313
+
1314
+ `request.input()` / `.all()` / `.validated()` read the same way regardless of how the body arrived — every content type below feeds the same parsed bag:
1315
+
1316
+ | Content type | Parsed by | Notes |
1317
+ | ------------------------------------ | -------------------------------- | --------------------------------------------------------------------- |
1318
+ | `application/json` | Fastify (built-in) | objects/arrays parsed as-is |
1319
+ | `multipart/form-data` | `@fastify/multipart` | fields + files; see [`upload-file`](../upload-file/SKILL.md) |
1320
+ | `application/x-www-form-urlencoded` | Warlock's own content-type parser (`http/parse-urlencoded-body.ts`) | plain HTML forms, OAuth `form_post` callbacks (e.g. Apple Sign in) |
1321
+
1322
+ For urlencoded bodies: fields decode via `URLSearchParams`. A key sent more than once (`tag=a&tag=b`) becomes an array (`request.input("tag")` → `["a", "b"]`); every other key is a plain string. Bracket-notation keys (`a[b]=1`) are **not** expanded by the urlencoded parser itself — nesting only happens through the same shared bracket-key logic every body type already goes through, so it behaves exactly like a JSON or query-string key of that shape, no differently than today.
1323
+
1324
+ All three content types are held to the same `http.bodyLimit` — an over-limit urlencoded body is rejected with the same `413` a JSON body would get.
1325
+
1285
1326
  ## Returning output
1286
1327
 
1287
1328
  Pick the helper that matches the outcome. Full surface in [send-response](../send-response/SKILL.md). Quick map:
@@ -2389,6 +2430,8 @@ GET /ready → 200 {"status":"ok","checks":{"db":true}}
2389
2430
  ### Config
2390
2431
 
2391
2432
  ```ts title="src/config/http.ts"
2433
+ import type { HttpConfigurations } from "@warlock.js/core";
2434
+
2392
2435
  const httpConfigurations: HttpConfigurations = {
2393
2436
  health: {
2394
2437
  enabled: true, // default; set false to remove both endpoints
@@ -2396,6 +2439,8 @@ const httpConfigurations: HttpConfigurations = {
2396
2439
  readinessPath: "/ready", // readiness path
2397
2440
  },
2398
2441
  };
2442
+
2443
+ export default httpConfigurations;
2399
2444
  ```
2400
2445
 
2401
2446
  ## Readiness checks
@@ -2436,12 +2481,16 @@ On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks
2436
2481
  3. Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
2437
2482
 
2438
2483
  ```ts title="src/config/http.ts"
2484
+ import type { HttpConfigurations } from "@warlock.js/core";
2485
+
2439
2486
  const httpConfigurations: HttpConfigurations = {
2440
2487
  gracefulShutdown: {
2441
2488
  timeout: 10_000, // ms to wait for in-flight drain (default 10s)
2442
2489
  forceCloseConnections: "idle", // close idle keep-alives, let active finish (default)
2443
2490
  },
2444
2491
  };
2492
+
2493
+ export default httpConfigurations;
2445
2494
  ```
2446
2495
 
2447
2496
  `forceCloseConnections`: `"idle"` (default) closes idle keep-alive connections and lets active requests finish; `true` force-closes everything immediately; `false` waits for every connection.
@@ -4378,7 +4427,7 @@ await Mail.to(user.email)
4378
4427
 
4379
4428
  ---
4380
4429
  name: send-response
4381
- description: 'Send HTTP responses via @warlock.js/core''s Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.'
4430
+ description: 'Send HTTP responses via @warlock.js/core''s Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`, `request.cookie`, `request.hasCookie`, `CookieJarUnavailableError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.'
4382
4431
  ---
4383
4432
 
4384
4433
  # Warlock — send a response
@@ -4462,6 +4511,8 @@ return response.sendBuffer(buffer, { contentType: "image/png" });
4462
4511
 
4463
4512
  `SendFileOptions` lets you set `cacheTime`, `immutable`, `inline`, `filename` (download attachment name).
4464
4513
 
4514
+ `response.xml(string | XMLable, statusCode?)` sends `application/xml` — pass a raw XML string, or anything with a `toXML(): string` method (structural, so core never depends on `@warlock.js/sitemap`). This is for BOUNDED values only: a raw string, or a single `@warlock.js/sitemap` `Sitemap` under the sitemaps.org 50,000-URL / 50MB ceiling. `SitemapIndex` has no `toXML()` — it is the streaming path for larger URL sets and does not fit in one response body. It writes shards plus a master index as FILES via `saveTo`/`publish`, and those files are served directly rather than passed through `xml()`. In Warlock web this is the `web.sitemap` config: `enabled`, `outputDir` (default `storagePath("sitemap")`), and `path` (default `/sitemap.xml`) — the app serves the generated files from `outputDir` at that route, it never builds a `SitemapIndex` response inline.
4515
+
4465
4516
  ## Streams
4466
4517
 
4467
4518
  ```ts
@@ -4580,6 +4631,20 @@ Every response cookie gets `httpOnly: true`, `sameSite: "lax"`, and `secure: tru
4580
4631
 
4581
4632
  Precedence, lowest to highest: **framework defaults → `http.cookies.options` → the per-call `options` argument.** Set an app-wide policy in config, override per cookie when a specific one genuinely needs different treatment.
4582
4633
 
4634
+ ### Reading cookies back — `request.cookie()` / `request.hasCookie()` throw when the jar is unavailable (5.15.0)
4635
+
4636
+ `request.cookie(name)` and `request.hasCookie(name)` are a deliberate
4637
+ by-name assertion — "this cookie should be readable here" — so when
4638
+ `@fastify/cookie` is not registered on the Fastify instance, both now
4639
+ **throw `CookieJarUnavailableError`** naming the missing cookie, instead of
4640
+ silently returning `undefined` / `false`. Register the plugin (see core's
4641
+ `http/plugins.ts`) before reading cookies by name.
4642
+
4643
+ `request.cookies` (the plain getter, no by-name assertion) is unchanged and
4644
+ stays lenient — it returns `{}` when the jar is unavailable, because the
4645
+ framework's own opportunistic reads (e.g. locale resolution) must not throw
4646
+ on a request that simply has no jar.
4647
+
4583
4648
  These are the flags whose absence never fails a test and is fatal in production: the app works perfectly and is simply insecure. Opting out is now explicit and visible in review.
4584
4649
 
4585
4650
  These mutate the response in place; chain or call before the final `return response.<helper>()`.
@@ -6656,6 +6721,12 @@ All three lookups go through `@mongez/localization`'s `trans()` under the hood,
6656
6721
 
6657
6722
  When an app uses `@warlock.js/web`, `warlock dev` writes `.warlock/typings/translations.d.ts` from literal `groupedTranslations("group", { key: ... })` registrations. It augments web's `TranslationKeyRegistry`, so `useTrans()("products.notFound")` is checked against registered keys and a typo fails TypeScript. Before the generated file exists, `useTrans()` accepts `string` for a non-breaking first boot. Dynamic groups/keys and placeholders are not inferred.
6658
6723
 
6724
+ `useTrans()` now works correctly across hydration (5.15.0): the hydration
6725
+ payload ships a `translations` key with the active locale's keywords, and
6726
+ `@warlock.js/web` registers them into this same lookup table before the
6727
+ client hydrates — see `@warlock.js/web/write-the-root/SKILL.md`'s "`useTrans()`
6728
+ survives hydration" section for the failure this fixed.
6729
+
6659
6730
  ### Locale on a specific lookup
6660
6731
 
6661
6732
  ```ts
package/llms.txt CHANGED
@@ -10,7 +10,7 @@
10
10
  - [benchmark-code](@warlock.js/core/benchmark-code/SKILL.md): Wrap a function with `measure(name, fn, options?)` to time it and classify the latency — onComplete/onError/onFinish hooks, `latencyRange` thresholds, `BenchmarkProfiler` for percentiles, `BenchmarkSnapshots` for raw captures. Triggers: `measure`, `BenchmarkProfiler`, `BenchmarkSnapshots`, `BenchmarkChannel`, `ConsoleChannel`, `latencyRange`, `shouldBenchmarkError`; "time this operation", "profile a slow service", "emit p50/p95/p99 metrics", "classify latency against thresholds"; typical import `import { measure, BenchmarkProfiler } from "@warlock.js/core"`. Skip: retry composition — `@warlock.js/core/retry-operation/SKILL.md`; benchmark config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `prom-client`, `pino`, `perf_hooks`, `console.time`.
11
11
  - [build-restful](@warlock.js/core/build-restful/SKILL.md): Generate standard CRUD endpoints — via `router.route(path).list().show().create().update().destroy()` chain or the `Restful` base class. Pick the chain by default; reach for `Restful` when you want repository-bound defaults. Triggers: `router.route`, `Restful`, `router.restfulResource`, `RouteResource`, `.crud`, `.nest`, `beforeCreate`, `onCreate`; "build a CRUD API", "register list/show/create/update/destroy", "repository-bound default handlers", "override a single REST action"; typical import `import { router, Restful } from "@warlock.js/core"`. Skip: wider router surface — `@warlock.js/core/register-route/SKILL.md`; per-action controllers — `@warlock.js/core/create-controller/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing pattern: hand-rolled controllers, `@nestjs/swagger` decorator-driven CRUD.
12
12
  - [build-url](@warlock.js/core/build-url/SKILL.md): HTTP URL helpers — `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, anchored at `app.baseUrl`. Use to render `src` / `href` / API URLs in resources and responses. `setBaseUrl` is wired by the HTTP connector from `config.get("app.baseUrl")`. Triggers: `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, `setBaseUrl`, `BASE_URL`; "render an avatar src URL", "absolute download link", "embed asset URL in email", "URL helpers vs path helpers"; typical import `import { url, publicUrl, uploadsUrl } from "@warlock.js/core"`. Skip: filesystem paths — `@warlock.js/core/resolve-path/SKILL.md`; signed CDN URLs — `@warlock.js/core/store-file/SKILL.md`; resource output — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: hand-rolled `${baseUrl}/...` template strings.
13
- - [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, and the `config()` getter for runtime reads. Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.
13
+ - [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app's public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.
14
14
  - [create-controller](@warlock.js/core/create-controller/SKILL.md): Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.
15
15
  - [create-module](@warlock.js/core/create-module/SKILL.md): Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `npx warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.
16
16
  - [define-resource](@warlock.js/core/define-resource/SKILL.md): Map model fields to wire-shape via `defineResource()` or `Resource` subclasses. Output-only — never put business logic, hydration, or reconciliation in a resource. Triggers: `defineResource`, `Resource`, `RegisterResource`, `toJSON`, `"self"`, `"localized"`, `"uploadsUrl"`; "shape an API response", "nest related resources", "rename a field on output", "self-referential tree resource"; typical import `import { defineResource } from "@warlock.js/core"`. Skip: localized columns — `@warlock.js/core/use-localization/SKILL.md`; URL casting — `@warlock.js/core/build-url/SKILL.md`; controller side — `@warlock.js/core/create-controller/SKILL.md`; competing libs `@nestjs/swagger` `@ApiProperty`, `class-transformer`, hand-rolled DTO mappers.
@@ -26,7 +26,7 @@
26
26
  - [retry-operation](@warlock.js/core/retry-operation/SKILL.md): Wrap a flaky operation with `retry(fn, options)` — now provided by `@mongez/reinforcements` (not `@warlock.js/core`). `attempts` total tries, `delay` + `backoff` (linear/exponential/fn), `maxDelay`, `jitter`, `shouldRetry` to bail on permanent errors, `signal` to cancel, plus `retryable()` to pre-bind options. Triggers: `retry`, `retryable`, `RetryOptions`, `attempts`, `backoff`, `jitter`, `maxDelay`, `shouldRetry`, `signal`; "retry a flaky API call", "handle transient errors", "exponential backoff with jitter", "wrap an external request"; typical import `import { retry } from "@mongez/reinforcements"`. Skip: timing the retried op — `@warlock.js/core/benchmark-code/SKILL.md`; use-case-level `retry` option — `@warlock.js/core/write-use-case/SKILL.md`; competing libs `p-retry`, `async-retry`, `cockatiel`.
27
27
  - [run-app](@warlock.js/core/run-app/SKILL.md): Three operational commands — `warlock dev` (HMR + type-gen + health checks), `warlock build` (esbuild bundle), `warlock start` (spawn the production bundle). All flags, all `warlock.config.ts` knobs that shape them. Triggers: `warlock dev`, `warlock build`, `warlock start`, `devServer`, `--fresh`, `--skip-typings`, `--skip-health`, `outdir`, `outFile`, `sourcemap`, `PortInUseError`, `assertPortIsAvailable`, `EADDRINUSE`, `EsbuildBinaryMissingError`; "start the dev server", "build for production", "run the bundle", "skip type generation", "tune watch globs", "dev server keyboard shortcuts", "press r to restart", "press q to quit", "restart the dev server", "port already in use"; typical config `warlock.config.ts > devServer / build`. Skip: writing a custom CLI — `@warlock.js/core/write-cli-command/SKILL.md`; config shape — `@warlock.js/core/configure-app/SKILL.md`; competing tooling `nodemon`, `tsx`, `ts-node-dev`, `esbuild` direct.
28
28
  - [send-mail](@warlock.js/core/send-mail/SKILL.md): Send transactional email — `Mail` fluent builder, `sendMail()` direct call, React Email components. Test mode auto-captures into an in-memory mailbox; dev mode logs. Triggers: `Mail.to`, `sendMail`, `setMailMode`, `mailEvents`, `assertMailSent`, `getTestMailbox`, `wasMailSentTo`, `closeAllMailers`; "send a transactional email", "build a React Email template", "configure SMTP or SES", "assert an email was sent in tests"; typical import `import { Mail, sendMail } from "@warlock.js/core"`. Skip: per-config wiring — `@warlock.js/core/configure-app/SKILL.md`; layered service patterns — `@warlock.js/core/warlock-conventions/SKILL.md`; competing libs `nodemailer` direct, `@sendgrid/mail`, `resend`, `mailgun.js`.
29
- - [send-response](@warlock.js/core/send-response/SKILL.md): Send HTTP responses via @warlock.js/core's Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.
29
+ - [send-response](@warlock.js/core/send-response/SKILL.md): Send HTTP responses via @warlock.js/core's Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`, `request.cookie`, `request.hasCookie`, `CookieJarUnavailableError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.
30
30
  - [store-file](@warlock.js/core/store-file/SKILL.md): Read/write/delete files via the `storage` singleton — disks, drivers (local/S3/R2/DO Spaces), `storage.use(name)`, `StorageFile` handles, presigned URLs. Triggers: `storage.put`, `storage.get`, `storage.use`, `StorageFile`, `storageConfigurations`, `getPresignedUrl`, `getPresignedUploadUrl`; "save an uploaded file", "switch between local and S3", "generate a presigned URL", "read file metadata"; typical import `import { storage } from "@warlock.js/core"`. Skip: multipart parsing + image chain — `@warlock.js/core/upload-file/SKILL.md`; image transforms — `@warlock.js/core/process-image/SKILL.md`; storage config shape — `@warlock.js/core/configure-app/SKILL.md`; competing libs `@aws-sdk/client-s3`, `multer`, `formidable`.
31
31
  - [test-http](@warlock.js/core/test-http/SKILL.md): Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `startHttpTestServer({ port })`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`, `PortInUseError`, `assertPortIsAvailable`, `isPortAvailable`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape", "test server port already in use", "EADDRINUSE while running tests", "run tests while the dev server is up"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core/tests"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.
32
32
  - [test-service](@warlock.js/core/test-service/SKILL.md): Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps the framework with its own DB/cache connections so you can call your code directly, and `teardownTest()` closes it. Triggers: `setupTest`, `teardownTest`, `src/test-setup.ts`, `tests.connectors`, `tests.setupTimeout`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest, teardownTest } from "@warlock.js/core/tests"`. Skip: HTTP integration — `@warlock.js/core/test-http/SKILL.md`; warlock add test scaffold — `@warlock.js/core/write-cli-command/SKILL.md`; competing tooling: jest direct, `supertest`, `nock`.
package/package.json CHANGED
@@ -25,12 +25,12 @@
25
25
  "@mongez/slug": "^1.0.7",
26
26
  "@mongez/supportive-is": "^2.1.4",
27
27
  "@mongez/time-wizard": "^1.0.6",
28
- "@warlock.js/cache": "5.14.0",
29
- "@warlock.js/cascade": "5.14.0",
30
- "@warlock.js/context": "5.14.0",
31
- "@warlock.js/logger": "5.14.0",
32
- "@warlock.js/seal": "5.14.0",
33
- "@warlock.js/fs": "5.14.0",
28
+ "@warlock.js/cache": "5.16.0",
29
+ "@warlock.js/cascade": "5.16.0",
30
+ "@warlock.js/context": "5.16.0",
31
+ "@warlock.js/logger": "5.16.0",
32
+ "@warlock.js/seal": "5.16.0",
33
+ "@warlock.js/fs": "5.16.0",
34
34
  "chokidar": "^5.0.0",
35
35
  "dayjs": "^1.11.19",
36
36
  "es-module-lexer": "^2.0.0",
@@ -56,10 +56,10 @@
56
56
  "react": "^19.2.3",
57
57
  "react-dom": "^19.2.3",
58
58
  "@react-email/render": "^2.0.5",
59
- "@warlock.js/herald": "5.14.0",
60
- "@warlock.js/ai": "5.14.0",
61
- "@warlock.js/access": "5.14.0",
62
- "@warlock.js/notifications": "5.14.0"
59
+ "@warlock.js/herald": "5.16.0",
60
+ "@warlock.js/ai": "5.16.0",
61
+ "@warlock.js/access": "5.16.0",
62
+ "@warlock.js/notifications": "5.16.0"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "sharp": {
@@ -122,7 +122,7 @@
122
122
  ],
123
123
  "author": "hassanzohdy",
124
124
  "license": "MIT",
125
- "version": "5.14.0",
125
+ "version": "5.16.0",
126
126
  "type": "module",
127
127
  "main": "./esm/index.mjs",
128
128
  "module": "./esm/index.mjs",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: configure-app
3
- description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, and the `config()` getter for runtime reads. Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
3
+ description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
4
4
  ---
5
5
 
6
6
  # Warlock — configure the app
@@ -105,7 +105,7 @@ same nonce reaches the page's `<script>` tags.
105
105
  | CLI commands (registered via `warlock <cmd>`) | `warlock.config.ts > cli` |
106
106
  | HTTP server tuning per env (port, host, retry) | `warlock.config.ts > server` |
107
107
  | HTTP runtime (CORS, cookies, rate limits, upload size) | `src/config/http.ts` |
108
- | App identity (name, baseUrl, timezone, locales) | `src/config/app.ts` |
108
+ | App identity (name, baseUrl, publicUrl, timezone, locales) | `src/config/app.ts` |
109
109
  | Subsystem configs (auth, mail, storage, cache, ai, …) | `src/config/<name>.ts` |
110
110
 
111
111
  Heuristic: if the setting changes how the framework **boots, builds, or scaffolds**, it goes in `warlock.config.ts`. If it changes how a **subsystem behaves at runtime**, it goes in `src/config/`.
@@ -239,6 +239,33 @@ const config = {
239
239
 
240
240
  Avoid scattering `process.env.NODE_ENV === "production"` checks — they don't get the same default-handling.
241
241
 
242
+ ### `app.publicUrl` — the app's public origin (5.15.0)
243
+
244
+ ```ts title="src/config/app.ts"
245
+ import type { AppConfigurations } from "@warlock.js/core";
246
+
247
+ const appConfigurations: AppConfigurations = {
248
+ appName: "My App",
249
+ publicUrl: "https://example.com",
250
+ };
251
+
252
+ export default appConfigurations;
253
+ ```
254
+
255
+ The one absolute-URL source every consumer that needs one — the sitemap
256
+ route, canonical links, OG tags, absolute URLs in mail — reads instead of
257
+ keeping its own copy. Optional in general (most apps have no consumer that
258
+ needs it yet); read it with `getPublicUrl()`, which returns `app.publicUrl`,
259
+ falling back to the `PUBLIC_APP_URL` env var, or `undefined` when neither is
260
+ set.
261
+
262
+ `getPublicUrl()` never throws — it is a consumer's job to fail loudly when it
263
+ requires the value. `@warlock.js/web`'s sitemap is the first such consumer:
264
+ with `web.sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
265
+ generation refuses (`MissingPublicUrlError`, naming both) rather than falling
266
+ back to a request-derived host — a sitemap pointing at the wrong host is
267
+ worse than none. See `@warlock.js/web/generate-sitemap/SKILL.md`.
268
+
242
269
  ## Common patterns
243
270
 
244
271
  ### Adding a new subsystem
@@ -41,6 +41,20 @@ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-
41
41
 
42
42
  Prefer `request.validated()` once a schema is attached — it's typed.
43
43
 
44
+ ### Request body content types
45
+
46
+ `request.input()` / `.all()` / `.validated()` read the same way regardless of how the body arrived — every content type below feeds the same parsed bag:
47
+
48
+ | Content type | Parsed by | Notes |
49
+ | ------------------------------------ | -------------------------------- | --------------------------------------------------------------------- |
50
+ | `application/json` | Fastify (built-in) | objects/arrays parsed as-is |
51
+ | `multipart/form-data` | `@fastify/multipart` | fields + files; see [`upload-file`](../upload-file/SKILL.md) |
52
+ | `application/x-www-form-urlencoded` | Warlock's own content-type parser (`http/parse-urlencoded-body.ts`) | plain HTML forms, OAuth `form_post` callbacks (e.g. Apple Sign in) |
53
+
54
+ For urlencoded bodies: fields decode via `URLSearchParams`. A key sent more than once (`tag=a&tag=b`) becomes an array (`request.input("tag")` → `["a", "b"]`); every other key is a plain string. Bracket-notation keys (`a[b]=1`) are **not** expanded by the urlencoded parser itself — nesting only happens through the same shared bracket-key logic every body type already goes through, so it behaves exactly like a JSON or query-string key of that shape, no differently than today.
55
+
56
+ All three content types are held to the same `http.bodyLimit` — an over-limit urlencoded body is rejected with the same `413` a JSON body would get.
57
+
44
58
  ## Returning output
45
59
 
46
60
  Pick the helper that matches the outcome. Full surface in [send-response](../send-response/SKILL.md). Quick map:
@@ -27,6 +27,8 @@ GET /ready → 200 {"status":"ok","checks":{"db":true}}
27
27
  ### Config
28
28
 
29
29
  ```ts title="src/config/http.ts"
30
+ import type { HttpConfigurations } from "@warlock.js/core";
31
+
30
32
  const httpConfigurations: HttpConfigurations = {
31
33
  health: {
32
34
  enabled: true, // default; set false to remove both endpoints
@@ -34,6 +36,8 @@ const httpConfigurations: HttpConfigurations = {
34
36
  readinessPath: "/ready", // readiness path
35
37
  },
36
38
  };
39
+
40
+ export default httpConfigurations;
37
41
  ```
38
42
 
39
43
  ## Readiness checks
@@ -74,12 +78,16 @@ On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks
74
78
  3. Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
75
79
 
76
80
  ```ts title="src/config/http.ts"
81
+ import type { HttpConfigurations } from "@warlock.js/core";
82
+
77
83
  const httpConfigurations: HttpConfigurations = {
78
84
  gracefulShutdown: {
79
85
  timeout: 10_000, // ms to wait for in-flight drain (default 10s)
80
86
  forceCloseConnections: "idle", // close idle keep-alives, let active finish (default)
81
87
  },
82
88
  };
89
+
90
+ export default httpConfigurations;
83
91
  ```
84
92
 
85
93
  `forceCloseConnections`: `"idle"` (default) closes idle keep-alive connections and lets active requests finish; `true` force-closes everything immediately; `false` waits for every connection.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: send-response
3
- description: 'Send HTTP responses via @warlock.js/core''s Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.'
3
+ description: 'Send HTTP responses via @warlock.js/core''s Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`, `request.cookie`, `request.hasCookie`, `CookieJarUnavailableError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.'
4
4
  ---
5
5
 
6
6
  # Warlock — send a response
@@ -84,6 +84,8 @@ return response.sendBuffer(buffer, { contentType: "image/png" });
84
84
 
85
85
  `SendFileOptions` lets you set `cacheTime`, `immutable`, `inline`, `filename` (download attachment name).
86
86
 
87
+ `response.xml(string | XMLable, statusCode?)` sends `application/xml` — pass a raw XML string, or anything with a `toXML(): string` method (structural, so core never depends on `@warlock.js/sitemap`). This is for BOUNDED values only: a raw string, or a single `@warlock.js/sitemap` `Sitemap` under the sitemaps.org 50,000-URL / 50MB ceiling. `SitemapIndex` has no `toXML()` — it is the streaming path for larger URL sets and does not fit in one response body. It writes shards plus a master index as FILES via `saveTo`/`publish`, and those files are served directly rather than passed through `xml()`. In Warlock web this is the `web.sitemap` config: `enabled`, `outputDir` (default `storagePath("sitemap")`), and `path` (default `/sitemap.xml`) — the app serves the generated files from `outputDir` at that route, it never builds a `SitemapIndex` response inline.
88
+
87
89
  ## Streams
88
90
 
89
91
  ```ts
@@ -202,6 +204,20 @@ Every response cookie gets `httpOnly: true`, `sameSite: "lax"`, and `secure: tru
202
204
 
203
205
  Precedence, lowest to highest: **framework defaults → `http.cookies.options` → the per-call `options` argument.** Set an app-wide policy in config, override per cookie when a specific one genuinely needs different treatment.
204
206
 
207
+ ### Reading cookies back — `request.cookie()` / `request.hasCookie()` throw when the jar is unavailable (5.15.0)
208
+
209
+ `request.cookie(name)` and `request.hasCookie(name)` are a deliberate
210
+ by-name assertion — "this cookie should be readable here" — so when
211
+ `@fastify/cookie` is not registered on the Fastify instance, both now
212
+ **throw `CookieJarUnavailableError`** naming the missing cookie, instead of
213
+ silently returning `undefined` / `false`. Register the plugin (see core's
214
+ `http/plugins.ts`) before reading cookies by name.
215
+
216
+ `request.cookies` (the plain getter, no by-name assertion) is unchanged and
217
+ stays lenient — it returns `{}` when the jar is unavailable, because the
218
+ framework's own opportunistic reads (e.g. locale resolution) must not throw
219
+ on a request that simply has no jar.
220
+
205
221
  These are the flags whose absence never fails a test and is fatal in production: the app works perfectly and is simply insecure. Opting out is now explicit and visible in review.
206
222
 
207
223
  These mutate the response in place; chain or call before the final `return response.<helper>()`.
@@ -100,6 +100,12 @@ All three lookups go through `@mongez/localization`'s `trans()` under the hood,
100
100
 
101
101
  When an app uses `@warlock.js/web`, `warlock dev` writes `.warlock/typings/translations.d.ts` from literal `groupedTranslations("group", { key: ... })` registrations. It augments web's `TranslationKeyRegistry`, so `useTrans()("products.notFound")` is checked against registered keys and a typo fails TypeScript. Before the generated file exists, `useTrans()` accepts `string` for a non-breaking first boot. Dynamic groups/keys and placeholders are not inferred.
102
102
 
103
+ `useTrans()` now works correctly across hydration (5.15.0): the hydration
104
+ payload ships a `translations` key with the active locale's keywords, and
105
+ `@warlock.js/web` registers them into this same lookup table before the
106
+ client hydrates — see `@warlock.js/web/write-the-root/SKILL.md`'s "`useTrans()`
107
+ survives hydration" section for the failure this fixed.
108
+
103
109
  ### Locale on a specific lookup
104
110
 
105
111
  ```ts