@warlock.js/core 4.15.0 → 4.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.
package/CHANGELOG.md CHANGED
@@ -6,7 +6,35 @@ 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
- ## 4.14.0
9
+ ## 4.16.0 - 2026-08-18
10
+
11
+ ### Security
12
+
13
+ - **`request.detectIp()` no longer trusts `X-Real-IP` / `X-Forwarded-For` unless `http.trustProxy` is set.** Both headers are client-settable, and `detectIp()` honoured them unconditionally — bypassing the `trustProxy` opt-in the Fastify server itself is configured with. Any client could therefore spoof its IP to everything keyed on `detectIp()`: `ipFilter` allowlists/denylists, the default rate-limit bucket key, and anonymous idempotency scoping. Without the opt-in, `detectIp()` (and its `realIp` alias) now returns `baseRequest.ip` — the socket peer address, which cannot be forged
14
+
15
+ ⚠ **If your app runs behind a proxy and relied on `detectIp()` reading the forwarding headers without setting `http.trustProxy`, set `http.trustProxy: true`** (or a Fastify `trustProxy` value matching your edge). With `true` set, behaviour is unchanged: `X-Real-IP` first, then the leftmost `X-Forwarded-For` hop, then the peer address. Only enable `true` when your edge overwrites those headers — it trusts them wholesale
16
+
17
+ - **`http.trustProxy` now accepts a hop count or a trusted-proxy list, and `detectIp()` honours them.** `true` is the wrong shape for the common topology: an edge that *appends* to `X-Forwarded-For` leaves whatever the client prepended as the leftmost entry, so "trust the leftmost hop" hands the client its own IP back. The config value is passed to Fastify untouched, and `detectIp()` now reads the resolved client off `request.ip` instead of re-parsing the header — so both agree, and every Fastify shape works:
18
+
19
+ | `http.trustProxy` | Client IP |
20
+ | --- | --- |
21
+ | `false` *(default)* | Socket peer address; forwarding headers ignored |
22
+ | `true` | Leftmost `X-Forwarded-For` entry (whole chain trusted) |
23
+ | `2` | Walks past the 2 rightmost hops — for an edge that appends |
24
+ | `"10.0.0.0/8"`, `"loopback, 10.0.0.0/8"`, `["10.0.0.0/8", "192.168.0.0/16"]` | Walks left while each hop is a listed proxy, stops at the first that isn't |
25
+ | `(address, hop) => boolean` | Your predicate |
26
+
27
+ Prefer the narrowest shape your topology allows: with `true`, any client that can reach the process directly picks its own IP, and an `ipFilter` allowlist in front of it is decorative
28
+
29
+ ⚠ **`X-Real-IP` is now honoured only under `trustProxy: true`.** It carries no chain, so there is nothing to check a hop count or proxy list against, and a trusted edge that forwards the client's own `X-Real-IP` verbatim would otherwise let any client escape the bound. Under a bounded `trustProxy` the value comes from the `X-Forwarded-For` chain instead — if your edge sets only `X-Real-IP`, have it set `X-Forwarded-For` as well
30
+
31
+ ### Dependencies
32
+
33
+ - Bumped `@mongez/*` deps to their 2026-08-17 security release specs: `concat-route` ^1.2.0, `config` ^1.2.1, `dotenv` ^1.3.2, `events` ^2.2.7, `http` ^3.5.0, `localization` ^3.4.7, `reinforcements` ^4.0.1, `supportive-is` ^2.1.4
34
+ - ⚠ **`@mongez/reinforcements` 4.0.1 is a major bump: `Random` is now CSPRNG-backed (WebCrypto) and `Random.seed()` was removed** — seeded/reproducible `Random.string/nanoid/id/token/uuid` calls now throw. Audited `core`'s `Random.string(...)` call sites (`use-case.ts`, `http/request.ts`, `dev-server/files-watcher.ts`, `http/uploaded-file.ts`) and its test suite: none rely on seeding or reproducible output, so no code changes were required
35
+ - `@mongez/encryption` 2.0.1 (async `encrypt`/`decrypt`, throws on failure) does not apply to this package — `core` is not a consumer; `src/encryption/encrypt.ts` uses Node's built-in `crypto` module directly and is unaffected
36
+
37
+ ## 4.14.0 - 2026-08-16
10
38
 
11
39
  ### ⚠ Upgrading from 4.13.0 — read this first
12
40
 
@@ -104,7 +132,7 @@ afterAll(teardownTest); // ← is new
104
132
 
105
133
  **The cost is a framework bootstrap per test file — which is exactly what 4.13.0 already paid**, since its module-level flag died with the module registry between files. **Nothing gets slower; an unearned speed-up is simply not being claimed.** A worker-scoped lifetime remains open, and gets taken when the real per-file cost has been measured on a real application and the runner integration is chosen deliberately rather than inherited from whatever the wiring happened to do
106
134
 
107
- ## 4.13.0
135
+ ## 4.13.0 - 2026-08-12
108
136
 
109
137
  ### ⚠ Upgrading from 4.12.0 — read this first
110
138
 
@@ -25,9 +25,12 @@ type IpFilterOptions = {
25
25
  * Allow / deny requests by client IP. Fail-closed: if the IP can't be read,
26
26
  * the request is rejected with 403.
27
27
  *
28
- * Reads the client IP via `request.detectIp()`, which honors `X-Real-IP` and
29
- * `X-Forwarded-For` (Fastify is started with `trustProxy: true`). Make sure
30
- * your upstream proxy is trustworthy `X-Forwarded-For` is client-settable.
28
+ * Reads the client IP via `request.detectIp()`, which honors the forwarding
29
+ * headers only when `http.trustProxy` is set otherwise the socket peer
30
+ * address is used, since both headers are client-settable. When opting in,
31
+ * prefer a bounded `http.trustProxy` (hop count or CIDR list of your proxies)
32
+ * over `true`: with `true`, any client that can reach the process directly
33
+ * picks its own IP and this allowlist is decorative.
31
34
  *
32
35
  * @example
33
36
  * import { middleware } from "@warlock.js/core";
@@ -1 +1 @@
1
- {"version":3,"file":"ip-filter.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/ip-filter.middleware.ts"],"mappings":";;;;;AAUA;;KAAY,eAAA;EAAe;;;;EAKzB,KAAA;EASY;AA4Bd;;;EAhCE,IAAA;EAgC0C;;;EA5B1C,YAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4Bc,kBAAA,CAAmB,OAAA,EAAS,eAAA,GAAkB,UAAU"}
1
+ {"version":3,"file":"ip-filter.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/ip-filter.middleware.ts"],"mappings":";;;;;AAUA;;KAAY,eAAA;EAAe;;;;EAKzB,KAAA;EASY;AA+Bd;;;EAnCE,IAAA;EAmC0C;;;EA/B1C,YAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+Bc,kBAAA,CAAmB,OAAA,EAAS,eAAA,GAAkB,UAAU"}
@@ -7,9 +7,12 @@ import { anyMatch } from "./utils/cidr-match.mjs";
7
7
  * Allow / deny requests by client IP. Fail-closed: if the IP can't be read,
8
8
  * the request is rejected with 403.
9
9
  *
10
- * Reads the client IP via `request.detectIp()`, which honors `X-Real-IP` and
11
- * `X-Forwarded-For` (Fastify is started with `trustProxy: true`). Make sure
12
- * your upstream proxy is trustworthy `X-Forwarded-For` is client-settable.
10
+ * Reads the client IP via `request.detectIp()`, which honors the forwarding
11
+ * headers only when `http.trustProxy` is set otherwise the socket peer
12
+ * address is used, since both headers are client-settable. When opting in,
13
+ * prefer a bounded `http.trustProxy` (hop count or CIDR list of your proxies)
14
+ * over `true`: with `true`, any client that can reach the process directly
15
+ * picks its own IP and this allowlist is decorative.
13
16
  *
14
17
  * @example
15
18
  * import { middleware } from "@warlock.js/core";
@@ -1 +1 @@
1
- {"version":3,"file":"ip-filter.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/ip-filter.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\nimport { anyMatch } from \"./utils/cidr-match\";\n\n/**\n * Options for the IP filter middleware. Use at least one of `allow` / `deny`.\n *\n * Precedence: `deny` wins. If a request matches both lists it is denied.\n */\nexport type IpFilterOptions = {\n /**\n * Allowlist of exact IPv4 / IPv6 strings or IPv4 CIDR blocks. When present,\n * only IPs matching the list pass through.\n */\n allow?: string[];\n /**\n * Denylist of exact IPv4 / IPv6 strings or IPv4 CIDR blocks. Matched IPs\n * are rejected regardless of the allowlist.\n */\n deny?: string[];\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\n/**\n * Allow / deny requests by client IP. Fail-closed: if the IP can't be read,\n * the request is rejected with 403.\n *\n * Reads the client IP via `request.detectIp()`, which honors `X-Real-IP` and\n * `X-Forwarded-For` (Fastify is started with `trustProxy: true`). Make sure\n * your upstream proxy is trustworthy `X-Forwarded-For` is client-settable.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.group(\n * {\n * prefix: \"/admin\",\n * middleware: [middleware.ipFilter({ allow: [\"10.0.0.0/8\", \"203.0.113.42\"] })],\n * },\n * () => {\n * router.get(\"/dashboard\", dashboardController);\n * },\n * );\n *\n * router.post(\"/webhooks/provider\", webhookController, {\n * middleware: [middleware.ipFilter({ allow: [\"198.51.100.0/24\"] })],\n * });\n */\nexport function ipFilterMiddleware(options: IpFilterOptions): Middleware {\n return (request, response) => {\n const ip = request.detectIp();\n\n if (!ip || typeof ip !== \"string\") {\n return response.forbidden({\n error: options.errorMessage || t(\"http.ipForbidden\"),\n errorCode: HttpErrorCodes.IpForbidden,\n });\n }\n\n if (options.deny && anyMatch(ip, options.deny)) {\n return response.forbidden({\n error: options.errorMessage || t(\"http.ipForbidden\"),\n errorCode: HttpErrorCodes.IpForbidden,\n });\n }\n\n if (options.allow && !anyMatch(ip, options.allow)) {\n return response.forbidden({\n error: options.errorMessage || t(\"http.ipForbidden\"),\n errorCode: HttpErrorCodes.IpForbidden,\n });\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,mBAAmB,SAAsC;CACvE,QAAQ,SAAS,aAAa;EAC5B,MAAM,KAAK,QAAQ,SAAS;EAE5B,IAAI,CAAC,MAAM,OAAO,OAAO,UACvB,OAAO,SAAS,UAAU;GACxB,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;EAGH,IAAI,QAAQ,QAAQ,SAAS,IAAI,QAAQ,IAAI,GAC3C,OAAO,SAAS,UAAU;GACxB,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;EAGH,IAAI,QAAQ,SAAS,CAAC,SAAS,IAAI,QAAQ,KAAK,GAC9C,OAAO,SAAS,UAAU;GACxB,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;CAEL;AACF"}
1
+ {"version":3,"file":"ip-filter.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/ip-filter.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\nimport { anyMatch } from \"./utils/cidr-match\";\n\n/**\n * Options for the IP filter middleware. Use at least one of `allow` / `deny`.\n *\n * Precedence: `deny` wins. If a request matches both lists it is denied.\n */\nexport type IpFilterOptions = {\n /**\n * Allowlist of exact IPv4 / IPv6 strings or IPv4 CIDR blocks. When present,\n * only IPs matching the list pass through.\n */\n allow?: string[];\n /**\n * Denylist of exact IPv4 / IPv6 strings or IPv4 CIDR blocks. Matched IPs\n * are rejected regardless of the allowlist.\n */\n deny?: string[];\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\n/**\n * Allow / deny requests by client IP. Fail-closed: if the IP can't be read,\n * the request is rejected with 403.\n *\n * Reads the client IP via `request.detectIp()`, which honors the forwarding\n * headers only when `http.trustProxy` is set — otherwise the socket peer\n * address is used, since both headers are client-settable. When opting in,\n * prefer a bounded `http.trustProxy` (hop count or CIDR list of your proxies)\n * over `true`: with `true`, any client that can reach the process directly\n * picks its own IP and this allowlist is decorative.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.group(\n * {\n * prefix: \"/admin\",\n * middleware: [middleware.ipFilter({ allow: [\"10.0.0.0/8\", \"203.0.113.42\"] })],\n * },\n * () => {\n * router.get(\"/dashboard\", dashboardController);\n * },\n * );\n *\n * router.post(\"/webhooks/provider\", webhookController, {\n * middleware: [middleware.ipFilter({ allow: [\"198.51.100.0/24\"] })],\n * });\n */\nexport function ipFilterMiddleware(options: IpFilterOptions): Middleware {\n return (request, response) => {\n const ip = request.detectIp();\n\n if (!ip || typeof ip !== \"string\") {\n return response.forbidden({\n error: options.errorMessage || t(\"http.ipForbidden\"),\n errorCode: HttpErrorCodes.IpForbidden,\n });\n }\n\n if (options.deny && anyMatch(ip, options.deny)) {\n return response.forbidden({\n error: options.errorMessage || t(\"http.ipForbidden\"),\n errorCode: HttpErrorCodes.IpForbidden,\n });\n }\n\n if (options.allow && !anyMatch(ip, options.allow)) {\n return response.forbidden({\n error: options.errorMessage || t(\"http.ipForbidden\"),\n errorCode: HttpErrorCodes.IpForbidden,\n });\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,mBAAmB,SAAsC;CACvE,QAAQ,SAAS,aAAa;EAC5B,MAAM,KAAK,QAAQ,SAAS;EAE5B,IAAI,CAAC,MAAM,OAAO,OAAO,UACvB,OAAO,SAAS,UAAU;GACxB,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;EAGH,IAAI,QAAQ,QAAQ,SAAS,IAAI,QAAQ,IAAI,GAC3C,OAAO,SAAS,UAAU;GACxB,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;EAGH,IAAI,QAAQ,SAAS,CAAC,SAAS,IAAI,QAAQ,KAAK,GAC9C,OAAO,SAAS,UAAU;GACxB,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;CAEL;AACF"}
@@ -418,27 +418,43 @@ declare class Request<RequestValidation = any> {
418
418
  */
419
419
  get ip(): string;
420
420
  /**
421
- * Best-effort real client IP — checks `X-Real-IP` and `X-Forwarded-For`
422
- * headers first, falls back to `baseRequest.ip` when neither is present.
421
+ * Best-effort real client IP — the value everything IP-scoped keys on
422
+ * (ip-filter allowlists, rate-limit buckets, idempotency scoping).
423
423
  *
424
- * For a multi-hop `X-Forwarded-For` (`client, proxy1, proxy2`) only the
425
- * FIRST entry is returned (the original client), comma-split and trimmed.
426
- * Without this, downstream scoping (ip-filter, rate-limit, idempotency)
427
- * would key off the whole header string and break behind chained proxies.
424
+ * `X-Forwarded-For` resolution is **delegated to Fastify**: `baseRequest.ip`
425
+ * is already the client address Fastify's `trustProxy` machinery picked out
426
+ * of the chain, so every shape `http.trustProxy` accepts is honoured here
427
+ * with exactly the semantics Fastify documents:
428
+ *
429
+ * - `false` (default) — no header is trusted; the socket peer address wins.
430
+ * Both forwarding headers are client-settable, so without a trusted edge
431
+ * that rewrites them any client could otherwise forge its own IP.
432
+ * - `true` — the whole chain is trusted; the leftmost hop (original client)
433
+ * wins.
434
+ * - `number` — that many rightmost hops are trusted, so an edge that
435
+ * APPENDS to `X-Forwarded-For` yields the real client rather than whatever
436
+ * the client prepended.
437
+ * - CIDR / IP list (string, comma-separated string, or array) or a custom
438
+ * predicate — the chain is walked right-to-left and stops at the first hop
439
+ * that isn't a trusted proxy.
440
+ *
441
+ * `X-Real-IP` is NOT part of that resolution — Fastify never looks at it,
442
+ * and unlike `X-Forwarded-For` it carries no chain, so there is nothing to
443
+ * validate a hop count or proxy allowlist against. It is therefore honoured
444
+ * only under `trustProxy: true` ("everything upstream is mine"), where it is
445
+ * no weaker than the trust already granted. Under a bounded `trustProxy`
446
+ * (hop count / CIDR list) it is ignored: a trusted-but-passthrough edge that
447
+ * forwards the client's own `X-Real-IP` verbatim would otherwise hand any
448
+ * client a way around the bound.
428
449
  *
429
450
  * **Prefer this over `request.ip` for any caller behind a proxy** (load
430
- * balancer, CDN, reverse proxy, k8s ingress). Only trust the result as
431
- * far as you trust the upstream proxy chain — `X-Forwarded-For` is
432
- * client-settable, so the leftmost entry is spoofable by the immediate
433
- * client when the request did NOT pass through a trusted edge that
434
- * appends/overwrites the header. Verify the request came through your
435
- * trusted edge before treating the value as authoritative.
451
+ * balancer, CDN, reverse proxy, k8s ingress).
436
452
  */
437
- detectIp(): any;
453
+ detectIp(): string;
438
454
  /**
439
455
  * An alias to detectIp
440
456
  */
441
- get realIp(): any;
457
+ get realIp(): string;
442
458
  /**
443
459
  * Get request ips
444
460
  */
@@ -1 +1 @@
1
- {"version":3,"file":"request.d.mts","names":[],"sources":["../../../../../../../core/src/http/request.ts"],"mappings":"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"}
1
+ {"version":3,"file":"request.d.mts","names":[],"sources":["../../../../../../../core/src/http/request.ts"],"mappings":"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"}
@@ -656,29 +656,45 @@ var Request = class Request {
656
656
  return this.baseRequest.ip;
657
657
  }
658
658
  /**
659
- * Best-effort real client IP — checks `X-Real-IP` and `X-Forwarded-For`
660
- * headers first, falls back to `baseRequest.ip` when neither is present.
659
+ * Best-effort real client IP — the value everything IP-scoped keys on
660
+ * (ip-filter allowlists, rate-limit buckets, idempotency scoping).
661
661
  *
662
- * For a multi-hop `X-Forwarded-For` (`client, proxy1, proxy2`) only the
663
- * FIRST entry is returned (the original client), comma-split and trimmed.
664
- * Without this, downstream scoping (ip-filter, rate-limit, idempotency)
665
- * would key off the whole header string and break behind chained proxies.
662
+ * `X-Forwarded-For` resolution is **delegated to Fastify**: `baseRequest.ip`
663
+ * is already the client address Fastify's `trustProxy` machinery picked out
664
+ * of the chain, so every shape `http.trustProxy` accepts is honoured here
665
+ * with exactly the semantics Fastify documents:
666
+ *
667
+ * - `false` (default) — no header is trusted; the socket peer address wins.
668
+ * Both forwarding headers are client-settable, so without a trusted edge
669
+ * that rewrites them any client could otherwise forge its own IP.
670
+ * - `true` — the whole chain is trusted; the leftmost hop (original client)
671
+ * wins.
672
+ * - `number` — that many rightmost hops are trusted, so an edge that
673
+ * APPENDS to `X-Forwarded-For` yields the real client rather than whatever
674
+ * the client prepended.
675
+ * - CIDR / IP list (string, comma-separated string, or array) or a custom
676
+ * predicate — the chain is walked right-to-left and stops at the first hop
677
+ * that isn't a trusted proxy.
678
+ *
679
+ * `X-Real-IP` is NOT part of that resolution — Fastify never looks at it,
680
+ * and unlike `X-Forwarded-For` it carries no chain, so there is nothing to
681
+ * validate a hop count or proxy allowlist against. It is therefore honoured
682
+ * only under `trustProxy: true` ("everything upstream is mine"), where it is
683
+ * no weaker than the trust already granted. Under a bounded `trustProxy`
684
+ * (hop count / CIDR list) it is ignored: a trusted-but-passthrough edge that
685
+ * forwards the client's own `X-Real-IP` verbatim would otherwise hand any
686
+ * client a way around the bound.
666
687
  *
667
688
  * **Prefer this over `request.ip` for any caller behind a proxy** (load
668
- * balancer, CDN, reverse proxy, k8s ingress). Only trust the result as
669
- * far as you trust the upstream proxy chain — `X-Forwarded-For` is
670
- * client-settable, so the leftmost entry is spoofable by the immediate
671
- * client when the request did NOT pass through a trusted edge that
672
- * appends/overwrites the header. Verify the request came through your
673
- * trusted edge before treating the value as authoritative.
689
+ * balancer, CDN, reverse proxy, k8s ingress).
674
690
  */
675
691
  detectIp() {
676
- const realIp = this.header("x-real-ip");
677
- if (realIp) return realIp;
678
- const forwardedIp = this.header("x-forwarded-for");
679
- if (forwardedIp) {
680
- const firstHop = String(forwardedIp).split(",")[0].trim();
681
- if (firstHop) return firstHop;
692
+ if (config.get("http.trustProxy", false) === true) {
693
+ const realIp = this.header("x-real-ip");
694
+ if (realIp) {
695
+ const address = String(realIp).split(",")[0].trim();
696
+ if (address) return address;
697
+ }
682
698
  }
683
699
  return this.baseRequest.ip;
684
700
  }
@@ -1 +1 @@
1
- {"version":3,"file":"request.mjs","names":[],"sources":["../../../../../../../core/src/http/request.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport events from \"@mongez/events\";\nimport { trans, transFrom } from \"@mongez/localization\";\nimport { Random, except, get, only, rtrim, set, unset } from \"@mongez/reinforcements\";\nimport { isEmpty } from \"@mongez/supportive-is\";\nimport type { LogLevel } from \"@warlock.js/logger\";\nimport { log } from \"@warlock.js/logger\";\nimport { BaseValidator, v } from \"@warlock.js/seal\";\nimport type { FastifyRequest } from \"fastify\";\nimport { type IncomingHttpHeaders } from \"node:http2\";\nimport { config } from \"../config/config-getter\";\nimport type { Middleware, Route } from \"../router\";\nimport { validateAll } from \"../validation/validateAll\";\nimport { createRequestStore } from \"./middleware/inject-request-context\";\nimport { Response } from \"./response\";\nimport type { RequestEvent } from \"./types\";\nimport { UploadedFile } from \"./uploaded-file\";\n\ntype StandardHeaders = {\n // copy every declared property from http.IncomingHttpHeaders\n // but remove index signatures\n [K in keyof IncomingHttpHeaders as string extends K\n ? never\n : number extends K\n ? never\n : K]: IncomingHttpHeaders[K];\n};\n\ntype HeaderKeys = keyof StandardHeaders;\n\nexport class Request<RequestValidation = any> {\n /**\n * Underlying Fastify request — a public escape hatch to capabilities the\n * framework's high-level helpers don't yet cover.\n *\n * **Prefer framework methods first**: `request.input()`, `request.header()`,\n * `request.body`, `request.query`, `request.params`, `request.file()`,\n * `request.user`, `request.detectIp()`, etc. They handle locale, parsing,\n * trust-proxy, and validation pipeline integration correctly.\n *\n * **Reach for `baseRequest` only** when the framework genuinely lacks a\n * helper for what you need — and when you do, file an issue so we can add\n * it. The escape hatch is the release valve that lets consumers move\n * faster than the framework, but every long-term reach here is a missing\n * helper waiting to be added.\n */\n public baseRequest!: FastifyRequest;\n\n /**\n * Response Object\n */\n public response!: Response;\n\n /**\n * Route Object\n */\n public route!: Route;\n\n /**\n * Parsed Request Payload\n */\n protected payload: any = {};\n\n /**\n * Decoded access token payload (set by auth middleware)\n */\n public decodedAccessToken?: any;\n\n /**\n * Current request instance\n */\n public static current: Request;\n\n /**\n * Translation method\n * Type of it is the same as the type of trans function\n */\n public trans: ReturnType<typeof trans> = trans;\n\n /**\n * Alias to trans method\n */\n public t: ReturnType<typeof trans> = trans;\n\n /**\n * Dynamic properties index signature\n *\n * This allows attaching custom properties to the request instance,\n * commonly used during validation middleware to attach fetched models.\n *\n * @example\n * // In validation middleware:\n * const post = await Post.find(request.int(\"id\"));\n * if (!post) return response.notFound();\n * request.post = post; // Attach the model to the request\n *\n * // In route handler:\n * const post = request.post;\n * // Work with the pre-fetched model\n */\n [key: string]: any;\n\n /**\n * Locale code\n */\n protected _locale = \"\";\n\n /**\n * Validated data\n */\n protected validatedData?: RequestValidation;\n\n /**\n * Request id\n */\n public id = Random.string(32);\n\n /**\n * Start Time\n */\n public startTime = Date.now();\n\n /**\n * End Time\n */\n public endTime?: undefined | number;\n\n /**\n * Set request handler\n */\n public setRequest(request: FastifyRequest) {\n this.baseRequest = request;\n\n this.resolveRequestId();\n\n this.parsePayload();\n\n const localeCode = this.getLocaleCode();\n\n this.trans = this.t = transFrom.bind(null, localeCode);\n\n return this;\n }\n\n /**\n * Inherit `X-Request-Id` from the incoming request, fall back to a custom\n * generator, then to the field-init default (`Random.string(32)`).\n *\n * Inherited values are validated (length cap + printable-ASCII) to prevent\n * log-injection from a malicious client. Disable the whole behavior by\n * setting `http.requestId.enabled = false` — in which case the field-init\n * default is used regardless of any incoming header.\n */\n protected resolveRequestId() {\n const requestIdConfig = config.key(\"http.requestId\") || {};\n\n if (requestIdConfig.enabled === false) return;\n\n const headerName = (requestIdConfig.header || \"x-request-id\").toLowerCase();\n const incoming = this.baseRequest.headers[headerName];\n\n if (Request.isValidRequestId(incoming)) {\n this.id = incoming;\n\n return;\n }\n\n if (typeof requestIdConfig.generator === \"function\") {\n this.id = requestIdConfig.generator();\n }\n }\n\n /**\n * Validate a candidate request-id value. Accepts non-empty printable ASCII\n * up to 128 characters — tight enough to reject newline / control-character\n * log-injection, loose enough to accept UUIDs, ULIDs, snowflakes, etc.\n */\n protected static isValidRequestId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n value.length > 0 &&\n value.length <= 128 &&\n /^[\\x21-\\x7e]+$/.test(value)\n );\n }\n\n /**\n * Translate from the given locale code\n */\n public transFrom(localeCode: string, keyword: string, placeholders?: any) {\n return transFrom(localeCode, keyword, placeholders);\n }\n\n /**\n * Get current locale code\n */\n public get locale() {\n if (this._locale) return this._locale;\n\n return this.header(\"translation-locale-code\") || this.localized;\n }\n\n /**\n * Set locale code\n */\n public set locale(localeCode: string) {\n this._locale = localeCode;\n }\n\n /**\n * Get locale code that will be used for translation\n */\n public get localized() {\n if (this._locale) return this._locale;\n\n return (this._locale = this.header(\"locale\") || this.query[\"locale\"]);\n }\n\n /**\n * Set locale code\n */\n public setLocaleCode(localeCode: string) {\n this._locale = localeCode;\n\n return this;\n }\n\n /**\n * Get current locale code or return default locale code\n */\n public getLocaleCode(defaultLocaleCode: string = config.key(\"app.localeCode\") || \"en\") {\n return this.locale || defaultLocaleCode;\n }\n\n /**\n * Get http protocol\n */\n public get protocol() {\n return this.baseRequest.protocol;\n }\n\n /**\n * Validate the given validation schema\n */\n public async validate(validation: BaseValidator, selectedInputs?: string[]) {\n return await v.validate(validation, selectedInputs ? this.only(selectedInputs) : this.all());\n }\n\n /**\n * Clear current user\n */\n public clearCurrentUser() {\n this.user = undefined;\n }\n\n /**\n * Get value of the given header\n */\n public header<TCustomHeader extends string = HeaderKeys>(\n name: TCustomHeader | HeaderKeys,\n defaultValue: any = null,\n ) {\n return this.baseRequest.headers[name.toLocaleLowerCase()] ?? defaultValue;\n }\n\n /**\n * Get all cookies from the current request\n */\n public get cookies(): Record<string, string | undefined> {\n return this.baseRequest.cookies || {};\n }\n\n /**\n * Get a particular cookie value or fallback to default\n */\n public cookie(name: string, defaultValue?: any): string | any {\n const value = this.cookies[name] ?? defaultValue;\n\n try {\n return JSON.parse(value);\n } catch (error) {\n return value;\n }\n }\n\n /**\n * Determine if the request has the specified cookie\n */\n public hasCookie(name: string): boolean {\n return this.cookies[name] !== undefined;\n }\n\n /**\n * Get the current request domain\n */\n public get domain() {\n return this.baseRequest.hostname.replace(/^www\\./, \"\");\n }\n\n /**\n * Get hostname\n */\n public get hostname() {\n return this.domain;\n }\n\n /**\n * Get request origin\n */\n public get origin() {\n return this.baseRequest.headers.origin as string;\n }\n\n /**\n * Get the domain of the origin\n */\n public get originDomain() {\n const domain = this.origin ? new URL(this.origin).hostname : null;\n\n if (domain?.startsWith(\"www.\")) {\n return domain.replace(/^www\\./, \"\");\n }\n\n return domain;\n }\n\n /**\n * Get authorization header value\n */\n public get authorizationValue(): string {\n const authorization = this.header(\"authorization\");\n\n if (!authorization) return \"\";\n\n const [type, value] = authorization.split(\" \");\n\n if (![\"bearer\", \"key\"].includes(type.toLowerCase())) return \"\";\n\n return value || \"\";\n }\n\n /**\n * Get access token from Authorization header\n *\n * If the Authorization header does not start with `Bearer` value then return null\n */\n public get accessToken(): string | undefined {\n const authorization = this.header(\"authorization\");\n\n if (!authorization) return;\n\n const [type, value] = authorization.split(\" \");\n\n if (type.toLowerCase() !== \"bearer\") return;\n\n return value;\n }\n\n /**\n * Get the authorization header\n */\n public get authorization() {\n return this.header(\"authorization\");\n }\n\n /**\n * Get current request method\n */\n public get method(): string {\n return this.baseRequest.method;\n }\n\n /**\n * Parse the payload and merge it from the request body, params and query string\n */\n protected parsePayload() {\n this.payload.body = this.parseBody(this.baseRequest.body);\n\n this.payload.query = this.parseBody(this.baseRequest.query);\n this.payload.params = { ...(this.baseRequest.params || {}) };\n this.payload.all = {\n ...this.payload.body,\n ...this.payload.query,\n ...this.payload.params,\n };\n }\n\n /**\n * Parse body payload\n */\n protected parseBody(data: any) {\n try {\n if (!data) return {};\n\n const body: any = {};\n\n const arrayOfObjectValues: any = {};\n\n for (let key in data) {\n const value = data[key];\n\n let isArrayKey = false;\n\n if (key.endsWith(\"[]\")) {\n isArrayKey = true;\n }\n\n key = rtrim(key, \"[]\");\n\n // check if the key is has a square brackets, then convert it into object\n // i.e user[email] => user: {email: \"value\"}\n // also check if its an array of objects\n\n if (key.includes(\"[\")) {\n // check if its an array of objects\n if (key.includes(\"][\")) {\n const keyParts = key.split(\"[\");\n\n const keyName = keyParts[0];\n if (!arrayOfObjectValues[keyName]) {\n arrayOfObjectValues[keyName] = [];\n }\n\n const keyNameParts = keyParts[1].split(\"]\");\n\n const index = Number(keyNameParts[0]);\n\n if (!arrayOfObjectValues[keyName][index]) {\n arrayOfObjectValues[keyName][index] = {};\n }\n\n // now get the key after the index\n const keyNameParts2 = keyParts[2].split(\"]\");\n const keyName2 = keyNameParts2[0];\n\n arrayOfObjectValues[keyName][index][keyName2] = this.parseValue(value);\n\n continue;\n }\n\n const keyParts = key.split(\"[\");\n const keyName = keyParts[0];\n const keyNameParts = keyParts[1].split(\"]\");\n\n set(\n body,\n keyName + \".\" + keyNameParts[0],\n Array.isArray(value) ? value.map(this.parseValue.bind(this)) : this.parseValue(value),\n );\n\n continue;\n }\n\n if (Array.isArray(value)) {\n set(body, key, value.map(this.parseValue.bind(this)));\n } else if (isArrayKey) {\n if (body[key]) {\n body[key].push(this.parseValue(value));\n } else {\n body[key] = [this.parseValue(value)];\n\n continue;\n }\n } else {\n set(body, key, this.parseValue(value));\n }\n }\n\n // now merge the array of objects into the body\n for (const key in arrayOfObjectValues) {\n body[key] = arrayOfObjectValues[key];\n }\n\n return body;\n } catch (error) {\n console.log(error);\n this.log(error, \"error\");\n }\n }\n\n /**\n * Parse the given data\n */\n protected parseValue(data: any) {\n // data.value appears only in the multipart form data\n // if it json, then just return the data\n if (data?.file) return new UploadedFile(data);\n if (data?.value !== undefined && data?.fields && data?.type) {\n data = data.value;\n }\n\n if (data === \"false\") return false;\n\n if (data === \"true\") return true;\n\n if (data === \"null\") return null;\n\n if (typeof data === \"string\") return data.trim();\n\n return data;\n }\n\n /**\n * Set route handler\n */\n public setRoute(route: Route) {\n this.route = route;\n\n // pass the route to the response object\n this.response.setRoute(route);\n\n return this;\n }\n\n /**\n * Trigger an http event\n */\n public trigger(eventName: RequestEvent, ...args: any[]) {\n return events.trigger(`request.${eventName}`, ...args, this);\n }\n\n /**\n * Listen to the given event\n */\n public on(eventName: RequestEvent, callback: any) {\n return this.subscribe(eventName, callback);\n }\n\n /**\n * Make a log message\n */\n public log(message: any, level: LogLevel = \"info\") {\n if (!config.key(\"http.log\")) return;\n\n log.log({\n module: \"request\",\n action: this.route.method + \" \" + this.route.path.replace(\"/*\", \"\") + `:${this.id}`,\n message,\n type: level,\n context: {\n request: this,\n },\n });\n }\n\n /**\n * Get current request path\n */\n public get path() {\n return this.baseRequest.url;\n }\n\n /**\n * {@alias}\n */\n public get url() {\n return this.baseRequest.url;\n }\n\n /**\n * Get full url\n */\n public get fullUrl() {\n return this.protocol + \"://\" + this.hostname + this.path;\n }\n\n /**\n * Drive the middleware chain for the current route, then defer to the\n * controller. Returns the first response value any middleware short-circuits\n * with, or `undefined` to continue into validation + handler.\n *\n * @internal Framework orchestration — do not call from app code. Will move\n * to a dedicated controller dispatcher in a future refactor.\n */\n public async runMiddleware() {\n // measure request time\n // check for middleware first\n const middlewareOutput = await this.executeMiddleware();\n\n if (middlewareOutput !== undefined) {\n // 👇🏻 make sure first its not a response instance\n if (middlewareOutput instanceof Response) return middlewareOutput;\n // 👇🏻 send the response\n return this.response.send(middlewareOutput);\n }\n\n const handler = this.route.handler;\n\n if (!handler.validation) return;\n\n // 👇🏻 check for validation using validateAll helper function\n const validationOutput = await validateAll(handler.validation, this, this.response);\n\n return validationOutput;\n }\n\n /**\n * Return the request handler attached to the current route.\n *\n * @internal Framework orchestration — do not call from app code.\n */\n public getHandler() {\n return this.route.handler;\n }\n\n /**\n * Get inputs that has been validated only\n * You can also pass an array of inputs to get only the validated inputs\n */\n public validated<Output = RequestValidation>(inputs?: (keyof Output | (string & {}))[]): Output {\n if (this.validatedData) {\n return inputs\n ? only(this.validatedData as Output, inputs as string[])\n : (this.validatedData as Output);\n }\n\n return {} as Output;\n }\n\n /**\n * Get inputs that has been validated except the given inputs\n */\n public validatedExcept(...inputs: string[]): RequestValidation {\n return except(this.validated(), inputs);\n }\n\n /**\n * Set validated data\n */\n public setValidatedData(data: RequestValidation) {\n this.validatedData = data;\n }\n\n /**\n * Top-level entry into the request lifecycle — opens the context store,\n * runs middleware, drives the handler, handles errors.\n *\n * @internal Framework orchestration — do not call from app code. Wired\n * from the Fastify route handler in `router.scan()`.\n */\n public async execute() {\n try {\n // call executingAction event\n\n this.log(\"Executing the request\");\n\n return await createRequestStore(this, this.response);\n } catch (error) {\n this.log(error, \"error\");\n\n throw error;\n }\n }\n\n /**\n * Iterate the collected middlewares in order; return the first short-circuit\n * value or `undefined` when every middleware passes through.\n *\n * @internal Framework orchestration — do not call from app code.\n */\n protected async executeMiddleware() {\n // collect all middlewares for current route\n const middlewares = this.collectMiddlewares();\n\n // check if there are no middlewares, then return\n if (middlewares.length === 0) return;\n\n this.log(\"About to execute request middlewares\");\n\n // trigger the executingMiddleware event\n this.trigger(\"executingMiddleware\", middlewares, this.route);\n\n for (const middleware of middlewares) {\n this.log(\"Executing middleware \" + colors.yellowBright(middleware.name));\n const output = await middleware(this, this.response);\n this.log(\"Executed middleware \" + colors.yellowBright(middleware.name), \"success\");\n\n if (output !== undefined) {\n this.log(\n colors.yellow(\"request intercepted by middleware \") + colors.cyanBright(middleware.name),\n \"warn\",\n );\n\n this.trigger(\"executedMiddleware\");\n\n this.log(\"Request middlewares executed\", \"success\");\n\n return output;\n }\n }\n\n this.log(\"Request middlewares executed\", \"success\");\n\n // trigger the executedMiddleware event\n this.trigger(\"executedMiddleware\", middlewares, this.route);\n }\n\n /**\n * Gather the middleware list for the current route — today just the\n * route-level array; future extraction may merge group + app-wide layers.\n *\n * @internal Framework orchestration — do not call from app code.\n */\n protected collectMiddlewares(): Middleware[] {\n const middlewaresList: Middleware[] = [];\n\n // collect route middlewares\n if (this.route.middleware) {\n middlewaresList.push(...this.route.middleware);\n }\n\n return middlewaresList;\n }\n\n /**\n * Get request input value from query string, params or body\n */\n public input(key: string, defaultValue?: any) {\n return get(this.payload.all, key, defaultValue);\n }\n\n /**\n * Get email input value, this will lowercase the value\n */\n public email(key: string = \"email\", defaultValue: string = \"\"): string {\n return this.input(key, defaultValue)?.toLowerCase() || defaultValue;\n }\n\n /**\n * @alias input\n */\n public get(key: string, defaultValue?: any) {\n return this.input(key, defaultValue);\n }\n\n /**\n * Determine if request has input value\n */\n public has(key: string) {\n return get(this.payload.all, key, undefined) !== undefined;\n }\n\n /**\n * Set request input value\n */\n public set(key: string, value: any) {\n set(this.payload.all, key, value);\n\n return this;\n }\n\n /**\n * Set the given value if the request does not have the input\n */\n public setDefault(key: string, value: any) {\n if (this.has(key)) return this;\n\n set(this.payload.all, key, value);\n\n return this;\n }\n\n /**\n * Unset request payload keys\n */\n public unset(...keys: string[]) {\n this.payload.all = unset(this.payload.all, keys);\n\n return this;\n }\n\n /**\n * Get request body\n */\n public get body() {\n return this.payload.body;\n }\n\n /**\n * Set request body value\n */\n public setBody(key: string, value: any) {\n set(this.payload.body, key, value);\n\n return this;\n }\n\n /**\n * Get body inputs except files\n */\n public get bodyInputs() {\n const inputs = this.payload.body;\n\n const bodyInputs: any = {};\n\n for (const key in inputs) {\n const value = inputs[key];\n\n if (value.file && value.fieldname) continue;\n\n bodyInputs[key] = value;\n }\n\n return bodyInputs;\n }\n\n /**\n * Get request file in UploadedFile instance\n */\n public file(key: string): UploadedFile | undefined {\n const file = this.input(key);\n\n return file;\n }\n\n /**\n * Get uploaded files from the request for the given name\n * If the given name is not present in the request, return an empty array\n */\n public files(name: string): UploadedFile[] {\n return this.input(name) || [];\n }\n\n /**\n * Get request params\n */\n public get params() {\n return this.payload.params;\n }\n\n /**\n * Set request params value\n */\n public setParam(key: string, value: any) {\n set(this.payload.params, key, value);\n\n return this;\n }\n\n /**\n * Get request query\n */\n public get query() {\n return this.payload.query;\n }\n\n /**\n * Set request query value\n */\n public setQuery(key: string, value: any) {\n set(this.payload.query, key, value);\n\n return this;\n }\n\n /**\n * Get all inputs\n */\n public all() {\n return this.payload.all;\n }\n\n /**\n * Get all inputs except params\n */\n public allExceptParams() {\n return {\n ...this.payload.query,\n ...this.payload.body,\n };\n }\n\n /**\n * Get all heavy inputs except params\n */\n public heavyExceptParams() {\n const inputs = this.allExceptParams();\n\n const heavyInputs: any = {};\n\n for (const key in inputs) {\n const value = inputs[key];\n\n if (isEmpty(value) && value !== null) continue;\n\n heavyInputs[key] = value;\n }\n\n return heavyInputs;\n }\n\n /**\n * Get only heavy inputs, the input with a value\n */\n public heavy() {\n const inputs = this.all();\n\n const heavyInputs: any = {};\n\n for (const key in inputs) {\n const value = inputs[key];\n\n if (isEmpty(value) && value !== null) continue;\n\n heavyInputs[key] = value;\n }\n\n return heavyInputs;\n }\n\n /**\n * Get only the given keys from the request data\n */\n public only(keys: string[]) {\n return only(this.all(), keys);\n }\n\n /**\n * Pluck the given keys from the request data\n */\n public pluck(keys: string[]) {\n const data = this.only(keys);\n\n this.unset(...keys);\n\n return data;\n }\n\n /**\n * Get all request inputs except the given keys\n */\n public except(keys: string[]) {\n return except(this.all(), keys);\n }\n\n /**\n * Get boolean input value\n */\n public bool(key: string, defaultValue = false) {\n const value = this.input(key, defaultValue);\n\n if (value === \"true\") {\n return true;\n }\n\n if (value === \"false\") {\n return false;\n }\n\n if (value === 0) {\n return false;\n }\n\n return Boolean(value);\n }\n\n /**\n * Get integer input value\n */\n public int(key: string, defaultValue: number = 0): number | undefined {\n const value = this.input(key, defaultValue);\n\n if (!value && value !== 0) return undefined;\n\n return parseInt(value);\n }\n\n /**\n * Shorthand getter to get id param\n */\n public get idParam() {\n return this.int(\"id\");\n }\n\n /**\n * Get string input value\n */\n public string(key: string, defaultValue: string = \"\"): string {\n const value = this.input(key, defaultValue);\n\n return String(value);\n }\n\n /**\n * Get float input value\n */\n public float(key: string, defaultValue: number = 0): number {\n const value = this.input(key, defaultValue);\n\n return parseFloat(value) || 0;\n }\n\n /**\n * Get number input value\n */\n public number(key: string, defaultValue: number = 0): number {\n const value = Number(this.input(key, defaultValue));\n\n return isNaN(value) ? defaultValue : value;\n }\n\n /**\n * Immediate-peer IP as Fastify reports it — the address that connected to\n * the server socket, with `trustProxy` resolution applied. Use this when\n * you specifically need the peer address (rate-limit-by-direct-connection,\n * health-check origin verification).\n *\n * **For most use cases prefer `request.detectIp()`** — behind any proxy\n * (load balancer, CDN, sidecar) `ip` reports the proxy, not the real client.\n */\n public get ip() {\n return this.baseRequest.ip;\n }\n\n /**\n * Best-effort real client IP — checks `X-Real-IP` and `X-Forwarded-For`\n * headers first, falls back to `baseRequest.ip` when neither is present.\n *\n * For a multi-hop `X-Forwarded-For` (`client, proxy1, proxy2`) only the\n * FIRST entry is returned (the original client), comma-split and trimmed.\n * Without this, downstream scoping (ip-filter, rate-limit, idempotency)\n * would key off the whole header string and break behind chained proxies.\n *\n * **Prefer this over `request.ip` for any caller behind a proxy** (load\n * balancer, CDN, reverse proxy, k8s ingress). Only trust the result as\n * far as you trust the upstream proxy chain — `X-Forwarded-For` is\n * client-settable, so the leftmost entry is spoofable by the immediate\n * client when the request did NOT pass through a trusted edge that\n * appends/overwrites the header. Verify the request came through your\n * trusted edge before treating the value as authoritative.\n */\n public detectIp() {\n // as the server maybe used behind a proxy\n // then we need to check first if there is a forwarded ip\n // check for the real-ip header\n\n const realIp = this.header(\"x-real-ip\");\n\n if (realIp) return realIp;\n\n const forwardedIp = this.header(\"x-forwarded-for\");\n\n if (forwardedIp) {\n // `X-Forwarded-For` may carry the full proxy chain (\"client, proxy1, ...\").\n // The original client is the leftmost entry; split + trim so multi-hop\n // values don't leak the whole header string into IP-scoped logic.\n const firstHop = String(forwardedIp).split(\",\")[0].trim();\n\n if (firstHop) return firstHop;\n }\n\n return this.baseRequest.ip;\n }\n\n /**\n * An alias to detectIp\n */\n public get realIp() {\n return this.detectIp();\n }\n\n /**\n * Get request ips\n */\n public get ips() {\n return this.baseRequest.ips;\n }\n\n /**\n * Get request referer\n */\n public get referer() {\n return this.baseRequest.headers.referer;\n }\n\n /**\n * Get user agent\n */\n public get userAgent() {\n return this.baseRequest.headers[\"user-agent\"];\n }\n\n /**\n * Get request headers\n */\n public get headers(): typeof this.baseRequest.headers {\n return this.baseRequest.headers;\n }\n\n /**\n * Set the given header\n */\n public setHeader(key: HeaderKeys, value: string) {\n this.baseRequest.headers[key.toLowerCase()] = value;\n\n return this;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"request.mjs","names":[],"sources":["../../../../../../../core/src/http/request.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport events from \"@mongez/events\";\nimport { trans, transFrom } from \"@mongez/localization\";\nimport { Random, except, get, only, rtrim, set, unset } from \"@mongez/reinforcements\";\nimport { isEmpty } from \"@mongez/supportive-is\";\nimport type { LogLevel } from \"@warlock.js/logger\";\nimport { log } from \"@warlock.js/logger\";\nimport { BaseValidator, v } from \"@warlock.js/seal\";\nimport type { FastifyRequest } from \"fastify\";\nimport { type IncomingHttpHeaders } from \"node:http2\";\nimport { config } from \"../config/config-getter\";\nimport type { Middleware, Route } from \"../router\";\nimport { validateAll } from \"../validation/validateAll\";\nimport { createRequestStore } from \"./middleware/inject-request-context\";\nimport { Response } from \"./response\";\nimport type { RequestEvent } from \"./types\";\nimport { UploadedFile } from \"./uploaded-file\";\n\ntype StandardHeaders = {\n // copy every declared property from http.IncomingHttpHeaders\n // but remove index signatures\n [K in keyof IncomingHttpHeaders as string extends K\n ? never\n : number extends K\n ? never\n : K]: IncomingHttpHeaders[K];\n};\n\ntype HeaderKeys = keyof StandardHeaders;\n\nexport class Request<RequestValidation = any> {\n /**\n * Underlying Fastify request — a public escape hatch to capabilities the\n * framework's high-level helpers don't yet cover.\n *\n * **Prefer framework methods first**: `request.input()`, `request.header()`,\n * `request.body`, `request.query`, `request.params`, `request.file()`,\n * `request.user`, `request.detectIp()`, etc. They handle locale, parsing,\n * trust-proxy, and validation pipeline integration correctly.\n *\n * **Reach for `baseRequest` only** when the framework genuinely lacks a\n * helper for what you need — and when you do, file an issue so we can add\n * it. The escape hatch is the release valve that lets consumers move\n * faster than the framework, but every long-term reach here is a missing\n * helper waiting to be added.\n */\n public baseRequest!: FastifyRequest;\n\n /**\n * Response Object\n */\n public response!: Response;\n\n /**\n * Route Object\n */\n public route!: Route;\n\n /**\n * Parsed Request Payload\n */\n protected payload: any = {};\n\n /**\n * Decoded access token payload (set by auth middleware)\n */\n public decodedAccessToken?: any;\n\n /**\n * Current request instance\n */\n public static current: Request;\n\n /**\n * Translation method\n * Type of it is the same as the type of trans function\n */\n public trans: ReturnType<typeof trans> = trans;\n\n /**\n * Alias to trans method\n */\n public t: ReturnType<typeof trans> = trans;\n\n /**\n * Dynamic properties index signature\n *\n * This allows attaching custom properties to the request instance,\n * commonly used during validation middleware to attach fetched models.\n *\n * @example\n * // In validation middleware:\n * const post = await Post.find(request.int(\"id\"));\n * if (!post) return response.notFound();\n * request.post = post; // Attach the model to the request\n *\n * // In route handler:\n * const post = request.post;\n * // Work with the pre-fetched model\n */\n [key: string]: any;\n\n /**\n * Locale code\n */\n protected _locale = \"\";\n\n /**\n * Validated data\n */\n protected validatedData?: RequestValidation;\n\n /**\n * Request id\n */\n public id = Random.string(32);\n\n /**\n * Start Time\n */\n public startTime = Date.now();\n\n /**\n * End Time\n */\n public endTime?: undefined | number;\n\n /**\n * Set request handler\n */\n public setRequest(request: FastifyRequest) {\n this.baseRequest = request;\n\n this.resolveRequestId();\n\n this.parsePayload();\n\n const localeCode = this.getLocaleCode();\n\n this.trans = this.t = transFrom.bind(null, localeCode);\n\n return this;\n }\n\n /**\n * Inherit `X-Request-Id` from the incoming request, fall back to a custom\n * generator, then to the field-init default (`Random.string(32)`).\n *\n * Inherited values are validated (length cap + printable-ASCII) to prevent\n * log-injection from a malicious client. Disable the whole behavior by\n * setting `http.requestId.enabled = false` — in which case the field-init\n * default is used regardless of any incoming header.\n */\n protected resolveRequestId() {\n const requestIdConfig = config.key(\"http.requestId\") || {};\n\n if (requestIdConfig.enabled === false) return;\n\n const headerName = (requestIdConfig.header || \"x-request-id\").toLowerCase();\n const incoming = this.baseRequest.headers[headerName];\n\n if (Request.isValidRequestId(incoming)) {\n this.id = incoming;\n\n return;\n }\n\n if (typeof requestIdConfig.generator === \"function\") {\n this.id = requestIdConfig.generator();\n }\n }\n\n /**\n * Validate a candidate request-id value. Accepts non-empty printable ASCII\n * up to 128 characters — tight enough to reject newline / control-character\n * log-injection, loose enough to accept UUIDs, ULIDs, snowflakes, etc.\n */\n protected static isValidRequestId(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n value.length > 0 &&\n value.length <= 128 &&\n /^[\\x21-\\x7e]+$/.test(value)\n );\n }\n\n /**\n * Translate from the given locale code\n */\n public transFrom(localeCode: string, keyword: string, placeholders?: any) {\n return transFrom(localeCode, keyword, placeholders);\n }\n\n /**\n * Get current locale code\n */\n public get locale() {\n if (this._locale) return this._locale;\n\n return this.header(\"translation-locale-code\") || this.localized;\n }\n\n /**\n * Set locale code\n */\n public set locale(localeCode: string) {\n this._locale = localeCode;\n }\n\n /**\n * Get locale code that will be used for translation\n */\n public get localized() {\n if (this._locale) return this._locale;\n\n return (this._locale = this.header(\"locale\") || this.query[\"locale\"]);\n }\n\n /**\n * Set locale code\n */\n public setLocaleCode(localeCode: string) {\n this._locale = localeCode;\n\n return this;\n }\n\n /**\n * Get current locale code or return default locale code\n */\n public getLocaleCode(defaultLocaleCode: string = config.key(\"app.localeCode\") || \"en\") {\n return this.locale || defaultLocaleCode;\n }\n\n /**\n * Get http protocol\n */\n public get protocol() {\n return this.baseRequest.protocol;\n }\n\n /**\n * Validate the given validation schema\n */\n public async validate(validation: BaseValidator, selectedInputs?: string[]) {\n return await v.validate(validation, selectedInputs ? this.only(selectedInputs) : this.all());\n }\n\n /**\n * Clear current user\n */\n public clearCurrentUser() {\n this.user = undefined;\n }\n\n /**\n * Get value of the given header\n */\n public header<TCustomHeader extends string = HeaderKeys>(\n name: TCustomHeader | HeaderKeys,\n defaultValue: any = null,\n ) {\n return this.baseRequest.headers[name.toLocaleLowerCase()] ?? defaultValue;\n }\n\n /**\n * Get all cookies from the current request\n */\n public get cookies(): Record<string, string | undefined> {\n return this.baseRequest.cookies || {};\n }\n\n /**\n * Get a particular cookie value or fallback to default\n */\n public cookie(name: string, defaultValue?: any): string | any {\n const value = this.cookies[name] ?? defaultValue;\n\n try {\n return JSON.parse(value);\n } catch (error) {\n return value;\n }\n }\n\n /**\n * Determine if the request has the specified cookie\n */\n public hasCookie(name: string): boolean {\n return this.cookies[name] !== undefined;\n }\n\n /**\n * Get the current request domain\n */\n public get domain() {\n return this.baseRequest.hostname.replace(/^www\\./, \"\");\n }\n\n /**\n * Get hostname\n */\n public get hostname() {\n return this.domain;\n }\n\n /**\n * Get request origin\n */\n public get origin() {\n return this.baseRequest.headers.origin as string;\n }\n\n /**\n * Get the domain of the origin\n */\n public get originDomain() {\n const domain = this.origin ? new URL(this.origin).hostname : null;\n\n if (domain?.startsWith(\"www.\")) {\n return domain.replace(/^www\\./, \"\");\n }\n\n return domain;\n }\n\n /**\n * Get authorization header value\n */\n public get authorizationValue(): string {\n const authorization = this.header(\"authorization\");\n\n if (!authorization) return \"\";\n\n const [type, value] = authorization.split(\" \");\n\n if (![\"bearer\", \"key\"].includes(type.toLowerCase())) return \"\";\n\n return value || \"\";\n }\n\n /**\n * Get access token from Authorization header\n *\n * If the Authorization header does not start with `Bearer` value then return null\n */\n public get accessToken(): string | undefined {\n const authorization = this.header(\"authorization\");\n\n if (!authorization) return;\n\n const [type, value] = authorization.split(\" \");\n\n if (type.toLowerCase() !== \"bearer\") return;\n\n return value;\n }\n\n /**\n * Get the authorization header\n */\n public get authorization() {\n return this.header(\"authorization\");\n }\n\n /**\n * Get current request method\n */\n public get method(): string {\n return this.baseRequest.method;\n }\n\n /**\n * Parse the payload and merge it from the request body, params and query string\n */\n protected parsePayload() {\n this.payload.body = this.parseBody(this.baseRequest.body);\n\n this.payload.query = this.parseBody(this.baseRequest.query);\n this.payload.params = { ...(this.baseRequest.params || {}) };\n this.payload.all = {\n ...this.payload.body,\n ...this.payload.query,\n ...this.payload.params,\n };\n }\n\n /**\n * Parse body payload\n */\n protected parseBody(data: any) {\n try {\n if (!data) return {};\n\n const body: any = {};\n\n const arrayOfObjectValues: any = {};\n\n for (let key in data) {\n const value = data[key];\n\n let isArrayKey = false;\n\n if (key.endsWith(\"[]\")) {\n isArrayKey = true;\n }\n\n key = rtrim(key, \"[]\");\n\n // check if the key is has a square brackets, then convert it into object\n // i.e user[email] => user: {email: \"value\"}\n // also check if its an array of objects\n\n if (key.includes(\"[\")) {\n // check if its an array of objects\n if (key.includes(\"][\")) {\n const keyParts = key.split(\"[\");\n\n const keyName = keyParts[0];\n if (!arrayOfObjectValues[keyName]) {\n arrayOfObjectValues[keyName] = [];\n }\n\n const keyNameParts = keyParts[1].split(\"]\");\n\n const index = Number(keyNameParts[0]);\n\n if (!arrayOfObjectValues[keyName][index]) {\n arrayOfObjectValues[keyName][index] = {};\n }\n\n // now get the key after the index\n const keyNameParts2 = keyParts[2].split(\"]\");\n const keyName2 = keyNameParts2[0];\n\n arrayOfObjectValues[keyName][index][keyName2] = this.parseValue(value);\n\n continue;\n }\n\n const keyParts = key.split(\"[\");\n const keyName = keyParts[0];\n const keyNameParts = keyParts[1].split(\"]\");\n\n set(\n body,\n keyName + \".\" + keyNameParts[0],\n Array.isArray(value) ? value.map(this.parseValue.bind(this)) : this.parseValue(value),\n );\n\n continue;\n }\n\n if (Array.isArray(value)) {\n set(body, key, value.map(this.parseValue.bind(this)));\n } else if (isArrayKey) {\n if (body[key]) {\n body[key].push(this.parseValue(value));\n } else {\n body[key] = [this.parseValue(value)];\n\n continue;\n }\n } else {\n set(body, key, this.parseValue(value));\n }\n }\n\n // now merge the array of objects into the body\n for (const key in arrayOfObjectValues) {\n body[key] = arrayOfObjectValues[key];\n }\n\n return body;\n } catch (error) {\n console.log(error);\n this.log(error, \"error\");\n }\n }\n\n /**\n * Parse the given data\n */\n protected parseValue(data: any) {\n // data.value appears only in the multipart form data\n // if it json, then just return the data\n if (data?.file) return new UploadedFile(data);\n if (data?.value !== undefined && data?.fields && data?.type) {\n data = data.value;\n }\n\n if (data === \"false\") return false;\n\n if (data === \"true\") return true;\n\n if (data === \"null\") return null;\n\n if (typeof data === \"string\") return data.trim();\n\n return data;\n }\n\n /**\n * Set route handler\n */\n public setRoute(route: Route) {\n this.route = route;\n\n // pass the route to the response object\n this.response.setRoute(route);\n\n return this;\n }\n\n /**\n * Trigger an http event\n */\n public trigger(eventName: RequestEvent, ...args: any[]) {\n return events.trigger(`request.${eventName}`, ...args, this);\n }\n\n /**\n * Listen to the given event\n */\n public on(eventName: RequestEvent, callback: any) {\n return this.subscribe(eventName, callback);\n }\n\n /**\n * Make a log message\n */\n public log(message: any, level: LogLevel = \"info\") {\n if (!config.key(\"http.log\")) return;\n\n log.log({\n module: \"request\",\n action: this.route.method + \" \" + this.route.path.replace(\"/*\", \"\") + `:${this.id}`,\n message,\n type: level,\n context: {\n request: this,\n },\n });\n }\n\n /**\n * Get current request path\n */\n public get path() {\n return this.baseRequest.url;\n }\n\n /**\n * {@alias}\n */\n public get url() {\n return this.baseRequest.url;\n }\n\n /**\n * Get full url\n */\n public get fullUrl() {\n return this.protocol + \"://\" + this.hostname + this.path;\n }\n\n /**\n * Drive the middleware chain for the current route, then defer to the\n * controller. Returns the first response value any middleware short-circuits\n * with, or `undefined` to continue into validation + handler.\n *\n * @internal Framework orchestration — do not call from app code. Will move\n * to a dedicated controller dispatcher in a future refactor.\n */\n public async runMiddleware() {\n // measure request time\n // check for middleware first\n const middlewareOutput = await this.executeMiddleware();\n\n if (middlewareOutput !== undefined) {\n // 👇🏻 make sure first its not a response instance\n if (middlewareOutput instanceof Response) return middlewareOutput;\n // 👇🏻 send the response\n return this.response.send(middlewareOutput);\n }\n\n const handler = this.route.handler;\n\n if (!handler.validation) return;\n\n // 👇🏻 check for validation using validateAll helper function\n const validationOutput = await validateAll(handler.validation, this, this.response);\n\n return validationOutput;\n }\n\n /**\n * Return the request handler attached to the current route.\n *\n * @internal Framework orchestration — do not call from app code.\n */\n public getHandler() {\n return this.route.handler;\n }\n\n /**\n * Get inputs that has been validated only\n * You can also pass an array of inputs to get only the validated inputs\n */\n public validated<Output = RequestValidation>(inputs?: (keyof Output | (string & {}))[]): Output {\n if (this.validatedData) {\n return inputs\n ? only(this.validatedData as Output, inputs as string[])\n : (this.validatedData as Output);\n }\n\n return {} as Output;\n }\n\n /**\n * Get inputs that has been validated except the given inputs\n */\n public validatedExcept(...inputs: string[]): RequestValidation {\n return except(this.validated(), inputs);\n }\n\n /**\n * Set validated data\n */\n public setValidatedData(data: RequestValidation) {\n this.validatedData = data;\n }\n\n /**\n * Top-level entry into the request lifecycle — opens the context store,\n * runs middleware, drives the handler, handles errors.\n *\n * @internal Framework orchestration — do not call from app code. Wired\n * from the Fastify route handler in `router.scan()`.\n */\n public async execute() {\n try {\n // call executingAction event\n\n this.log(\"Executing the request\");\n\n return await createRequestStore(this, this.response);\n } catch (error) {\n this.log(error, \"error\");\n\n throw error;\n }\n }\n\n /**\n * Iterate the collected middlewares in order; return the first short-circuit\n * value or `undefined` when every middleware passes through.\n *\n * @internal Framework orchestration — do not call from app code.\n */\n protected async executeMiddleware() {\n // collect all middlewares for current route\n const middlewares = this.collectMiddlewares();\n\n // check if there are no middlewares, then return\n if (middlewares.length === 0) return;\n\n this.log(\"About to execute request middlewares\");\n\n // trigger the executingMiddleware event\n this.trigger(\"executingMiddleware\", middlewares, this.route);\n\n for (const middleware of middlewares) {\n this.log(\"Executing middleware \" + colors.yellowBright(middleware.name));\n const output = await middleware(this, this.response);\n this.log(\"Executed middleware \" + colors.yellowBright(middleware.name), \"success\");\n\n if (output !== undefined) {\n this.log(\n colors.yellow(\"request intercepted by middleware \") + colors.cyanBright(middleware.name),\n \"warn\",\n );\n\n this.trigger(\"executedMiddleware\");\n\n this.log(\"Request middlewares executed\", \"success\");\n\n return output;\n }\n }\n\n this.log(\"Request middlewares executed\", \"success\");\n\n // trigger the executedMiddleware event\n this.trigger(\"executedMiddleware\", middlewares, this.route);\n }\n\n /**\n * Gather the middleware list for the current route — today just the\n * route-level array; future extraction may merge group + app-wide layers.\n *\n * @internal Framework orchestration — do not call from app code.\n */\n protected collectMiddlewares(): Middleware[] {\n const middlewaresList: Middleware[] = [];\n\n // collect route middlewares\n if (this.route.middleware) {\n middlewaresList.push(...this.route.middleware);\n }\n\n return middlewaresList;\n }\n\n /**\n * Get request input value from query string, params or body\n */\n public input(key: string, defaultValue?: any) {\n return get(this.payload.all, key, defaultValue);\n }\n\n /**\n * Get email input value, this will lowercase the value\n */\n public email(key: string = \"email\", defaultValue: string = \"\"): string {\n return this.input(key, defaultValue)?.toLowerCase() || defaultValue;\n }\n\n /**\n * @alias input\n */\n public get(key: string, defaultValue?: any) {\n return this.input(key, defaultValue);\n }\n\n /**\n * Determine if request has input value\n */\n public has(key: string) {\n return get(this.payload.all, key, undefined) !== undefined;\n }\n\n /**\n * Set request input value\n */\n public set(key: string, value: any) {\n set(this.payload.all, key, value);\n\n return this;\n }\n\n /**\n * Set the given value if the request does not have the input\n */\n public setDefault(key: string, value: any) {\n if (this.has(key)) return this;\n\n set(this.payload.all, key, value);\n\n return this;\n }\n\n /**\n * Unset request payload keys\n */\n public unset(...keys: string[]) {\n this.payload.all = unset(this.payload.all, keys);\n\n return this;\n }\n\n /**\n * Get request body\n */\n public get body() {\n return this.payload.body;\n }\n\n /**\n * Set request body value\n */\n public setBody(key: string, value: any) {\n set(this.payload.body, key, value);\n\n return this;\n }\n\n /**\n * Get body inputs except files\n */\n public get bodyInputs() {\n const inputs = this.payload.body;\n\n const bodyInputs: any = {};\n\n for (const key in inputs) {\n const value = inputs[key];\n\n if (value.file && value.fieldname) continue;\n\n bodyInputs[key] = value;\n }\n\n return bodyInputs;\n }\n\n /**\n * Get request file in UploadedFile instance\n */\n public file(key: string): UploadedFile | undefined {\n const file = this.input(key);\n\n return file;\n }\n\n /**\n * Get uploaded files from the request for the given name\n * If the given name is not present in the request, return an empty array\n */\n public files(name: string): UploadedFile[] {\n return this.input(name) || [];\n }\n\n /**\n * Get request params\n */\n public get params() {\n return this.payload.params;\n }\n\n /**\n * Set request params value\n */\n public setParam(key: string, value: any) {\n set(this.payload.params, key, value);\n\n return this;\n }\n\n /**\n * Get request query\n */\n public get query() {\n return this.payload.query;\n }\n\n /**\n * Set request query value\n */\n public setQuery(key: string, value: any) {\n set(this.payload.query, key, value);\n\n return this;\n }\n\n /**\n * Get all inputs\n */\n public all() {\n return this.payload.all;\n }\n\n /**\n * Get all inputs except params\n */\n public allExceptParams() {\n return {\n ...this.payload.query,\n ...this.payload.body,\n };\n }\n\n /**\n * Get all heavy inputs except params\n */\n public heavyExceptParams() {\n const inputs = this.allExceptParams();\n\n const heavyInputs: any = {};\n\n for (const key in inputs) {\n const value = inputs[key];\n\n if (isEmpty(value) && value !== null) continue;\n\n heavyInputs[key] = value;\n }\n\n return heavyInputs;\n }\n\n /**\n * Get only heavy inputs, the input with a value\n */\n public heavy() {\n const inputs = this.all();\n\n const heavyInputs: any = {};\n\n for (const key in inputs) {\n const value = inputs[key];\n\n if (isEmpty(value) && value !== null) continue;\n\n heavyInputs[key] = value;\n }\n\n return heavyInputs;\n }\n\n /**\n * Get only the given keys from the request data\n */\n public only(keys: string[]) {\n return only(this.all(), keys);\n }\n\n /**\n * Pluck the given keys from the request data\n */\n public pluck(keys: string[]) {\n const data = this.only(keys);\n\n this.unset(...keys);\n\n return data;\n }\n\n /**\n * Get all request inputs except the given keys\n */\n public except(keys: string[]) {\n return except(this.all(), keys);\n }\n\n /**\n * Get boolean input value\n */\n public bool(key: string, defaultValue = false) {\n const value = this.input(key, defaultValue);\n\n if (value === \"true\") {\n return true;\n }\n\n if (value === \"false\") {\n return false;\n }\n\n if (value === 0) {\n return false;\n }\n\n return Boolean(value);\n }\n\n /**\n * Get integer input value\n */\n public int(key: string, defaultValue: number = 0): number | undefined {\n const value = this.input(key, defaultValue);\n\n if (!value && value !== 0) return undefined;\n\n return parseInt(value);\n }\n\n /**\n * Shorthand getter to get id param\n */\n public get idParam() {\n return this.int(\"id\");\n }\n\n /**\n * Get string input value\n */\n public string(key: string, defaultValue: string = \"\"): string {\n const value = this.input(key, defaultValue);\n\n return String(value);\n }\n\n /**\n * Get float input value\n */\n public float(key: string, defaultValue: number = 0): number {\n const value = this.input(key, defaultValue);\n\n return parseFloat(value) || 0;\n }\n\n /**\n * Get number input value\n */\n public number(key: string, defaultValue: number = 0): number {\n const value = Number(this.input(key, defaultValue));\n\n return isNaN(value) ? defaultValue : value;\n }\n\n /**\n * Immediate-peer IP as Fastify reports it — the address that connected to\n * the server socket, with `trustProxy` resolution applied. Use this when\n * you specifically need the peer address (rate-limit-by-direct-connection,\n * health-check origin verification).\n *\n * **For most use cases prefer `request.detectIp()`** — behind any proxy\n * (load balancer, CDN, sidecar) `ip` reports the proxy, not the real client.\n */\n public get ip() {\n return this.baseRequest.ip;\n }\n\n /**\n * Best-effort real client IP — the value everything IP-scoped keys on\n * (ip-filter allowlists, rate-limit buckets, idempotency scoping).\n *\n * `X-Forwarded-For` resolution is **delegated to Fastify**: `baseRequest.ip`\n * is already the client address Fastify's `trustProxy` machinery picked out\n * of the chain, so every shape `http.trustProxy` accepts is honoured here\n * with exactly the semantics Fastify documents:\n *\n * - `false` (default) — no header is trusted; the socket peer address wins.\n * Both forwarding headers are client-settable, so without a trusted edge\n * that rewrites them any client could otherwise forge its own IP.\n * - `true` — the whole chain is trusted; the leftmost hop (original client)\n * wins.\n * - `number` — that many rightmost hops are trusted, so an edge that\n * APPENDS to `X-Forwarded-For` yields the real client rather than whatever\n * the client prepended.\n * - CIDR / IP list (string, comma-separated string, or array) or a custom\n * predicate — the chain is walked right-to-left and stops at the first hop\n * that isn't a trusted proxy.\n *\n * `X-Real-IP` is NOT part of that resolution — Fastify never looks at it,\n * and unlike `X-Forwarded-For` it carries no chain, so there is nothing to\n * validate a hop count or proxy allowlist against. It is therefore honoured\n * only under `trustProxy: true` (\"everything upstream is mine\"), where it is\n * no weaker than the trust already granted. Under a bounded `trustProxy`\n * (hop count / CIDR list) it is ignored: a trusted-but-passthrough edge that\n * forwards the client's own `X-Real-IP` verbatim would otherwise hand any\n * client a way around the bound.\n *\n * **Prefer this over `request.ip` for any caller behind a proxy** (load\n * balancer, CDN, reverse proxy, k8s ingress).\n */\n public detectIp() {\n // Trusting `X-Real-IP` is only sound when the config trusts the entire\n // upstream chain; bounded shapes get chain-aware resolution instead.\n if (config.get(\"http.trustProxy\", false) === true) {\n const realIp = this.header(\"x-real-ip\");\n\n if (realIp) {\n const address = String(realIp).split(\",\")[0].trim();\n\n if (address) return address;\n }\n }\n\n // Fastify resolved this against the configured `trustProxy` already:\n // socket peer when trust is off, the correct hop of `X-Forwarded-For`\n // when it is on. Re-parsing the header here would mean a second, weaker\n // trust model that could disagree with `request.ip` and with the plugins\n // (rate limit, proxy) that key on it.\n return this.baseRequest.ip;\n }\n\n /**\n * An alias to detectIp\n */\n public get realIp() {\n return this.detectIp();\n }\n\n /**\n * Get request ips\n */\n public get ips() {\n return this.baseRequest.ips;\n }\n\n /**\n * Get request referer\n */\n public get referer() {\n return this.baseRequest.headers.referer;\n }\n\n /**\n * Get user agent\n */\n public get userAgent() {\n return this.baseRequest.headers[\"user-agent\"];\n }\n\n /**\n * Get request headers\n */\n public get headers(): typeof this.baseRequest.headers {\n return this.baseRequest.headers;\n }\n\n /**\n * Set the given header\n */\n public setHeader(key: HeaderKeys, value: string) {\n this.baseRequest.headers[key.toLowerCase()] = value;\n\n return this;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.mts","names":[],"sources":["../../../../../../../core/src/http/server.ts"],"mappings":";;;KAGY,iBAAA,GAAkB,UAAU,QAAQ,OAAA;AAAA,iBAKhC,eAAA,CAAgB,OAAA,GAAU,oBAAA,GAAuB,iBAAe;AALhF;;;AAAA,iBAiCgB,aAAA,IAAiB,iBAAe;AAjCO;AAKvD;;;AALuD,KAyC3C,cAAA;EAAmB,KAAA,QAAa,OAAO;AAAA;;;AApC6B;AA4BhF;;;;iBAiBsB,sBAAA,CACpB,MAAA,EAAQ,cAAA,EACR,SAAA,WACC,OAAO"}
1
+ {"version":3,"file":"server.d.mts","names":[],"sources":["../../../../../../../core/src/http/server.ts"],"mappings":";;;KAGY,iBAAA,GAAkB,UAAU,QAAQ,OAAA;AAAA,iBAKhC,eAAA,CAAgB,OAAA,GAAU,oBAAA,GAAuB,iBAAe;AALhF;;;AAAA,iBAsCgB,aAAA,IAAiB,iBAAe;AAtCO;AAKvD;;;AALuD,KA8C3C,cAAA;EAAmB,KAAA,QAAa,OAAO;AAAA;;;AAzC6B;AAiChF;;;;iBAiBsB,sBAAA,CACpB,MAAA,EAAQ,cAAA,EACR,SAAA,WACC,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.mjs","names":[],"sources":["../../../../../../../core/src/http/server.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport Fastify, { FastifyServerOptions } from \"fastify\";\r\n\r\nexport type FastifyInstance = ReturnType<typeof Fastify>;\r\n\r\n// Instantiate Fastify server\r\nlet server: FastifyInstance | undefined = undefined;\r\n\r\nexport function startHttpServer(options?: FastifyServerOptions): FastifyInstance {\r\n // `config.set(key, undefined)` stores null rather than unsetting, so an app\r\n // that clears a key would otherwise hand Fastify `null` and crash the boot.\r\n const bodyLimit = config.get(\"http.bodyLimit\") ?? undefined;\r\n\r\n return (server = Fastify({\r\n // `X-Forwarded-For` is client-settable and spoofable, and `request.ip` is\r\n // what @fastify/rate-limit keys its buckets on — so trusting it by default\r\n // makes rate limiting bypassable on any deployment NOT behind a proxy that\r\n // strips the header. Apps behind such a proxy opt in explicitly.\r\n trustProxy: config.get(\"http.trustProxy\", false),\r\n // No default: an app that configures nothing keeps Fastify's own 1MB limit\r\n // rather than the historical 200GB, which silently removed the protection\r\n // Fastify provides. Per-route caps go through `serverOptions.bodyLimit`;\r\n // the `maxBodySize()` middleware runs AFTER parsing and cannot reject\r\n // before the bytes are resident.\r\n ...(bodyLimit !== undefined && { bodyLimit }),\r\n // Close idle keep-alive connections on shutdown while letting in-flight\r\n // requests finish — the basis for graceful draining. Override via\r\n // `http.gracefulShutdown.forceCloseConnections`.\r\n forceCloseConnections: config.get(\"http.gracefulShutdown.forceCloseConnections\", \"idle\"),\r\n ...options,\r\n }));\r\n}\r\n\r\n/**\r\n * Expose the server to be publicly accessible\r\n */\r\nexport function getHttpServer(): FastifyInstance {\r\n return server;\r\n}\r\n\r\n/**\r\n * Minimal shape needed to close a server — lets {@link closeServerWithTimeout}\r\n * be unit-tested with a fake instead of a real Fastify instance.\r\n */\r\nexport type ClosableServer = { close: () => Promise<unknown> };\r\n\r\n/**\r\n * Close a server, bounded by a timeout. Fastify's `close()` stops accepting new\r\n * requests (it answers 503 while closing) and drains the in-flight ones; this\r\n * wraps it so a single stuck request can't hang shutdown forever.\r\n *\r\n * @returns `true` if the server drained cleanly, `false` if the timeout fired first.\r\n */\r\nexport async function closeServerWithTimeout(\r\n server: ClosableServer,\r\n timeoutMs: number,\r\n): Promise<boolean> {\r\n let timer: ReturnType<typeof setTimeout> | undefined;\r\n\r\n const drained = server.close().then(() => true);\r\n\r\n const timedOut = new Promise<boolean>((resolve) => {\r\n timer = setTimeout(() => resolve(false), timeoutMs);\r\n });\r\n\r\n try {\r\n return await Promise.race([drained, timedOut]);\r\n } finally {\r\n if (timer) {\r\n clearTimeout(timer);\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;AAMA,IAAI,SAAsC;AAE1C,SAAgB,gBAAgB,SAAiD;CAG/E,MAAM,YAAY,OAAO,IAAI,gBAAgB,KAAK;CAElD,OAAQ,SAAS,QAAQ;EAKvB,YAAY,OAAO,IAAI,mBAAmB,KAAK;EAM/C,GAAI,cAAc,UAAa,EAAE,UAAU;EAI3C,uBAAuB,OAAO,IAAI,+CAA+C,MAAM;EACvF,GAAG;CACL,CAAC;AACH;;;;AAKA,SAAgB,gBAAiC;CAC/C,OAAO;AACT;;;;;;;;AAeA,eAAsB,uBACpB,QACA,WACkB;CAClB,IAAI;CAEJ,MAAM,UAAU,OAAO,MAAM,CAAC,CAAC,WAAW,IAAI;CAE9C,MAAM,WAAW,IAAI,SAAkB,YAAY;EACjD,QAAQ,iBAAiB,QAAQ,KAAK,GAAG,SAAS;CACpD,CAAC;CAED,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CAAC,SAAS,QAAQ,CAAC;CAC/C,UAAU;EACR,IAAI,OACF,aAAa,KAAK;CAEtB;AACF"}
1
+ {"version":3,"file":"server.mjs","names":[],"sources":["../../../../../../../core/src/http/server.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport Fastify, { FastifyServerOptions } from \"fastify\";\r\n\r\nexport type FastifyInstance = ReturnType<typeof Fastify>;\r\n\r\n// Instantiate Fastify server\r\nlet server: FastifyInstance | undefined = undefined;\r\n\r\nexport function startHttpServer(options?: FastifyServerOptions): FastifyInstance {\r\n // `config.set(key, undefined)` stores null rather than unsetting, so an app\r\n // that clears a key would otherwise hand Fastify `null` and crash the boot.\r\n const bodyLimit = config.get(\"http.bodyLimit\") ?? undefined;\r\n\r\n return (server = Fastify({\r\n // `X-Forwarded-For` is client-settable and spoofable, and `request.ip` is\r\n // what @fastify/rate-limit keys its buckets on — so trusting it by default\r\n // makes rate limiting bypassable on any deployment NOT behind a proxy that\r\n // strips the header. Apps behind such a proxy opt in explicitly.\r\n //\r\n // The value is passed through untouched so every shape Fastify supports\r\n // works: `true`, a hop count, a CIDR/IP list (string, comma-separated\r\n // string or array), or a predicate. `request.detectIp()` reads the client\r\n // off `request.ip`, so it resolves the chain exactly the same way.\r\n trustProxy: config.get(\"http.trustProxy\", false),\r\n // No default: an app that configures nothing keeps Fastify's own 1MB limit\r\n // rather than the historical 200GB, which silently removed the protection\r\n // Fastify provides. Per-route caps go through `serverOptions.bodyLimit`;\r\n // the `maxBodySize()` middleware runs AFTER parsing and cannot reject\r\n // before the bytes are resident.\r\n ...(bodyLimit !== undefined && { bodyLimit }),\r\n // Close idle keep-alive connections on shutdown while letting in-flight\r\n // requests finish — the basis for graceful draining. Override via\r\n // `http.gracefulShutdown.forceCloseConnections`.\r\n forceCloseConnections: config.get(\"http.gracefulShutdown.forceCloseConnections\", \"idle\"),\r\n ...options,\r\n }));\r\n}\r\n\r\n/**\r\n * Expose the server to be publicly accessible\r\n */\r\nexport function getHttpServer(): FastifyInstance {\r\n return server;\r\n}\r\n\r\n/**\r\n * Minimal shape needed to close a server — lets {@link closeServerWithTimeout}\r\n * be unit-tested with a fake instead of a real Fastify instance.\r\n */\r\nexport type ClosableServer = { close: () => Promise<unknown> };\r\n\r\n/**\r\n * Close a server, bounded by a timeout. Fastify's `close()` stops accepting new\r\n * requests (it answers 503 while closing) and drains the in-flight ones; this\r\n * wraps it so a single stuck request can't hang shutdown forever.\r\n *\r\n * @returns `true` if the server drained cleanly, `false` if the timeout fired first.\r\n */\r\nexport async function closeServerWithTimeout(\r\n server: ClosableServer,\r\n timeoutMs: number,\r\n): Promise<boolean> {\r\n let timer: ReturnType<typeof setTimeout> | undefined;\r\n\r\n const drained = server.close().then(() => true);\r\n\r\n const timedOut = new Promise<boolean>((resolve) => {\r\n timer = setTimeout(() => resolve(false), timeoutMs);\r\n });\r\n\r\n try {\r\n return await Promise.race([drained, timedOut]);\r\n } finally {\r\n if (timer) {\r\n clearTimeout(timer);\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;AAMA,IAAI,SAAsC;AAE1C,SAAgB,gBAAgB,SAAiD;CAG/E,MAAM,YAAY,OAAO,IAAI,gBAAgB,KAAK;CAElD,OAAQ,SAAS,QAAQ;EAUvB,YAAY,OAAO,IAAI,mBAAmB,KAAK;EAM/C,GAAI,cAAc,UAAa,EAAE,UAAU;EAI3C,uBAAuB,OAAO,IAAI,+CAA+C,MAAM;EACvF,GAAG;CACL,CAAC;AACH;;;;AAKA,SAAgB,gBAAiC;CAC/C,OAAO;AACT;;;;;;;;AAeA,eAAsB,uBACpB,QACA,WACkB;CAClB,IAAI;CAEJ,MAAM,UAAU,OAAO,MAAM,CAAC,CAAC,WAAW,IAAI;CAE9C,MAAM,WAAW,IAAI,SAAkB,YAAY;EACjD,QAAQ,iBAAiB,QAAQ,KAAK,GAAG,SAAS;CACpD,CAAC;CAED,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CAAC,SAAS,QAAQ,CAAC;CAC/C,UAAU;EACR,IAAI,OACF,aAAa,KAAK;CAEtB;AACF"}
@@ -150,6 +150,28 @@ interface HttpConfigurations {
150
150
  * @default 200 * 1024 * 1024 * 1024 // 200GB — historical default; consider lowering for production.
151
151
  */
152
152
  bodyLimit?: number;
153
+ /**
154
+ * Which upstream hops may be trusted to report the real client address via
155
+ * `X-Forwarded-For`. Passed straight to Fastify, so every shape Fastify
156
+ * supports works here, and `request.ip` / `request.detectIp()` resolve the
157
+ * client identically:
158
+ *
159
+ * - `false` (default) — trust nothing; the socket peer address is the client.
160
+ * - `true` — trust the whole chain; the leftmost `X-Forwarded-For` entry wins.
161
+ * - `number` — trust that many rightmost hops (an edge that APPENDS to
162
+ * `X-Forwarded-For`; `2` = "my CDN plus my load balancer").
163
+ * - `string` / `string[]` — trust only these proxy addresses: exact IPs,
164
+ * CIDR blocks (`"10.0.0.0/8"`), the named ranges `"loopback"`,
165
+ * `"linklocal"`, `"uniquelocal"`, or a comma-separated string of those.
166
+ * - `(address, hop) => boolean` — custom predicate.
167
+ *
168
+ * Anything but `false` is a trust boundary: every hop you trust can forge the
169
+ * addresses to its left. Prefer the narrowest shape your topology allows —
170
+ * `true` is only safe when nothing but your edge can reach the process.
171
+ *
172
+ * @default false
173
+ */
174
+ trustProxy?: boolean | number | string | string[] | ((address: string, hop: number) => boolean);
153
175
  cookies?: {
154
176
  /**
155
177
  * Secret key for signed cookies
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/http/types.ts"],"mappings":";;;;;;;KAMY,YAAA;;;AAAZ;KASY,gBAAA;;;AATY;AAapB;;;;EAIA,OAAA,CAAQ,QAAA;;;;EAIR,MAAA;;;;EAIA,OAAA,CAAQ,MAAA;;;;;;;;EAQR,OAAA;AAKJ;;;AAAA,KAAY,aAAA;AAAa;AAiEzB;;AAjEyB;;;;;;;AAgFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAfP,iBAAA;EA+NL;;;;EA1NV,MAAA;EA8NO;;;;;EAxNP,WAAA;EAoOA;;AAAK;EAhOL,UAAA,EAAY,UAAU;AAAA;;;;UAMP,kBAAA;EAiPwB;;;EA7OvC,IAAA;EAgOO;;;EA5NP,GAAA;EAgOA;;;EA5NA,IAAA,GAAO,kBAAA;EAgOI;;;;;EA1NX,eAAA;EAmOK;;;;;;;;;EAzNL,SAAA;EACA,OAAA;;;;IAIE,MAAA;;;;IAIA,OAAA,GAAU,sBAAA;EAAA;;;;EAKZ,SAAA;;;;;;IAME,GAAA;;;;;;IAMA,QAAA;EAAA;;;;;;;;EASF,SAAA;;;;;;IAME,MAAA;;;;IAIA,SAAA;;;;;;;IAOA,OAAA;EAAA;;;;EAKF,WAAA;;;;;;IAME,GAAA;;;;;;IAMA,UAAA;;;;;;;IAOA,OAAA;;;;IAIA,MAAA;EAAA;;;;EAKF,WAAA;;;;;;;IAOE,OAAA;;;;;;IAMA,SAAA;;;;;;IAMA,UAAA;EAAA;;;;EAKF,gBAAA;;;;;;;IAOE,OAAA;;;;;;;;IAQA,qBAAA;EAAA;;;;EAKF,MAAA;;;;;;IAME,OAAA;;;;;;;IAOA,IAAA;;;;;;;IAOA,aAAA;EAAA;;;;EAKF,IAAA;;;;EAIA,UAAA;;;;IAIE,GAAA,GAAM,UAAA;;;;IAIN,IAAA,GAAO,iBAAA;;;;IAIP,MAAA,GAAS,iBAAA;EAAA;AAAA;AAAA,KAID,wBAAA;;;;EAIV,IAAA,GAAO,IAAA;;;;EAIP,MAAA,GAAS,IAAA,EAAM,KAAA,CAAM,SAAS;;;;EAI9B,GAAA;;;;EAIA,KAAA;AAAA;AAAA,KAGU,qBAAA;;;;;;;EAOV,IAAA,GAAO,KAAA,UAAe,IAAA,OAAW,EAAA,cAAgB,qBAAA;;;;EAIjD,OAAA,GAAU,OAAA,aAAoB,qBAAA;;;;EAI9B,GAAA,QAAW,qBAAA;;;;;EAKX,YAAA,GAAe,OAAA,iBAAwB,qBAAA;;;;EAIvC,KAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/http/types.ts"],"mappings":";;;;;;;KAMY,YAAA;;;AAAZ;KASY,gBAAA;;;AATY;AAapB;;;;EAIA,OAAA,CAAQ,QAAA;;;;EAIR,MAAA;;;;EAIA,OAAA,CAAQ,MAAA;;;;;;;;EAQR,OAAA;AAKJ;;;AAAA,KAAY,aAAA;AAAa;AAiEzB;;AAjEyB;;;;;;;AAgFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAfP,iBAAA;EAiPJ;;AAAiB;AAI9B;EAhPE,MAAA;;;;;;EAMA,WAAA;EAkPqB;;;EA9OrB,UAAA,EAAY,UAAU;AAAA;AAsPjB;AAGP;;AAHO,UAhPU,kBAAA;EA0PkC;;;EAtPjD,IAAA;EAmQ4D;;;EA/P5D,GAAA;EAkPsB;;;EA9OtB,IAAA,GAAO,kBAAA;EAkPG;;;;;EA5OV,eAAA;EAqPuC;;;AAIlC;;;;;;EA/OL,SAAA;;;;;;;;;;;;;;;;;;;;;;EAsBA,UAAA,4CAAsD,OAAA,UAAiB,GAAA;EACvE,OAAA;;;;IAIE,MAAA;;;;IAIA,OAAA,GAAU,sBAAA;EAAA;;;;EAKZ,SAAA;;;;;;IAME,GAAA;;;;;;IAMA,QAAA;EAAA;;;;;;;;EASF,SAAA;;;;;;IAME,MAAA;;;;IAIA,SAAA;;;;;;;IAOA,OAAA;EAAA;;;;EAKF,WAAA;;;;;;IAME,GAAA;;;;;;IAMA,UAAA;;;;;;;IAOA,OAAA;;;;IAIA,MAAA;EAAA;;;;EAKF,WAAA;;;;;;;IAOE,OAAA;;;;;;IAMA,SAAA;;;;;;IAMA,UAAA;EAAA;;;;EAKF,gBAAA;;;;;;;IAOE,OAAA;;;;;;;;IAQA,qBAAA;EAAA;;;;EAKF,MAAA;;;;;;IAME,OAAA;;;;;;;IAOA,IAAA;;;;;;;IAOA,aAAA;EAAA;;;;EAKF,IAAA;;;;EAIA,UAAA;;;;IAIE,GAAA,GAAM,UAAA;;;;IAIN,IAAA,GAAO,iBAAA;;;;IAIP,MAAA,GAAS,iBAAA;EAAA;AAAA;AAAA,KAID,wBAAA;;;;EAIV,IAAA,GAAO,IAAA;;;;EAIP,MAAA,GAAS,IAAA,EAAM,KAAA,CAAM,SAAS;;;;EAI9B,GAAA;;;;EAIA,KAAA;AAAA;AAAA,KAGU,qBAAA;;;;;;;EAOV,IAAA,GAAO,KAAA,UAAe,IAAA,OAAW,EAAA,cAAgB,qBAAA;;;;EAIjD,OAAA,GAAU,OAAA,aAAoB,qBAAA;;;;EAI9B,GAAA,QAAW,qBAAA;;;;;EAKX,YAAA,GAAe,OAAA,iBAAwB,qBAAA;;;;EAIvC,KAAA;AAAA"}
package/llms-full.txt CHANGED
@@ -4692,7 +4692,7 @@ export default defineConfig({
4692
4692
  plugins: [lowerStage3Decorators(), mongezVite()],
4693
4693
  test: {
4694
4694
  globalSetup: "./src/test-global-setup.ts", // ← starts the HTTP server
4695
- setupFiles: ["./src/test-setup.ts"], // ← per-worker setupTest (see test-service skill)
4695
+ setupFiles: ["./src/test-setup.ts"], // ← setupTest + afterAll(teardownTest), per test file
4696
4696
  environment: "node",
4697
4697
  globals: false,
4698
4698
  include: ["src/app/**/*.test.ts"],
@@ -4700,7 +4700,13 @@ export default defineConfig({
4700
4700
  });
4701
4701
  ```
4702
4702
 
4703
- Both files (and this config, with `lowerStage3Decorators()` first so decorated models load) are created by `warlock add test`. The split is intentional: `globalSetup` runs ONCE in the main vitest process; `setupFiles` runs per worker thread.
4703
+ Both files (and this config, with `lowerStage3Decorators()` first so decorated models load) are created by `warlock add test`. The split is intentional: `globalSetup` runs **ONCE** in the main vitest process; `setupFiles` runs **before every test file**.
4704
+
4705
+ ⚠ **Corrected in 4.14.0.** This line previously said `setupFiles` runs "per worker thread". **It does not** — Vitest runs it before each test file, and the setup module's registry is rebuilt every time. Measured across all four `pool` × `isolate` combinations.
4706
+
4707
+ **So the service-layer framework is file-scoped: bootstrapped by `setupTest` and closed by the `afterAll(teardownTest)` the setup file registers, once per test file.** ⚠ **`setupTest` alone is not the whole wiring — the paired teardown is mandatory from 4.14.0**; see the `test-service` skill.
4708
+
4709
+ **HTTP is the exception and stays in `globalSetup`**, which genuinely does run once in the main vitest process and owns a real port. **That split is the point:** one server for the whole run, one framework per test file.
4704
4710
 
4705
4711
  ## HTTP request helpers
4706
4712
 
@@ -4901,14 +4907,22 @@ This is fine for normal test flow. It bites when you're inside a transaction the
4901
4907
 
4902
4908
  ---
4903
4909
  name: test-service
4904
- description: 'Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps each Vitest worker with its own DB/cache connections so you can call your code directly. Triggers: `setupTest`, `src/test-setup.ts`, `tests.connectors`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest } 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`.'
4910
+ description: '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`.'
4905
4911
  ---
4906
4912
 
4907
4913
  # Warlock — test a service
4908
4914
 
4909
- For unit tests, you import the thing under test and call it directly. No HTTP, no fetch, no controllers. Framework testing in Warlock is about getting your **service layer** under test efficiently — and that means each Vitest worker needs its own bootstrapped framework with a DB connection.
4915
+ For unit tests, you import the thing under test and call it directly. No HTTP, no fetch, no controllers. Framework testing in Warlock is about getting your **service layer** under test efficiently — and that means your tests need a bootstrapped framework with a DB connection.
4916
+
4917
+ `setupTest()` is the one-call bootstrap that provides that environment; `teardownTest()` closes it.
4918
+
4919
+ ⚠ **Corrected in 4.14.0 — `setupTest` is CALLED once per TEST FILE, not once per worker.** Every version of this skill through 4.13.0 said "per worker", the generated `src/test-setup.ts` carries a `Per-Worker Test Setup` comment saying the same thing, and **both were wrong.** Vitest runs `setupFiles` before **each test file**, and the setup module's registry is rebuilt every time — measured across all four `pool` × `isolate` combinations.
4910
4920
 
4911
- `setupTest()` is the one-call bootstrap that gives each worker that environment.
4921
+ ⛔ **If your project was generated before 4.14.0, fix BOTH the comment and the call.** The comment is false, **and** the generated `setupTest({ connectors: true })` is now an *explicit* value that overrides your `src/config/tests.ts`. **Bare `setupTest()` is the correct call.**
4922
+
4923
+ **The lifetime is FILE-SCOPED, on purpose.** Your setup file bootstraps the framework and its `afterAll(teardownTest)` closes it, once per test file. **One owner, one pairing — correct under every pool, every isolation setting, and watch mode.**
4924
+
4925
+ ⚠ **A worker-scoped lifetime is possible and is deliberately not shipped yet.** Lifecycle state now lives in the worker runtime, so leaving the framework running would let every file in a worker share one bootstrap. Two things block claiming it: under `pool: "threads"` we cannot observe whether Node reclaims a torn-down thread's sockets and pools, and **in watch mode Vitest reuses workers between reruns, so there is no recycle and no cleanup owner.** It gets taken when the real cost is measured and the integration is chosen, not inherited. See *Lifecycle and repeated calls* below.
4912
4926
 
4913
4927
  ⚠ **Changed in 4.13.0 — the import is a subpath now.** `setupTest` used to be re-exported from the package root; it is not any more, because that put the test helpers into every application's production module graph. `import { setupTest } from "@warlock.js/core"` now fails with *"has no exported member"* — **add `/tests` to the specifier and nothing else changes.**
4914
4928
 
@@ -4935,9 +4949,9 @@ describe("registerUserService", () => {
4935
4949
  });
4936
4950
  ```
4937
4951
 
4938
- No `beforeAll(setupTest)` in this file — the project's `src/test-setup.ts` (registered as `setupFiles` in `vite.config.ts`) already ran it once per worker before any test executed.
4952
+ No `beforeAll(setupTest)` in this file — the project's `src/test-setup.ts` (registered as `setupFiles` in `vite.config.ts`) already ran it **before this file's tests executed**, as it does before every test file.
4939
4953
 
4940
- ## `setupTest({ connectors })` — the worker bootstrap
4954
+ ## `setupTest({ connectors })` — the bootstrap
4941
4955
 
4942
4956
  ```ts
4943
4957
  import { setupTest } from "@warlock.js/core/tests";
@@ -4952,10 +4966,10 @@ What it does (in order):
4952
4966
  3. Runs `bootstrap()` — env, app, prestart hooks.
4953
4967
  4. Initializes the `filesOrchestrator` (module/route/config discovery, no file watching).
4954
4968
  5. Loads all `src/config/*.ts` files.
4955
- 6. Reads `tests.connectors` from config (overrides the parameter if set).
4969
+ 6. Resolves the connector selection — **an explicit parameter wins, then `tests.connectors` from config, then the `true` default.** See *Selecting connectors* below.
4956
4970
  7. Starts the chosen connectors — but **never `http`** when you pass a boolean. HTTP is the global-setup's job.
4957
4971
 
4958
- The result: each worker has its own DB/cache/logger/storage connections. Models save, repositories query, services run. Same code as production, just isolated to the test process.
4972
+ The result: DB/cache/logger/storage connections your code can use. Models save, repositories query, services run. Same code as production, just isolated to the test process.
4959
4973
 
4960
4974
  ### The `connectors` parameter
4961
4975
 
@@ -4965,9 +4979,9 @@ The result: each worker has its own DB/cache/logger/storage connections. Models
4965
4979
  | `false` | None | Pure logic tests with no DB / cache touches (parsers, validators, util functions). |
4966
4980
  | `["database", "cache"]` | Just those, in that order | A test that only needs DB but not, say, the storage driver. |
4967
4981
 
4968
- The default `true` is the sane choice. Reach for `false` when the unit you're testing genuinely doesn't talk to any framework subsystem — pulling up a DB connection per worker just to test a string parser is wasted setup time.
4982
+ The default `true` is the sane choice. Reach for `false` when the unit you're testing genuinely doesn't talk to any framework subsystem — pulling up a DB connection **for every file** just to test a string parser is wasted setup time.
4969
4983
 
4970
- ### Override via config — `src/config/tests.ts`
4984
+ ### Selecting connectors — `src/config/tests.ts`
4971
4985
 
4972
4986
  ```ts title="src/config/tests.ts"
4973
4987
  const testsConfigurations = {
@@ -4977,7 +4991,33 @@ const testsConfigurations = {
4977
4991
  export default testsConfigurations;
4978
4992
  ```
4979
4993
 
4980
- If `tests.connectors` is set, **it wins over the `setupTest({ connectors })` parameter**. Use this when every test file in the project agrees on the same minimal connector list saves repeating the explicit array in `test-setup.ts`.
4994
+ **BREAKING in 4.14.0 — the precedence flipped.**
4995
+
4996
+ | | Order |
4997
+ |---|---|
4998
+ | **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true` |
4999
+ | **4.14.0 onward** | **explicit `setupTest({ connectors })` parameter** **>** `tests.connectors` config **>** `true` |
5000
+
5001
+ **An explicit call-site value now beats project config.** If your project sets `tests.connectors` *and* some test file passes `connectors` explicitly, **that file will start a different connector set after upgrading.** Search for `setupTest({` across your tests before you upgrade — a call passing `connectors` was previously ignored and is now honoured.
5002
+
5003
+ **"Explicit" means you supplied a non-`undefined` value.** Both of these fall through to config:
5004
+
5005
+ ```ts
5006
+ await setupTest(); // → tests.connectors, else true
5007
+ await setupTest({}); // → tests.connectors, else true
5008
+ await setupTest({ connectors: undefined }); // → tests.connectors, else true — NOT "start none"
5009
+ ```
5010
+
5011
+ The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase your project config.
5012
+
5013
+ ```ts
5014
+ await setupTest({ connectors: false }); // → none, even if config says otherwise
5015
+ await setupTest({ connectors: ["database"] }); // → exactly that, even if config differs
5016
+ ```
5017
+
5018
+ ⚠ **The generated `src/test-setup.ts` calls `setupTest()` with no argument, on purpose.** If you "helpfully" change it to `setupTest({ connectors: true })`, you have made it explicit and **erased the `tests.connectors` layer for the whole project.**
5019
+
5020
+ Use `tests.connectors` when every test file agrees on the same minimal list — it saves repeating the array, and individual files can still override it.
4981
5021
 
4982
5022
  ## Project wiring — `src/test-setup.ts` + `vite.config.ts`
4983
5023
 
@@ -4985,14 +5025,22 @@ The `warlock add test` feature creates both files. The standard wiring:
4985
5025
 
4986
5026
  ```ts title="src/test-setup.ts"
4987
5027
  /**
4988
- * Per-Worker Test Setup
4989
- * Runs in EACH Vitest worker thread before tests execute.
5028
+ * Test Setup
5029
+ * Runs before EACH test file not once per worker.
4990
5030
  */
4991
- import { setupTest } from "@warlock.js/core/tests";
5031
+ import { afterAll } from "vitest";
5032
+ import { setupTest, teardownTest } from "@warlock.js/core/tests";
4992
5033
 
4993
- await setupTest({ connectors: true });
5034
+ await setupTest();
5035
+ afterAll(teardownTest);
4994
5036
  ```
4995
5037
 
5038
+ ⛔ **Three things changed here in 4.14.0. If you generated this file earlier, replace all three — it is not a comment fix.**
5039
+
5040
+ 1. **`afterAll(teardownTest)` is new and mandatory.** Nothing else closes the framework your tests started. This is what makes the lifetime file-scoped and owned rather than left running.
5041
+ 2. **The call is now bare `setupTest()`, not `setupTest({ connectors: true })`.** Under the flipped precedence, passing `true` is an *explicit* value and would override `tests.connectors` for **every file in the project.** Bare means "whatever this project configured, else the default".
5042
+ 3. **The comment used to say `Per-Worker Test Setup` / "Runs in EACH Vitest worker thread".** False — see the top of this skill.
5043
+
4996
5044
  ```ts title="vite.config.ts"
4997
5045
  import { lowerStage3Decorators } from "@warlock.js/core/vite";
4998
5046
  import mongezVite from "@mongez/vite";
@@ -5002,7 +5050,7 @@ export default defineConfig({
5002
5050
  plugins: [lowerStage3Decorators(), mongezVite()],
5003
5051
  test: {
5004
5052
  globalSetup: "./src/test-global-setup.ts", // ← HTTP server (see test-http skill)
5005
- setupFiles: ["./src/test-setup.ts"], // ← runs setupTest per worker
5053
+ setupFiles: ["./src/test-setup.ts"], // ← runs setupTest before EACH test file
5006
5054
  environment: "node",
5007
5055
  globals: false,
5008
5056
  include: ["src/app/**/*.test.ts"],
@@ -5107,7 +5155,9 @@ afterEach(async () => {
5107
5155
  });
5108
5156
  ```
5109
5157
 
5110
- Vitest runs tests in a single worker file sequentially, so an `afterEach` truncate gives each test a clean slate. For cross-file isolation, run the suite with `vitest --pool=forks --maxWorkers=N` and rely on the per-worker connection — each file's data stays within its worker until the run ends.
5158
+ Vitest runs the tests within one file sequentially, so an `afterEach` truncate gives each test a clean slate.
5159
+
5160
+ ⚠ **Cross-*file* isolation is not solved by this.** Separate workers get separate **connections**, not separate **rows** — two files pointed at the same database see each other's committed data regardless of pool or worker count. **Truncate what your file wrote; don't assume the worker boundary did it for you.** Real data isolation (DB-per-worker, transaction-per-test) is a separate piece of work and is not in this release.
5111
5161
 
5112
5162
  ### Skipping connectors for pure logic tests
5113
5163
 
@@ -5127,14 +5177,109 @@ describe("slugify", () => {
5127
5177
  });
5128
5178
  ```
5129
5179
 
5130
- `setupTest` is idempotent per worker (`isSetupComplete` flag) — calling it again with different options after `src/test-setup.ts` already ran is a no-op. **That includes a `connectors: false` call: if `src/test-setup.ts` already ran `setupTest()` in this worker, the example above changes nothing.** To genuinely skip connectors, either set `tests.connectors: false` in config (project-wide) or rely on the default in `src/test-setup.ts` being what you want most of the time.
5180
+ **BREAKING in 4.14.0 the example above now REJECTS if your project has a `src/test-setup.ts`.**
5181
+
5182
+ Through 4.13.0 a second `setupTest` call with different options was a **silent no-op** — you asked for no connectors, got all of them, and nothing told you. In 4.14.0 a conflicting call **rejects with an error naming both the active and the requested selection**, because silently ignoring what you asked for is worse than failing.
5183
+
5184
+ **If `src/test-setup.ts` already ran `setupTest()` in this file, use one of these instead:**
5185
+
5186
+ ```ts
5187
+ // 1. Tear down first, then set up differently — and PUT IT BACK when the file ends.
5188
+ import { afterAll, beforeAll } from "vitest";
5189
+ import { setupTest, teardownTest } from "@warlock.js/core/tests";
5190
+
5191
+ beforeAll(async () => {
5192
+ await teardownTest();
5193
+ await setupTest({ connectors: false });
5194
+ });
5195
+
5196
+ afterAll(async () => {
5197
+ await teardownTest(); // ← REQUIRED, see below
5198
+ });
5199
+ ```
5200
+
5201
+ ⛔ **The `afterAll` is not optional** — without it you leave a `connectors: false` runtime ready when the file ends.
5131
5202
 
5132
- ⚠ **Config beats the parameter.** If `tests.connectors` is set at all, `setupTest({ connectors })` cannot override it the config value wins. That is the current contract, not an accident; a per-call override is under discussion for a later release.
5203
+ ⚠ **This pattern interacts with the `afterAll(teardownTest)` in your setup file, and the relative ordering of the two has not been verified.** `teardownTest` is idempotent, so whichever runs second finds an idle lifecycle and no-ops but **do not build anything on a particular order until someone has measured it.** This is the strongest argument for option 2 below.
5204
+
5205
+ ```ts
5206
+ // 2. Or don't call setupTest at all — a pure-logic test needs nothing from it.
5207
+ // The connectors your setup file started are already running; you simply don't use them.
5208
+ ```
5209
+
5210
+ **3. Or set `tests.connectors: false` in `src/config/tests.ts`** if no test file in the project needs connectors.
5211
+
5212
+ **Option 2 is usually right, and option 1 is easy to get wrong.** Tearing down and re-bootstrapping costs a full framework startup — twice, once for your file and once for the next — to avoid a DB connection you were never going to use. **Reach for option 1 only when a connector's mere presence breaks the thing you're testing**, not to save setup time.
5213
+
5214
+ ## Lifecycle and repeated calls
5215
+
5216
+ `setupTest` / `teardownTest` are a pair. **The harness that calls one owns calling the other in the same context.**
5217
+
5218
+ | Call | Behaviour |
5219
+ |---|---|
5220
+ | `setupTest(x)` while idle | bootstraps |
5221
+ | `setupTest(x)` while already ready with the **same** effective options | no-op |
5222
+ | `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested |
5223
+ | two concurrent `setupTest(x)` calls | share one startup |
5224
+ | `setupTest` after a failed setup | allowed — a failed setup unwinds and returns to idle |
5225
+ | `teardownTest()` while idle | no-op |
5226
+ | two concurrent `teardownTest()` calls | share one shutdown |
5227
+ | `setupTest(y)` after a **successful** teardown | allowed, different options fine |
5228
+
5229
+ **"Same options" is compared by meaning, not by literal value** — connector arrays are deduplicated and compared as sets, so `["cache", "database"]` and `["database", "cache", "cache"]` are the same selection.
5230
+
5231
+ ⚠ **A failed shutdown poisons the lifecycle.** If `teardownTest()` rejects because the shutdown layer reported a failure, later `setupTest` calls **refuse until the Vitest worker is recycled** or a retried teardown fully succeeds. This is deliberate: a cleared flag does not prove that ports, sockets, pools or timers actually closed, and pretending otherwise hands you a "clean" run built on a leaked runtime.
5232
+
5233
+ ⚠ **What it cannot detect:** `connectorsManager.shutdown()` catches and logs individual connector failures internally. Those never reach this lifecycle, so they never poison it. It surfaces what that layer reports — no more.
5234
+
5235
+ ### `tests.setupTimeout` — the setup attempt is bounded
5236
+
5237
+ **New in 4.14.0.** A setup attempt that never settles used to leave the lifecycle stuck in `starting` and take the worker down with an out-of-memory crash. It is now bounded.
5238
+
5239
+ ```ts title="src/config/tests.ts"
5240
+ const testsConfigurations = {
5241
+ connectors: ["database", "logger"],
5242
+ setupTimeout: 120000, // milliseconds — this is the default
5243
+ };
5244
+
5245
+ export default testsConfigurations;
5246
+ ```
5247
+
5248
+ **Default: `120000` (two minutes)** — far above a healthy cold start, below the point where you'd stop watching the terminal. When it expires:
5249
+
5250
+ ```
5251
+ setupTest() did not finish within 120000ms and is stuck in the "starting" state. The
5252
+ lifecycle is now poisoned: whatever that attempt had already started is not known to be
5253
+ closed, so later setupTest() calls refuse until the Vitest worker is recycled. If your
5254
+ cold start is legitimately slower than this, raise the bound with `tests.setupTimeout`
5255
+ in `src/config/tests.ts` — milliseconds, default 120000.
5256
+ ```
5257
+
5258
+ 1. **It bounds the setup ATTEMPT, not teardown separately.** `teardownTest()` awaits the same attempt, so it inherits the bound — **one timer, not two.** A second teardown-side deadline was tried and rejected: it re-introduced the unbounded re-entry this whole guard exists to remove.
5259
+ 2. **Expiry poisons the lifecycle**, it does not return to `idle`. The attempt may have started connectors nobody can now account for, so pretending the runtime is clean would be worse than refusing.
5260
+ 3. **⛔ An invalid `setupTimeout` throws, naming the bad value.** Zero, negative and non-numeric all fail loudly rather than falling back to the default — a silent fallback would hide a typo behind a working suite.
5261
+ 4. **The bound is measured from when the attempt started**, not from when config was read. `tests.setupTimeout` is only readable after `loadConfigFiles()`, which happens *inside* the window being bounded; re-arming naively would give you the default plus your configured value.
5262
+
5263
+ ⚠ **What is proven and what is not.** The nine guards above were each seen to fail under their own mutation. **But every spec injects its scheduler**, so the default *value* is tested while the production timer — and whether its `unref` actually releases the worker — is not. **And no spec observes a real hang**: the stuck attempt is a mock gate, not a socket that never returns. These prove what the lifecycle *decides*, not what a genuinely wedged connector does.
5264
+
5265
+ ### State is per worker runtime, not per module
5266
+
5267
+ The lifecycle state lives in the worker runtime, not in a module variable. That matters because **Vitest rebuilds the module registry between test files while the worker itself keeps running** — so a module-level flag resets exactly where live DB connections and pools survive. Scope:
5268
+
5269
+ - **`pool: "forks"`** — state is per worker **process**.
5270
+ - **`pool: "threads"`** — state is per worker **thread**. It does **not** cross threads; `globalThis` is per realm, not per process.
5271
+
5272
+ In both cases the guard's scope matches the resource's scope, which is the point.
5273
+
5274
+ ⚠ **Not guaranteed:** under `threads` with `isolate: true`, Vitest tears the thread down while the process lives on. **Whether Node reclaims that thread's sockets and pools is unmeasured**, and nothing in this lifecycle can observe it.
5133
5275
 
5134
5276
  ## Gotchas
5135
5277
 
5136
- - **`setupTest` is idempotent per worker.** Second + later calls early-return. You can't "swap" the connector set mid-run the first call wins, including the one in `src/test-setup.ts`. Choose your worker default carefully.
5137
- - **Per-worker connections are separate from the HTTP server's connections.** A row inserted by a service-level test is on the worker's connection; the HTTP test server has its own. They don't see each other unless they're both pointing at the same physical DB and the inserting test has already committed.
5278
+ - **`setupTest` runs per test file, not per worker** every version of this skill through 4.13.0 said otherwise. **You pay one framework bootstrap per test file**, which is what 4.13.0 already cost; the difference is that it is now a chosen lifetime rather than a side effect of a module flag resetting.
5279
+ - **Never delete the `afterAll(teardownTest)` from your setup file.** Without it nothing closes what `setupTest` opened, and the connectors outlive the file that started them.
5280
+ - ⛔ **You can't swap the connector set by calling `setupTest` again — it rejects now.** Through 4.13.0 the second call was silently ignored. Tear down first, or don't call it.
5281
+ - **These connections are separate from the HTTP test server's.** A row inserted by a service-level test is on this connection; the HTTP test server has its own. They only see each other if both point at the same physical DB **and** the inserting test has committed.
5282
+ - **`setupTest` takes over the process's `connectorsManager` for its lifetime.** Teardown is manager-wide, so **mixing `setupTest` with manually started connectors is unsupported** — teardown may close yours too.
5138
5283
  - **`NODE_ENV` is set to `"test"`** by `setupTest`. Code that branches on `Application.isProduction` / `Application.isDevelopment` sees `false` for both. If your tests need production-like config (cookies, CORS), set those values in `src/config/*.ts` explicitly under the test branch — don't rely on the env flag.
5139
5284
  - **No HTTP from this layer.** `setupTest({ connectors: true })` never starts the HTTP connector by design. Don't try to `request.app.http` your way to a fetch test — use the `test-http` skill instead.
5140
5285
  - **Don't import `vitest-setup` from `@warlock.js/core/src/...`.** The public surface is `import { setupTest } from "@warlock.js/core/tests"`. Reaching into source paths breaks when the package layout shifts.
@@ -6308,7 +6453,18 @@ export default {
6308
6453
 
6309
6454
  `deny` wins over `allow`. If the IP can't be read (empty / unparseable), the request is rejected with 403. Reads via `request.detectIp()`.
6310
6455
 
6311
- ⚠ **Since 4.13.0 `http.trustProxy` defaults to `false`**, so `request.detectIp()` returns the socket address and **`X-Real-IP` / `X-Forwarded-For` are ignored unless you opt in.** Set `trustProxy: true` **only when you are genuinely behind a proxy that overwrites those headers** — before 4.13.0 the default was `true`, which meant any client could set its own forwarding header and be believed.
6456
+ ⚠ **Since 4.13.0 `http.trustProxy` defaults to `false`**, so `request.detectIp()` returns the socket address and **`X-Real-IP` / `X-Forwarded-For` are ignored unless you opt in.** Before 4.13.0 the default was `true`, which meant any client could set its own forwarding header and be believed.
6457
+
6458
+ ⚠ **Opt in with the narrowest shape your topology allows, not with `true`.** Since 4.15.0 `http.trustProxy` takes a hop count or a trusted-proxy list, and `detectIp()` resolves the chain the same way Fastify's `request.ip` does:
6459
+
6460
+ | `http.trustProxy` | Client IP |
6461
+ | --- | --- |
6462
+ | `false` *(default)* | Socket peer address |
6463
+ | `2` | Walks past the 2 rightmost `X-Forwarded-For` hops — for an edge that **appends** (the usual case: nginx, ALB, most CDNs) |
6464
+ | `"10.0.0.0/8"` / `["10.0.0.0/8", "192.168.0.0/16"]` | Walks left while each hop is a listed proxy |
6465
+ | `true` | Trusts the whole chain — the leftmost hop, i.e. **whatever the client put there** if your edge appends rather than overwrites |
6466
+
6467
+ With `true`, any client that can reach the process directly picks its own IP and this allowlist is decorative. `X-Real-IP` is honoured only under `true` — it carries no chain to check a hop count or proxy list against — so if your edge sets only that header, have it set `X-Forwarded-For` too.
6312
6468
 
6313
6469
  ```ts
6314
6470
  import { middleware } from "@warlock.js/core";
package/llms.txt CHANGED
@@ -27,7 +27,7 @@
27
27
  - [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.
28
28
  - [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`.
29
29
  - [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`.
30
- - [test-service](@warlock.js/core/test-service/SKILL.md): Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps each Vitest worker with its own DB/cache connections so you can call your code directly. Triggers: `setupTest`, `src/test-setup.ts`, `tests.connectors`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest } 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`.
30
+ - [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`.
31
31
  - [update-packages](@warlock.js/core/update-packages/SKILL.md): Keep a project current with `warlock update` — bump every `@warlock.js/*` dependency in package.json to its latest published version (range operator preserved), then run the lockfile-detected package manager install. Also covers the `warlock dev` update notice, its `u` update-and-restart keyboard shortcut, and the `devServer.checkForUpdates` toggle. Triggers: `warlock update`, `--no-install`, `--dry-run`, `--check`, `checkForUpdates`, `fetchLatestVersion`, `isNewerVersion`; "update warlock packages", "upgrade the framework", "is there a new warlock version", "update notice in the dev server", "press u to update", "dev server keyboard shortcut", "update check offline", "bump @warlock.js/* to latest"; typical CLI `warlock update`. Skip: dev/build/start runtime — `@warlock.js/core/run-app/SKILL.md`; writing a custom command — `@warlock.js/core/write-cli-command/SKILL.md`; installing a NEW feature package (auth, mail, storage) — that is `warlock add`; releasing/publishing the framework — workspace release tooling, not this command.
32
32
  - [upload-file](@warlock.js/core/upload-file/SKILL.md): Handle multipart file uploads — read via `request.file()` or `request.validated()`, validate with `v.file()`, save via `UploadedFile.save()` or the storage layer, transform images inline. Triggers: `UploadedFile`, `request.file`, `v.file`, `.save`, `.saveAs`, `.resize`, `.format`, `.quality`, `.image`, `.mimeType`, `.maxSize`; "accept a file upload", "validate file size and mime", "save to S3 or local disk", "resize an uploaded image on save"; typical import `import type { UploadedFile, RequestHandler } from "@warlock.js/core"`. Skip: storage drivers + presigned URLs — `@warlock.js/core/store-file/SKILL.md`; image-only transforms — `@warlock.js/core/process-image/SKILL.md`; schema rules — `@warlock.js/core/validate-input/SKILL.md`; competing libs `multer`, `formidable`, `busboy`.
33
33
  - [use-app-context](@warlock.js/core/use-app-context/SKILL.md): Read app-wide context — the `Application` static class (env, version, uptime, runtime strategy, boot lifecycle) plus the `app` runtime accessor (live Fastify, socket.io, router, database via the DI container). Triggers: `Application.isProduction`, `Application.environment`, `Application.runtimeStrategy`, `Application.uptime`, `Application.version`, `Application.onceBooted`, `Application.whenBooted`, `Application.isBooted`, `Application.onShutdown`, `Application.isShuttingDown`, `app.http`, `app.socket`, `app.database`, `app.router`; "branch on environment", "reach the live Fastify instance", "framework version in health endpoint", "dev vs production runtime check", "run code once the app is fully booted", "after all connectors started", "app booted hook", "run cleanup before shutdown", "graceful shutdown hook"; typical import `import { Application, app } from "@warlock.js/core"`. Skip: path helpers — `@warlock.js/core/resolve-path/SKILL.md`; connector start order — `@warlock.js/core/add-connector/SKILL.md`; competing patterns: bare `process.env.NODE_ENV`, ad-hoc Fastify imports.
package/package.json CHANGED
@@ -11,24 +11,24 @@
11
11
  "@fastify/multipart": "^9.3.0",
12
12
  "@fastify/rate-limit": "^10.3.0",
13
13
  "@fastify/static": "^8.3.0",
14
- "@mongez/concat-route": "^1.1.4",
15
- "@mongez/config": "^1.1.4",
14
+ "@mongez/concat-route": "^1.2.0",
15
+ "@mongez/config": "^1.2.1",
16
16
  "@mongez/copper": "^2.1.2",
17
- "@mongez/dotenv": "^1.3.1",
18
- "@mongez/events": "^2.2.6",
19
- "@mongez/http": "^3.3.8",
20
- "@mongez/localization": "^3.4.6",
21
- "@mongez/reinforcements": "^3.3.0",
17
+ "@mongez/dotenv": "^1.3.2",
18
+ "@mongez/events": "^2.2.7",
19
+ "@mongez/http": "^3.5.0",
20
+ "@mongez/localization": "^3.4.7",
21
+ "@mongez/reinforcements": "^4.0.1",
22
22
  "@mongez/slug": "^1.0.7",
23
- "@mongez/supportive-is": "^2.1.3",
23
+ "@mongez/supportive-is": "^2.1.4",
24
24
  "@mongez/time-wizard": "^1.0.6",
25
- "@warlock.js/auth": "4.15.0",
26
- "@warlock.js/cache": "4.15.0",
27
- "@warlock.js/cascade": "4.15.0",
28
- "@warlock.js/context": "4.15.0",
29
- "@warlock.js/logger": "4.15.0",
30
- "@warlock.js/seal": "4.15.0",
31
- "@warlock.js/fs": "4.15.0",
25
+ "@warlock.js/auth": "4.16.0",
26
+ "@warlock.js/cache": "4.16.0",
27
+ "@warlock.js/cascade": "4.16.0",
28
+ "@warlock.js/context": "4.16.0",
29
+ "@warlock.js/logger": "4.16.0",
30
+ "@warlock.js/seal": "4.16.0",
31
+ "@warlock.js/fs": "4.16.0",
32
32
  "chokidar": "^5.0.0",
33
33
  "dayjs": "^1.11.19",
34
34
  "es-module-lexer": "^2.0.0",
@@ -54,10 +54,10 @@
54
54
  "react": "^19.2.3",
55
55
  "react-dom": "^19.2.3",
56
56
  "@react-email/render": "^2.0.5",
57
- "@warlock.js/herald": "4.15.0",
58
- "@warlock.js/ai": "4.15.0",
59
- "@warlock.js/access": "4.15.0",
60
- "@warlock.js/notifications": "4.15.0"
57
+ "@warlock.js/herald": "4.16.0",
58
+ "@warlock.js/ai": "4.16.0",
59
+ "@warlock.js/access": "4.16.0",
60
+ "@warlock.js/notifications": "4.16.0"
61
61
  },
62
62
  "peerDependenciesMeta": {
63
63
  "sharp": {
@@ -120,7 +120,7 @@
120
120
  ],
121
121
  "author": "hassanzohdy",
122
122
  "license": "MIT",
123
- "version": "4.15.0",
123
+ "version": "4.16.0",
124
124
  "type": "module",
125
125
  "main": "./esm/index.mjs",
126
126
  "module": "./esm/index.mjs",
@@ -115,7 +115,18 @@ export default {
115
115
 
116
116
  `deny` wins over `allow`. If the IP can't be read (empty / unparseable), the request is rejected with 403. Reads via `request.detectIp()`.
117
117
 
118
- ⚠ **Since 4.13.0 `http.trustProxy` defaults to `false`**, so `request.detectIp()` returns the socket address and **`X-Real-IP` / `X-Forwarded-For` are ignored unless you opt in.** Set `trustProxy: true` **only when you are genuinely behind a proxy that overwrites those headers** — before 4.13.0 the default was `true`, which meant any client could set its own forwarding header and be believed.
118
+ ⚠ **Since 4.13.0 `http.trustProxy` defaults to `false`**, so `request.detectIp()` returns the socket address and **`X-Real-IP` / `X-Forwarded-For` are ignored unless you opt in.** Before 4.13.0 the default was `true`, which meant any client could set its own forwarding header and be believed.
119
+
120
+ ⚠ **Opt in with the narrowest shape your topology allows, not with `true`.** Since 4.15.0 `http.trustProxy` takes a hop count or a trusted-proxy list, and `detectIp()` resolves the chain the same way Fastify's `request.ip` does:
121
+
122
+ | `http.trustProxy` | Client IP |
123
+ | --- | --- |
124
+ | `false` *(default)* | Socket peer address |
125
+ | `2` | Walks past the 2 rightmost `X-Forwarded-For` hops — for an edge that **appends** (the usual case: nginx, ALB, most CDNs) |
126
+ | `"10.0.0.0/8"` / `["10.0.0.0/8", "192.168.0.0/16"]` | Walks left while each hop is a listed proxy |
127
+ | `true` | Trusts the whole chain — the leftmost hop, i.e. **whatever the client put there** if your edge appends rather than overwrites |
128
+
129
+ With `true`, any client that can reach the process directly picks its own IP and this allowlist is decorative. `X-Real-IP` is honoured only under `true` — it carries no chain to check a hop count or proxy list against — so if your edge sets only that header, have it set `X-Forwarded-For` too.
119
130
 
120
131
  ```ts
121
132
  import { middleware } from "@warlock.js/core";