@warlock.js/core 5.16.0 → 5.17.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 +22 -0
- package/esm/cli/commands/migrate.command.mjs +5 -1
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/seed.command.mjs +2 -1
- package/esm/cli/commands/seed.command.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs +16 -23
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +7 -1
- package/esm/config/locale-configuration.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +1 -18
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/read-bound-port.mjs +32 -0
- package/esm/connectors/read-bound-port.mjs.map +1 -0
- package/esm/dev-server/development-server.mjs +5 -1
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/extract-translation-keys.mjs +68 -0
- package/esm/dev-server/extract-translation-keys.mjs.map +1 -0
- package/esm/dev-server/translation-keys-sources.mjs +19 -0
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -0
- package/esm/dev-server/type-generator.mjs +64 -11
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/stubs.mjs +7 -6
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/csrf-default-guard.d.mts +32 -0
- package/esm/http/csrf-default-guard.d.mts.map +1 -0
- package/esm/http/csrf-default-guard.mjs +156 -0
- package/esm/http/csrf-default-guard.mjs.map +1 -0
- package/esm/http/csrf-origin-policy.d.mts +53 -0
- package/esm/http/csrf-origin-policy.d.mts.map +1 -0
- package/esm/http/csrf-origin-policy.mjs +104 -0
- package/esm/http/csrf-origin-policy.mjs.map +1 -0
- package/esm/http/error-codes.d.mts +14 -1
- package/esm/http/error-codes.d.mts.map +1 -1
- package/esm/http/error-codes.mjs +13 -0
- package/esm/http/error-codes.mjs.map +1 -1
- package/esm/http/index.d.mts +7 -1
- package/esm/http/index.mjs +7 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +6 -0
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
- package/esm/http/request.d.mts +2 -2
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +7 -3
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +7 -1
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/uploads/detect-image-format.mjs +59 -0
- package/esm/http/uploads/detect-image-format.mjs.map +1 -0
- package/esm/http/uploads/generate-image-variant.mjs +46 -0
- package/esm/http/uploads/generate-image-variant.mjs.map +1 -0
- package/esm/http/uploads/generate-image-variants.d.mts +49 -0
- package/esm/http/uploads/generate-image-variants.d.mts.map +1 -0
- package/esm/http/uploads/generate-image-variants.mjs +141 -0
- package/esm/http/uploads/generate-image-variants.mjs.map +1 -0
- package/esm/http/uploads/image-variant-types.d.mts +93 -0
- package/esm/http/uploads/image-variant-types.d.mts.map +1 -0
- package/esm/http/uploads/image-variants-config-error.d.mts +13 -0
- package/esm/http/uploads/image-variants-config-error.d.mts.map +1 -0
- package/esm/http/uploads/image-variants-config-error.mjs +17 -0
- package/esm/http/uploads/image-variants-config-error.mjs.map +1 -0
- package/esm/http/uploads/index.d.mts +4 -0
- package/esm/http/uploads/index.mjs +5 -0
- package/esm/http/uploads/is-path-inside.mjs +26 -0
- package/esm/http/uploads/is-path-inside.mjs.map +1 -0
- package/esm/http/uploads/load-variant-source.mjs +42 -0
- package/esm/http/uploads/load-variant-source.mjs.map +1 -0
- package/esm/http/uploads/matches-if-none-match.mjs +13 -0
- package/esm/http/uploads/matches-if-none-match.mjs.map +1 -0
- package/esm/http/uploads/parse-uploaded-file-query.mjs +51 -0
- package/esm/http/uploads/parse-uploaded-file-query.mjs.map +1 -0
- package/esm/http/uploads/read-file-head.mjs +20 -0
- package/esm/http/uploads/read-file-head.mjs.map +1 -0
- package/esm/http/uploads/resolve-image-variants-config.mjs +91 -0
- package/esm/http/uploads/resolve-image-variants-config.mjs.map +1 -0
- package/esm/http/uploads/resolve-inline-image-content-type.mjs +53 -0
- package/esm/http/uploads/resolve-inline-image-content-type.mjs.map +1 -0
- package/esm/http/uploads/resolve-original-content-type.mjs +31 -0
- package/esm/http/uploads/resolve-original-content-type.mjs.map +1 -0
- package/esm/http/uploads/resolve-upload-path.mjs +56 -0
- package/esm/http/uploads/resolve-upload-path.mjs.map +1 -0
- package/esm/http/uploads/resolve-variant-candidate.mjs +39 -0
- package/esm/http/uploads/resolve-variant-candidate.mjs.map +1 -0
- package/esm/http/uploads/single-flight.mjs +40 -0
- package/esm/http/uploads/single-flight.mjs.map +1 -0
- package/esm/http/uploads/uploaded-file.controller.d.mts +24 -0
- package/esm/http/uploads/uploaded-file.controller.d.mts.map +1 -0
- package/esm/http/uploads/uploaded-file.controller.mjs +174 -0
- package/esm/http/uploads/uploaded-file.controller.mjs.map +1 -0
- package/esm/http/uploads/variant-cache-key.mjs +45 -0
- package/esm/http/uploads/variant-cache-key.mjs.map +1 -0
- package/esm/http/uploads/variant-generations.mjs +16 -0
- package/esm/http/uploads/variant-generations.mjs.map +1 -0
- package/esm/http/uploads-types.d.mts +102 -1
- package/esm/http/uploads-types.d.mts.map +1 -1
- package/esm/index.d.mts +9 -2
- package/esm/index.mjs +8 -2
- package/esm/router/types.d.mts +23 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/index.d.mts +1 -0
- package/esm/storage/index.mjs +1 -0
- package/esm/storage/scoped-storage.d.mts +1 -0
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +3 -0
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts +2 -0
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +4 -0
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/utils/storage-not-initialized-error.d.mts +22 -0
- package/esm/storage/utils/storage-not-initialized-error.d.mts.map +1 -0
- package/esm/storage/utils/storage-not-initialized-error.mjs +24 -0
- package/esm/storage/utils/storage-not-initialized-error.mjs.map +1 -0
- package/llms-full.txt +17 -1
- package/package.json +22 -21
- package/skills/use-localization/SKILL.md +16 -0
- package/skills/use-middleware/SKILL.md +1 -1
- package/esm/dev-server/translation-type-generator.mjs +0 -28
- package/esm/dev-server/translation-type-generator.mjs.map +0 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { LOCALE_COOKIE_NAME, LOCALE_PREFERENCE_COOKIE_NAME } from "../config/locale-configuration.mjs";
|
|
2
|
+
import { HttpErrorCodes } from "./error-codes.mjs";
|
|
3
|
+
import { resolveCsrfOriginVerdict } from "./csrf-origin-policy.mjs";
|
|
4
|
+
|
|
5
|
+
//#region ../core/src/http/csrf-default-guard.ts
|
|
6
|
+
/**
|
|
7
|
+
* The DEFAULT core CSRF-Origin guard — SECURITY, card 8a752ab2 (5.17), lead
|
|
8
|
+
* ruling from Aria (`releases/v5.17-web-readiness-audit.md` §3.1, quoted):
|
|
9
|
+
*
|
|
10
|
+
* > "Scope the default guard to unsafe browser requests carrying a
|
|
11
|
+
* > non-locale credential-looking cookie (fail closed on unknown names), at
|
|
12
|
+
* > an early core/auth HTTP seam before app handlers; give explicit,
|
|
13
|
+
* > narrowly scoped route-level exemptions for third-party callbacks and
|
|
14
|
+
* > machine-to-machine routes, documented as dangerous. Reuse current
|
|
15
|
+
* > same-origin/allowedOrigins policy, trustProxy-aware own origin, and fail
|
|
16
|
+
* > closed on missing Origin/Referer where in scope."
|
|
17
|
+
*
|
|
18
|
+
* Before this guard, the CSRF Origin check (`csrf-origin-policy.ts`, moved
|
|
19
|
+
* here from `@warlock.js/auth` in this same change) only ran inside
|
|
20
|
+
* `authMiddleware("cookie:*")` — a cookie-authenticated write reaching any
|
|
21
|
+
* OTHER path (an app-owned optional-auth pattern reading its own `token`
|
|
22
|
+
* cookie without ever calling `authMiddleware`) was never checked at all.
|
|
23
|
+
*
|
|
24
|
+
* This guard closes that gap at the earliest seam common to EVERY request —
|
|
25
|
+
* production (`Router.scan()`) and the dev wildcard dispatcher both funnel
|
|
26
|
+
* into `Router["handleRoute"]` → `request.execute()` →
|
|
27
|
+
* `createRequestStore()` (`./middleware/inject-request-context.ts`), which is
|
|
28
|
+
* where this is wired, BEFORE `request.runMiddleware()` runs the route's own
|
|
29
|
+
* middleware (so it applies with or without `authMiddleware` on the route)
|
|
30
|
+
* and before any app handler. It lives in `@warlock.js/core`, not
|
|
31
|
+
* `@warlock.js/auth`, because `core` — not `auth` — is a dependency of every
|
|
32
|
+
* Warlock app, so this is the only seam that reaches an app that never
|
|
33
|
+
* installs `@warlock.js/auth` at all.
|
|
34
|
+
*
|
|
35
|
+
* Scope (mirrors `web`'s page-cache Cookie-header bypass rule,
|
|
36
|
+
* `web/src/server/page-cache-cookie-bypass.ts`, card ad861076 — same
|
|
37
|
+
* fail-closed strict-parse posture, independently reimplemented here because
|
|
38
|
+
* `core` cannot depend on `web`, the dependency runs the other way):
|
|
39
|
+
* - method is POST/PUT/PATCH/DELETE, AND
|
|
40
|
+
* - the `Cookie` header carries any cookie other than the framework's own
|
|
41
|
+
* `locale` cookies (legacy plus the browser preference; a header that fails to parse cleanly counts as
|
|
42
|
+
* carrying one — fail closed), AND
|
|
43
|
+
* - the route is not exempted via `{ csrf: false }` (`RouteOptions.csrf`,
|
|
44
|
+
* `../router/types.ts`) — a narrow, per-route, explicitly "dangerous" opt
|
|
45
|
+
* out for third-party callbacks and machine-to-machine routes.
|
|
46
|
+
*
|
|
47
|
+
* A header-only API request (`Authorization: Bearer …`, no `Cookie` header
|
|
48
|
+
* at all) never reaches the cookie check above and is entirely unaffected.
|
|
49
|
+
*/
|
|
50
|
+
/** HTTP methods the default CSRF guard is scoped to — every unsafe method. */
|
|
51
|
+
const UNSAFE_METHODS = new Set([
|
|
52
|
+
"POST",
|
|
53
|
+
"PUT",
|
|
54
|
+
"PATCH",
|
|
55
|
+
"DELETE"
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* Strictly parse a raw `Cookie` request header into a set of cookie names, or
|
|
59
|
+
* `undefined` when the header does not parse cleanly.
|
|
60
|
+
*
|
|
61
|
+
* Deliberately NOT a lenient cookie parser that silently drops malformed
|
|
62
|
+
* pairs and returns whatever it could salvage: a header this function cannot
|
|
63
|
+
* fully account for must read as "unknown cookies present", never as "the
|
|
64
|
+
* pairs we understood, minus the ones we didn't" — dropping a pair is how a
|
|
65
|
+
* malformed header could otherwise be misread as carrying only the locale
|
|
66
|
+
* cookie (or none) and wrongly skip the guard. Mirrors
|
|
67
|
+
* `web/src/server/page-cache-cookie-bypass.ts`'s `parseCookieHeaderStrict`.
|
|
68
|
+
*
|
|
69
|
+
* Fails (`undefined`) on:
|
|
70
|
+
* - an empty or whitespace-only header;
|
|
71
|
+
* - any empty segment between/around `;`;
|
|
72
|
+
* - any segment with no `=`, or an empty name.
|
|
73
|
+
*/
|
|
74
|
+
function parseCookieNamesStrict(rawHeader) {
|
|
75
|
+
if (rawHeader.trim() === "") return void 0;
|
|
76
|
+
const names = /* @__PURE__ */ new Set();
|
|
77
|
+
for (const segment of rawHeader.split(";")) {
|
|
78
|
+
const pair = segment.trim();
|
|
79
|
+
if (pair === "") return void 0;
|
|
80
|
+
const separatorIndex = pair.indexOf("=");
|
|
81
|
+
if (separatorIndex <= 0) return void 0;
|
|
82
|
+
const name = pair.slice(0, separatorIndex).trim();
|
|
83
|
+
if (name === "") return void 0;
|
|
84
|
+
names.add(name);
|
|
85
|
+
}
|
|
86
|
+
return names;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether the request carries a `Cookie` header naming anything other than
|
|
90
|
+
* the framework's locale cookies.
|
|
91
|
+
*
|
|
92
|
+
* - No `Cookie` header at all ⇒ `false` — nothing to guard on.
|
|
93
|
+
* - A header that fails to parse cleanly ⇒ `true` — fails CLOSED.
|
|
94
|
+
* - A header whose parsed names are a non-empty set equal to exactly
|
|
95
|
+
* `{locale}` ⇒ `false` — the one exemption.
|
|
96
|
+
* - Anything else (any other cookie name, alone or alongside `locale`) ⇒ `true`.
|
|
97
|
+
*/
|
|
98
|
+
function carriesNonLocaleCookie(request) {
|
|
99
|
+
const rawCookieHeader = request.header("cookie", void 0);
|
|
100
|
+
if (rawCookieHeader === void 0 || rawCookieHeader === null) return false;
|
|
101
|
+
if (typeof rawCookieHeader !== "string") return true;
|
|
102
|
+
const names = parseCookieNamesStrict(rawCookieHeader);
|
|
103
|
+
if (names === void 0 || names.size === 0) return true;
|
|
104
|
+
for (const name of names) if (name !== "locale" && name !== "warlock.locale-preference") return true;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Whether the matched route opted out of the default CSRF guard via
|
|
109
|
+
* `{ csrf: false }` (`RouteOptions.csrf`). Any other value — including
|
|
110
|
+
* `undefined` — leaves the route in scope.
|
|
111
|
+
*/
|
|
112
|
+
function isCsrfExempt(request) {
|
|
113
|
+
return request.route?.csrf === false;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Whether the current request is in scope for the default core CSRF-Origin
|
|
117
|
+
* guard at all.
|
|
118
|
+
*/
|
|
119
|
+
function requiresDefaultCsrfGuard(request) {
|
|
120
|
+
if (!UNSAFE_METHODS.has(request.method.toUpperCase())) return false;
|
|
121
|
+
if (isCsrfExempt(request)) return false;
|
|
122
|
+
return carriesNonLocaleCookie(request);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run the default core CSRF-Origin guard for the current request.
|
|
126
|
+
*
|
|
127
|
+
* Returns `undefined` when the request is out of scope or passes the
|
|
128
|
+
* Origin/Referer check — the caller continues into route middleware and the
|
|
129
|
+
* handler as normal. Returns the 403 {@link Response} when the guard refuses
|
|
130
|
+
* the request; the caller must return that value immediately, the same
|
|
131
|
+
* short-circuit contract every other middleware in the chain follows.
|
|
132
|
+
*
|
|
133
|
+
* Uses the SAME translated message and client-visible error code
|
|
134
|
+
* `authMiddleware("cookie:*")`'s own CSRF-Origin check uses
|
|
135
|
+
* (`auth.errors.csrfOriginMismatch`, `HttpErrorCodes.CsrfOriginMismatch` /
|
|
136
|
+
* `AuthErrorCodes.CsrfOriginMismatch`, both `"EC006"`) — one message, one
|
|
137
|
+
* error code, regardless of which seam caught the request. Logs exactly once
|
|
138
|
+
* per rejection here; a route that also carries `authMiddleware("cookie:*")`
|
|
139
|
+
* never reaches that middleware's own check for a request THIS guard already
|
|
140
|
+
* rejected, because a guard rejection short-circuits `runMiddleware()`
|
|
141
|
+
* entirely — so a rejected request is never logged twice.
|
|
142
|
+
*/
|
|
143
|
+
async function runDefaultCsrfGuard(request, response, translate, logRejection) {
|
|
144
|
+
if (!requiresDefaultCsrfGuard(request)) return void 0;
|
|
145
|
+
const verdict = resolveCsrfOriginVerdict(request);
|
|
146
|
+
if (verdict.allowed) return void 0;
|
|
147
|
+
logRejection(verdict.reason);
|
|
148
|
+
return response.forbidden({
|
|
149
|
+
error: translate("auth.errors.csrfOriginMismatch"),
|
|
150
|
+
errorCode: "EC006"
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
export { requiresDefaultCsrfGuard, runDefaultCsrfGuard };
|
|
156
|
+
//# sourceMappingURL=csrf-default-guard.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf-default-guard.mjs","names":[],"sources":["../../../../../../../core/src/http/csrf-default-guard.ts"],"sourcesContent":["/**\n * The DEFAULT core CSRF-Origin guard — SECURITY, card 8a752ab2 (5.17), lead\n * ruling from Aria (`releases/v5.17-web-readiness-audit.md` §3.1, quoted):\n *\n * > \"Scope the default guard to unsafe browser requests carrying a\n * > non-locale credential-looking cookie (fail closed on unknown names), at\n * > an early core/auth HTTP seam before app handlers; give explicit,\n * > narrowly scoped route-level exemptions for third-party callbacks and\n * > machine-to-machine routes, documented as dangerous. Reuse current\n * > same-origin/allowedOrigins policy, trustProxy-aware own origin, and fail\n * > closed on missing Origin/Referer where in scope.\"\n *\n * Before this guard, the CSRF Origin check (`csrf-origin-policy.ts`, moved\n * here from `@warlock.js/auth` in this same change) only ran inside\n * `authMiddleware(\"cookie:*\")` — a cookie-authenticated write reaching any\n * OTHER path (an app-owned optional-auth pattern reading its own `token`\n * cookie without ever calling `authMiddleware`) was never checked at all.\n *\n * This guard closes that gap at the earliest seam common to EVERY request —\n * production (`Router.scan()`) and the dev wildcard dispatcher both funnel\n * into `Router[\"handleRoute\"]` → `request.execute()` →\n * `createRequestStore()` (`./middleware/inject-request-context.ts`), which is\n * where this is wired, BEFORE `request.runMiddleware()` runs the route's own\n * middleware (so it applies with or without `authMiddleware` on the route)\n * and before any app handler. It lives in `@warlock.js/core`, not\n * `@warlock.js/auth`, because `core` — not `auth` — is a dependency of every\n * Warlock app, so this is the only seam that reaches an app that never\n * installs `@warlock.js/auth` at all.\n *\n * Scope (mirrors `web`'s page-cache Cookie-header bypass rule,\n * `web/src/server/page-cache-cookie-bypass.ts`, card ad861076 — same\n * fail-closed strict-parse posture, independently reimplemented here because\n * `core` cannot depend on `web`, the dependency runs the other way):\n * - method is POST/PUT/PATCH/DELETE, AND\n * - the `Cookie` header carries any cookie other than the framework's own\n * `locale` cookies (legacy plus the browser preference; a header that fails to parse cleanly counts as\n * carrying one — fail closed), AND\n * - the route is not exempted via `{ csrf: false }` (`RouteOptions.csrf`,\n * `../router/types.ts`) — a narrow, per-route, explicitly \"dangerous\" opt\n * out for third-party callbacks and machine-to-machine routes.\n *\n * A header-only API request (`Authorization: Bearer …`, no `Cookie` header\n * at all) never reaches the cookie check above and is entirely unaffected.\n */\nimport { LOCALE_COOKIE_NAME, LOCALE_PREFERENCE_COOKIE_NAME } from \"../config/locale-configuration\";\nimport { HttpErrorCodes } from \"./error-codes\";\nimport { resolveCsrfOriginVerdict } from \"./csrf-origin-policy\";\nimport type { Request } from \"./request\";\nimport type { Response } from \"./response\";\n\n/** HTTP methods the default CSRF guard is scoped to — every unsafe method. */\nconst UNSAFE_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * Strictly parse a raw `Cookie` request header into a set of cookie names, or\n * `undefined` when the header does not parse cleanly.\n *\n * Deliberately NOT a lenient cookie parser that silently drops malformed\n * pairs and returns whatever it could salvage: a header this function cannot\n * fully account for must read as \"unknown cookies present\", never as \"the\n * pairs we understood, minus the ones we didn't\" — dropping a pair is how a\n * malformed header could otherwise be misread as carrying only the locale\n * cookie (or none) and wrongly skip the guard. Mirrors\n * `web/src/server/page-cache-cookie-bypass.ts`'s `parseCookieHeaderStrict`.\n *\n * Fails (`undefined`) on:\n * - an empty or whitespace-only header;\n * - any empty segment between/around `;`;\n * - any segment with no `=`, or an empty name.\n */\nfunction parseCookieNamesStrict(rawHeader: string): Set<string> | undefined {\n if (rawHeader.trim() === \"\") return undefined;\n\n const names = new Set<string>();\n\n for (const segment of rawHeader.split(\";\")) {\n const pair = segment.trim();\n\n if (pair === \"\") return undefined;\n\n const separatorIndex = pair.indexOf(\"=\");\n\n if (separatorIndex <= 0) return undefined;\n\n const name = pair.slice(0, separatorIndex).trim();\n\n if (name === \"\") return undefined;\n\n names.add(name);\n }\n\n return names;\n}\n\n/**\n * Whether the request carries a `Cookie` header naming anything other than\n * the framework's locale cookies.\n *\n * - No `Cookie` header at all ⇒ `false` — nothing to guard on.\n * - A header that fails to parse cleanly ⇒ `true` — fails CLOSED.\n * - A header whose parsed names are a non-empty set equal to exactly\n * `{locale}` ⇒ `false` — the one exemption.\n * - Anything else (any other cookie name, alone or alongside `locale`) ⇒ `true`.\n */\nfunction carriesNonLocaleCookie(request: Request): boolean {\n const rawCookieHeader = request.header(\"cookie\", undefined);\n\n // Only an ABSENT header is cookie-free. A present header in any other\n // shape (an array from a duplicated Cookie header) fails closed.\n if (rawCookieHeader === undefined || rawCookieHeader === null) return false;\n\n if (typeof rawCookieHeader !== \"string\") return true;\n\n const names = parseCookieNamesStrict(rawCookieHeader);\n\n if (names === undefined || names.size === 0) return true;\n\n for (const name of names) {\n if (name !== LOCALE_COOKIE_NAME && name !== LOCALE_PREFERENCE_COOKIE_NAME) return true;\n }\n\n return false;\n}\n\n/**\n * Whether the matched route opted out of the default CSRF guard via\n * `{ csrf: false }` (`RouteOptions.csrf`). Any other value — including\n * `undefined` — leaves the route in scope.\n */\nfunction isCsrfExempt(request: Request): boolean {\n return request.route?.csrf === false;\n}\n\n/**\n * Whether the current request is in scope for the default core CSRF-Origin\n * guard at all.\n */\nexport function requiresDefaultCsrfGuard(request: Request): boolean {\n if (!UNSAFE_METHODS.has(request.method.toUpperCase())) return false;\n\n if (isCsrfExempt(request)) return false;\n\n return carriesNonLocaleCookie(request);\n}\n\n/**\n * Run the default core CSRF-Origin guard for the current request.\n *\n * Returns `undefined` when the request is out of scope or passes the\n * Origin/Referer check — the caller continues into route middleware and the\n * handler as normal. Returns the 403 {@link Response} when the guard refuses\n * the request; the caller must return that value immediately, the same\n * short-circuit contract every other middleware in the chain follows.\n *\n * Uses the SAME translated message and client-visible error code\n * `authMiddleware(\"cookie:*\")`'s own CSRF-Origin check uses\n * (`auth.errors.csrfOriginMismatch`, `HttpErrorCodes.CsrfOriginMismatch` /\n * `AuthErrorCodes.CsrfOriginMismatch`, both `\"EC006\"`) — one message, one\n * error code, regardless of which seam caught the request. Logs exactly once\n * per rejection here; a route that also carries `authMiddleware(\"cookie:*\")`\n * never reaches that middleware's own check for a request THIS guard already\n * rejected, because a guard rejection short-circuits `runMiddleware()`\n * entirely — so a rejected request is never logged twice.\n */\nexport async function runDefaultCsrfGuard(\n request: Request,\n response: Response,\n translate: (key: string) => string,\n logRejection: (reason: string) => void,\n): Promise<Response | undefined> {\n if (!requiresDefaultCsrfGuard(request)) return undefined;\n\n const verdict = resolveCsrfOriginVerdict(request);\n\n if (verdict.allowed) return undefined;\n\n logRejection(verdict.reason);\n\n return response.forbidden({\n error: translate(\"auth.errors.csrfOriginMismatch\"),\n errorCode: HttpErrorCodes.CsrfOriginMismatch,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAM,iBAAiB,IAAI,IAAI;CAAC;CAAQ;CAAO;CAAS;AAAQ,CAAC;;;;;;;;;;;;;;;;;;AAmBjE,SAAS,uBAAuB,WAA4C;CAC1E,IAAI,UAAU,KAAK,MAAM,IAAI,OAAO;CAEpC,MAAM,wBAAQ,IAAI,IAAY;CAE9B,KAAK,MAAM,WAAW,UAAU,MAAM,GAAG,GAAG;EAC1C,MAAM,OAAO,QAAQ,KAAK;EAE1B,IAAI,SAAS,IAAI,OAAO;EAExB,MAAM,iBAAiB,KAAK,QAAQ,GAAG;EAEvC,IAAI,kBAAkB,GAAG,OAAO;EAEhC,MAAM,OAAO,KAAK,MAAM,GAAG,cAAc,CAAC,CAAC,KAAK;EAEhD,IAAI,SAAS,IAAI,OAAO;EAExB,MAAM,IAAI,IAAI;CAChB;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,SAAS,uBAAuB,SAA2B;CACzD,MAAM,kBAAkB,QAAQ,OAAO,UAAU,MAAS;CAI1D,IAAI,oBAAoB,UAAa,oBAAoB,MAAM,OAAO;CAEtE,IAAI,OAAO,oBAAoB,UAAU,OAAO;CAEhD,MAAM,QAAQ,uBAAuB,eAAe;CAEpD,IAAI,UAAU,UAAa,MAAM,SAAS,GAAG,OAAO;CAEpD,KAAK,MAAM,QAAQ,OACjB,IAAI,qBAA+B,sCAAwC,OAAO;CAGpF,OAAO;AACT;;;;;;AAOA,SAAS,aAAa,SAA2B;CAC/C,OAAO,QAAQ,OAAO,SAAS;AACjC;;;;;AAMA,SAAgB,yBAAyB,SAA2B;CAClE,IAAI,CAAC,eAAe,IAAI,QAAQ,OAAO,YAAY,CAAC,GAAG,OAAO;CAE9D,IAAI,aAAa,OAAO,GAAG,OAAO;CAElC,OAAO,uBAAuB,OAAO;AACvC;;;;;;;;;;;;;;;;;;;;AAqBA,eAAsB,oBACpB,SACA,UACA,WACA,cAC+B;CAC/B,IAAI,CAAC,yBAAyB,OAAO,GAAG,OAAO;CAE/C,MAAM,UAAU,yBAAyB,OAAO;CAEhD,IAAI,QAAQ,SAAS,OAAO;CAE5B,aAAa,QAAQ,MAAM;CAE3B,OAAO,SAAS,UAAU;EACxB,OAAO,UAAU,gCAAgC;EACjD;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Request } from "./request.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/http/csrf-origin-policy.d.ts
|
|
4
|
+
/** Why {@link resolveCsrfOriginVerdict} refused a request. */
|
|
5
|
+
type CsrfOriginCheckReason = "origin-mismatch" | "referer-mismatch" | "missing-origin-and-referer";
|
|
6
|
+
/** The verdict {@link resolveCsrfOriginVerdict} reaches for one request. */
|
|
7
|
+
type CsrfOriginVerdict = {
|
|
8
|
+
allowed: true;
|
|
9
|
+
} | {
|
|
10
|
+
allowed: false;
|
|
11
|
+
reason: CsrfOriginCheckReason;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The request's own origin — what an `Origin`/`Referer` header must match.
|
|
15
|
+
*
|
|
16
|
+
* `request.hostname` (core's `Request`, backed by Fastify's `hostname`) never
|
|
17
|
+
* carries a port. A browser's `Origin` header on a non-default port (e.g. any
|
|
18
|
+
* `warlock dev` session) does, so the port must come from the raw `Host`
|
|
19
|
+
* header instead — core exposes no getter for that, so it is read directly
|
|
20
|
+
* here.
|
|
21
|
+
*
|
|
22
|
+
* Trust-proxy-aware for free: `request.protocol` is Fastify's own
|
|
23
|
+
* `request.protocol`, which already honours `http.trustProxy`
|
|
24
|
+
* (`X-Forwarded-Proto`) when the app has configured it (`server.ts`) — this
|
|
25
|
+
* function does not need its own proxy-trust logic on top of that.
|
|
26
|
+
*/
|
|
27
|
+
declare function ownOrigin(request: Request): string;
|
|
28
|
+
/** Extract `scheme://host` from a full URL (e.g. a `Referer` header value). */
|
|
29
|
+
declare function originOf(rawUrl: string): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Normalize an origin string (`scheme://host[:port]`) so that a default port
|
|
32
|
+
* (`:80` on `http:`, `:443` on `https:`) compares equal to the same origin
|
|
33
|
+
* written without a port. Falls back to the raw value if it does not parse
|
|
34
|
+
* as a URL (in which case it will simply fail the exact-match comparison).
|
|
35
|
+
*/
|
|
36
|
+
declare function normalizeOrigin(origin: string): string;
|
|
37
|
+
/** Same-origin, or an explicit entry in `auth.csrf.allowedOrigins` (default `[]`). */
|
|
38
|
+
declare function isAllowedCsrfOrigin(origin: string, request: Request): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The shared CSRF Origin/Referer verdict (lead decision, card 8a752ab2 §3.1):
|
|
41
|
+
* allowed when `Origin` — or, when `Origin` is absent, `Referer` — names the
|
|
42
|
+
* request's own origin or an entry in `auth.csrf.allowedOrigins`. Refused
|
|
43
|
+
* otherwise, including when BOTH headers are absent (fail closed).
|
|
44
|
+
*
|
|
45
|
+
* Callers decide WHETHER a request is in scope for this check at all (a
|
|
46
|
+
* cookie-sourced credential / a non-locale cookie, an unsafe method, no route
|
|
47
|
+
* exemption) — this function only answers the Origin/Referer question once a
|
|
48
|
+
* caller has already decided the check applies.
|
|
49
|
+
*/
|
|
50
|
+
declare function resolveCsrfOriginVerdict(request: Request): CsrfOriginVerdict;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { CsrfOriginCheckReason, CsrfOriginVerdict, isAllowedCsrfOrigin, normalizeOrigin, originOf, ownOrigin, resolveCsrfOriginVerdict };
|
|
53
|
+
//# sourceMappingURL=csrf-origin-policy.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf-origin-policy.d.mts","names":[],"sources":["../../../../../../../core/src/http/csrf-origin-policy.ts"],"mappings":";;;;KAyBY,qBAAA;AAAZ;AAAA,KAMY,iBAAA;EACN,OAAA;AAAA;EACA,OAAA;EAAgB,MAAA,EAAQ,qBAAqB;AAAA;;;;;;;;AAAA;AAgBnD;;;;AAA0C;AAQ1C;iBARgB,SAAA,CAAU,OAAgB,EAAP,OAAO;;iBAQ1B,QAAA,CAAS,MAAc;AAAA;AAgBvC;;;;AAA8C;AAhBP,iBAgBvB,eAAA,CAAgB,MAAc;;iBAc9B,mBAAA,CAAoB,MAAA,UAAgB,OAAA,EAAS,OAAO;;;;;;AAAA;AAmBpE;;;;;iBAAgB,wBAAA,CAAyB,OAAA,EAAS,OAAA,GAAU,iBAAiB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import config from "@mongez/config";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/http/csrf-origin-policy.ts
|
|
4
|
+
/**
|
|
5
|
+
* The Origin/Referer same-origin (or `auth.csrf.allowedOrigins`) policy — the
|
|
6
|
+
* one comparison every CSRF-Origin check in the framework must agree on.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY, card 8a752ab2 (5.17). Extracted from
|
|
9
|
+
* `@warlock.js/auth`'s `csrf-origin-check.ts` (the ONLY prior owner of this
|
|
10
|
+
* logic) into `@warlock.js/core` so both call sites — the
|
|
11
|
+
* `authMiddleware("cookie:*")` check in `@warlock.js/auth` and the default
|
|
12
|
+
* core HTTP-seam guard in `./csrf-default-guard.ts` — share ONE
|
|
13
|
+
* implementation instead of drifting copies. `@warlock.js/core` cannot depend
|
|
14
|
+
* on `@warlock.js/auth` (the dependency runs the other way), so the shared
|
|
15
|
+
* logic had to move down into `core`, not up into a new shared package —
|
|
16
|
+
* `auth`'s `csrf-origin-check.ts` now delegates to
|
|
17
|
+
* {@link resolveCsrfOriginVerdict} instead of reimplementing it.
|
|
18
|
+
*
|
|
19
|
+
* Reads `auth.csrf.allowedOrigins` directly via `@mongez/config` (the exact
|
|
20
|
+
* same config key `@warlock.js/auth`'s `authConfig.csrf.allowedOrigins()`
|
|
21
|
+
* reads) rather than importing `@warlock.js/auth`, so an app's one
|
|
22
|
+
* `auth.csrf.allowedOrigins` setting governs both checks without `core`
|
|
23
|
+
* taking a hard dependency on `auth`.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The request's own origin — what an `Origin`/`Referer` header must match.
|
|
27
|
+
*
|
|
28
|
+
* `request.hostname` (core's `Request`, backed by Fastify's `hostname`) never
|
|
29
|
+
* carries a port. A browser's `Origin` header on a non-default port (e.g. any
|
|
30
|
+
* `warlock dev` session) does, so the port must come from the raw `Host`
|
|
31
|
+
* header instead — core exposes no getter for that, so it is read directly
|
|
32
|
+
* here.
|
|
33
|
+
*
|
|
34
|
+
* Trust-proxy-aware for free: `request.protocol` is Fastify's own
|
|
35
|
+
* `request.protocol`, which already honours `http.trustProxy`
|
|
36
|
+
* (`X-Forwarded-Proto`) when the app has configured it (`server.ts`) — this
|
|
37
|
+
* function does not need its own proxy-trust logic on top of that.
|
|
38
|
+
*/
|
|
39
|
+
function ownOrigin(request) {
|
|
40
|
+
const hostHeader = request.header("host");
|
|
41
|
+
const host = typeof hostHeader === "string" && hostHeader ? hostHeader : request.hostname;
|
|
42
|
+
return `${request.protocol}://${host}`;
|
|
43
|
+
}
|
|
44
|
+
/** Extract `scheme://host` from a full URL (e.g. a `Referer` header value). */
|
|
45
|
+
function originOf(rawUrl) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(rawUrl);
|
|
48
|
+
return `${url.protocol}//${url.host}`;
|
|
49
|
+
} catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Normalize an origin string (`scheme://host[:port]`) so that a default port
|
|
55
|
+
* (`:80` on `http:`, `:443` on `https:`) compares equal to the same origin
|
|
56
|
+
* written without a port. Falls back to the raw value if it does not parse
|
|
57
|
+
* as a URL (in which case it will simply fail the exact-match comparison).
|
|
58
|
+
*/
|
|
59
|
+
function normalizeOrigin(origin) {
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(origin);
|
|
62
|
+
const isDefaultPort = url.protocol === "http:" && (url.port === "" || url.port === "80") || url.protocol === "https:" && (url.port === "" || url.port === "443");
|
|
63
|
+
return `${url.protocol}//${isDefaultPort ? url.hostname : url.host}`;
|
|
64
|
+
} catch {
|
|
65
|
+
return origin;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Same-origin, or an explicit entry in `auth.csrf.allowedOrigins` (default `[]`). */
|
|
69
|
+
function isAllowedCsrfOrigin(origin, request) {
|
|
70
|
+
if (normalizeOrigin(origin) === normalizeOrigin(ownOrigin(request))) return true;
|
|
71
|
+
return config.get("auth.csrf.allowedOrigins", []).includes(origin);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The shared CSRF Origin/Referer verdict (lead decision, card 8a752ab2 §3.1):
|
|
75
|
+
* allowed when `Origin` — or, when `Origin` is absent, `Referer` — names the
|
|
76
|
+
* request's own origin or an entry in `auth.csrf.allowedOrigins`. Refused
|
|
77
|
+
* otherwise, including when BOTH headers are absent (fail closed).
|
|
78
|
+
*
|
|
79
|
+
* Callers decide WHETHER a request is in scope for this check at all (a
|
|
80
|
+
* cookie-sourced credential / a non-locale cookie, an unsafe method, no route
|
|
81
|
+
* exemption) — this function only answers the Origin/Referer question once a
|
|
82
|
+
* caller has already decided the check applies.
|
|
83
|
+
*/
|
|
84
|
+
function resolveCsrfOriginVerdict(request) {
|
|
85
|
+
const origin = request.origin;
|
|
86
|
+
if (origin) return isAllowedCsrfOrigin(origin, request) ? { allowed: true } : {
|
|
87
|
+
allowed: false,
|
|
88
|
+
reason: "origin-mismatch"
|
|
89
|
+
};
|
|
90
|
+
const referer = request.header("referer");
|
|
91
|
+
const refererOrigin = typeof referer === "string" ? originOf(referer) : void 0;
|
|
92
|
+
if (refererOrigin) return isAllowedCsrfOrigin(refererOrigin, request) ? { allowed: true } : {
|
|
93
|
+
allowed: false,
|
|
94
|
+
reason: "referer-mismatch"
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
allowed: false,
|
|
98
|
+
reason: "missing-origin-and-referer"
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
export { isAllowedCsrfOrigin, normalizeOrigin, originOf, ownOrigin, resolveCsrfOriginVerdict };
|
|
104
|
+
//# sourceMappingURL=csrf-origin-policy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf-origin-policy.mjs","names":[],"sources":["../../../../../../../core/src/http/csrf-origin-policy.ts"],"sourcesContent":["/**\n * The Origin/Referer same-origin (or `auth.csrf.allowedOrigins`) policy — the\n * one comparison every CSRF-Origin check in the framework must agree on.\n *\n * SECURITY, card 8a752ab2 (5.17). Extracted from\n * `@warlock.js/auth`'s `csrf-origin-check.ts` (the ONLY prior owner of this\n * logic) into `@warlock.js/core` so both call sites — the\n * `authMiddleware(\"cookie:*\")` check in `@warlock.js/auth` and the default\n * core HTTP-seam guard in `./csrf-default-guard.ts` — share ONE\n * implementation instead of drifting copies. `@warlock.js/core` cannot depend\n * on `@warlock.js/auth` (the dependency runs the other way), so the shared\n * logic had to move down into `core`, not up into a new shared package —\n * `auth`'s `csrf-origin-check.ts` now delegates to\n * {@link resolveCsrfOriginVerdict} instead of reimplementing it.\n *\n * Reads `auth.csrf.allowedOrigins` directly via `@mongez/config` (the exact\n * same config key `@warlock.js/auth`'s `authConfig.csrf.allowedOrigins()`\n * reads) rather than importing `@warlock.js/auth`, so an app's one\n * `auth.csrf.allowedOrigins` setting governs both checks without `core`\n * taking a hard dependency on `auth`.\n */\nimport config from \"@mongez/config\";\nimport type { Request } from \"./request\";\n\n/** Why {@link resolveCsrfOriginVerdict} refused a request. */\nexport type CsrfOriginCheckReason =\n | \"origin-mismatch\"\n | \"referer-mismatch\"\n | \"missing-origin-and-referer\";\n\n/** The verdict {@link resolveCsrfOriginVerdict} reaches for one request. */\nexport type CsrfOriginVerdict =\n | { allowed: true }\n | { allowed: false; reason: CsrfOriginCheckReason };\n\n/**\n * The request's own origin — what an `Origin`/`Referer` header must match.\n *\n * `request.hostname` (core's `Request`, backed by Fastify's `hostname`) never\n * carries a port. A browser's `Origin` header on a non-default port (e.g. any\n * `warlock dev` session) does, so the port must come from the raw `Host`\n * header instead — core exposes no getter for that, so it is read directly\n * here.\n *\n * Trust-proxy-aware for free: `request.protocol` is Fastify's own\n * `request.protocol`, which already honours `http.trustProxy`\n * (`X-Forwarded-Proto`) when the app has configured it (`server.ts`) — this\n * function does not need its own proxy-trust logic on top of that.\n */\nexport function ownOrigin(request: Request): string {\n const hostHeader = request.header(\"host\");\n const host = typeof hostHeader === \"string\" && hostHeader ? hostHeader : request.hostname;\n\n return `${request.protocol}://${host}`;\n}\n\n/** Extract `scheme://host` from a full URL (e.g. a `Referer` header value). */\nexport function originOf(rawUrl: string): string | undefined {\n try {\n const url = new URL(rawUrl);\n\n return `${url.protocol}//${url.host}`;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Normalize an origin string (`scheme://host[:port]`) so that a default port\n * (`:80` on `http:`, `:443` on `https:`) compares equal to the same origin\n * written without a port. Falls back to the raw value if it does not parse\n * as a URL (in which case it will simply fail the exact-match comparison).\n */\nexport function normalizeOrigin(origin: string): string {\n try {\n const url = new URL(origin);\n const isDefaultPort =\n (url.protocol === \"http:\" && (url.port === \"\" || url.port === \"80\")) ||\n (url.protocol === \"https:\" && (url.port === \"\" || url.port === \"443\"));\n\n return `${url.protocol}//${isDefaultPort ? url.hostname : url.host}`;\n } catch {\n return origin;\n }\n}\n\n/** Same-origin, or an explicit entry in `auth.csrf.allowedOrigins` (default `[]`). */\nexport function isAllowedCsrfOrigin(origin: string, request: Request): boolean {\n if (normalizeOrigin(origin) === normalizeOrigin(ownOrigin(request))) return true;\n\n const allowedOrigins: string[] = config.get(\"auth.csrf.allowedOrigins\", []);\n\n return allowedOrigins.includes(origin);\n}\n\n/**\n * The shared CSRF Origin/Referer verdict (lead decision, card 8a752ab2 §3.1):\n * allowed when `Origin` — or, when `Origin` is absent, `Referer` — names the\n * request's own origin or an entry in `auth.csrf.allowedOrigins`. Refused\n * otherwise, including when BOTH headers are absent (fail closed).\n *\n * Callers decide WHETHER a request is in scope for this check at all (a\n * cookie-sourced credential / a non-locale cookie, an unsafe method, no route\n * exemption) — this function only answers the Origin/Referer question once a\n * caller has already decided the check applies.\n */\nexport function resolveCsrfOriginVerdict(request: Request): CsrfOriginVerdict {\n const origin = request.origin;\n\n if (origin) {\n return isAllowedCsrfOrigin(origin, request)\n ? { allowed: true }\n : { allowed: false, reason: \"origin-mismatch\" };\n }\n\n const referer = request.header(\"referer\");\n const refererOrigin = typeof referer === \"string\" ? originOf(referer) : undefined;\n\n if (refererOrigin) {\n return isAllowedCsrfOrigin(refererOrigin, request)\n ? { allowed: true }\n : { allowed: false, reason: \"referer-mismatch\" };\n }\n\n return { allowed: false, reason: \"missing-origin-and-referer\" };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,UAAU,SAA0B;CAClD,MAAM,aAAa,QAAQ,OAAO,MAAM;CACxC,MAAM,OAAO,OAAO,eAAe,YAAY,aAAa,aAAa,QAAQ;CAEjF,OAAO,GAAG,QAAQ,SAAS,KAAK;AAClC;;AAGA,SAAgB,SAAS,QAAoC;CAC3D,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,MAAM;EAE1B,OAAO,GAAG,IAAI,SAAS,IAAI,IAAI;CACjC,QAAQ;EACN;CACF;AACF;;;;;;;AAQA,SAAgB,gBAAgB,QAAwB;CACtD,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,MAAM;EAC1B,MAAM,gBACH,IAAI,aAAa,YAAY,IAAI,SAAS,MAAM,IAAI,SAAS,SAC7D,IAAI,aAAa,aAAa,IAAI,SAAS,MAAM,IAAI,SAAS;EAEjE,OAAO,GAAG,IAAI,SAAS,IAAI,gBAAgB,IAAI,WAAW,IAAI;CAChE,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,oBAAoB,QAAgB,SAA2B;CAC7E,IAAI,gBAAgB,MAAM,MAAM,gBAAgB,UAAU,OAAO,CAAC,GAAG,OAAO;CAI5E,OAFiC,OAAO,IAAI,4BAA4B,CAAC,CAErD,CAAC,CAAC,SAAS,MAAM;AACvC;;;;;;;;;;;;AAaA,SAAgB,yBAAyB,SAAqC;CAC5E,MAAM,SAAS,QAAQ;CAEvB,IAAI,QACF,OAAO,oBAAoB,QAAQ,OAAO,IACtC,EAAE,SAAS,KAAK,IAChB;EAAE,SAAS;EAAO,QAAQ;CAAkB;CAGlD,MAAM,UAAU,QAAQ,OAAO,SAAS;CACxC,MAAM,gBAAgB,OAAO,YAAY,WAAW,SAAS,OAAO,IAAI;CAExE,IAAI,eACF,OAAO,oBAAoB,eAAe,OAAO,IAC7C,EAAE,SAAS,KAAK,IAChB;EAAE,SAAS;EAAO,QAAQ;CAAmB;CAGnD,OAAO;EAAE,SAAS;EAAO,QAAQ;CAA6B;AAChE"}
|
|
@@ -37,7 +37,20 @@ declare enum HttpErrorCodes {
|
|
|
37
37
|
/**
|
|
38
38
|
* Application is in maintenance mode and the request did not match the allowlist.
|
|
39
39
|
*/
|
|
40
|
-
Maintenance = "EC106"
|
|
40
|
+
Maintenance = "EC106",
|
|
41
|
+
/**
|
|
42
|
+
* Default core CSRF-Origin guard (SECURITY, card 8a752ab2, `./csrf-default-guard.ts`)
|
|
43
|
+
* refused an unsafe-method, cookie-carrying request whose `Origin`/`Referer`
|
|
44
|
+
* did not name the request's own origin or an entry in
|
|
45
|
+
* `auth.csrf.allowedOrigins`.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately reuses `@warlock.js/auth`'s `AuthErrorCodes.CsrfOriginMismatch`
|
|
48
|
+
* value (`EC006`), NOT a fresh `EC1xx` core code: this is the exact same
|
|
49
|
+
* failure class `authMiddleware("cookie:*")`'s own CSRF-Origin check
|
|
50
|
+
* reports — one client-visible identifier for "CSRF Origin check failed",
|
|
51
|
+
* regardless of which seam caught it.
|
|
52
|
+
*/
|
|
53
|
+
CsrfOriginMismatch = "EC006"
|
|
41
54
|
}
|
|
42
55
|
//#endregion
|
|
43
56
|
export { HttpErrorCodes };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-codes.d.mts","names":[],"sources":["../../../../../../../core/src/http/error-codes.ts"],"mappings":";;AASA;;;;;;;;aAAY,cAAA;EA8BV
|
|
1
|
+
{"version":3,"file":"error-codes.d.mts","names":[],"sources":["../../../../../../../core/src/http/error-codes.ts"],"mappings":";;AASA;;;;;;;;aAAY,cAAA;EA8BV;;;;EAzBA,sBAAA;;;;EAKA,qBAAA;;;;EAKA,iBAAA;;;;EAKA,uBAAA;;;;EAKA,YAAA;;;;EAKA,WAAA;;;;EAKA,WAAA;;;;;;;;;;;;;EAcA,kBAAA;AAAA"}
|
package/esm/http/error-codes.mjs
CHANGED
|
@@ -38,6 +38,19 @@ let HttpErrorCodes = /* @__PURE__ */ function(HttpErrorCodes) {
|
|
|
38
38
|
* Application is in maintenance mode and the request did not match the allowlist.
|
|
39
39
|
*/
|
|
40
40
|
HttpErrorCodes["Maintenance"] = "EC106";
|
|
41
|
+
/**
|
|
42
|
+
* Default core CSRF-Origin guard (SECURITY, card 8a752ab2, `./csrf-default-guard.ts`)
|
|
43
|
+
* refused an unsafe-method, cookie-carrying request whose `Origin`/`Referer`
|
|
44
|
+
* did not name the request's own origin or an entry in
|
|
45
|
+
* `auth.csrf.allowedOrigins`.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately reuses `@warlock.js/auth`'s `AuthErrorCodes.CsrfOriginMismatch`
|
|
48
|
+
* value (`EC006`), NOT a fresh `EC1xx` core code: this is the exact same
|
|
49
|
+
* failure class `authMiddleware("cookie:*")`'s own CSRF-Origin check
|
|
50
|
+
* reports — one client-visible identifier for "CSRF Origin check failed",
|
|
51
|
+
* regardless of which seam caught it.
|
|
52
|
+
*/
|
|
53
|
+
HttpErrorCodes["CsrfOriginMismatch"] = "EC006";
|
|
41
54
|
return HttpErrorCodes;
|
|
42
55
|
}({});
|
|
43
56
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-codes.mjs","names":[],"sources":["../../../../../../../core/src/http/error-codes.ts"],"sourcesContent":["/**\n * HTTP middleware error codes.\n *\n * Mirrors the convention used by `@warlock.js/auth`'s `AuthErrorCodes` —\n * stable string identifiers clients can branch on without parsing message text.\n *\n * Range `EC100..EC199` is reserved for `@warlock.js/core` HTTP middleware.\n * `EC001..EC099` belongs to `@warlock.js/auth`.\n */\nexport enum HttpErrorCodes {\n /**\n * Same idempotency key reused with a different request body.\n * Client likely has a bug — same intent must reuse the same body.\n */\n IdempotencyKeyConflict = \"EC100\",\n\n /**\n * Idempotency key header is malformed (wrong length / non-printable chars).\n */\n IdempotencyKeyInvalid = \"EC101\",\n\n /**\n * Per-route rate limit exceeded. Different from the global `@fastify/rate-limit` 429.\n */\n RateLimitExceeded = \"EC102\",\n\n /**\n * Per-route concurrency cap reached — too many in-flight requests against this endpoint.\n */\n ConcurrencyLimitReached = \"EC103\",\n\n /**\n * Request `Content-Length` exceeds the per-route body cap.\n */\n BodyTooLarge = \"EC104\",\n\n /**\n * Client IP failed the `ipFilter()` allow/deny check.\n */\n IpForbidden = \"EC105\",\n\n /**\n * Application is in maintenance mode and the request did not match the allowlist.\n */\n Maintenance = \"EC106\",\n}\n"],"mappings":";;;;;;;;;;AASA,IAAY,iBAAL;;;;;CAKL;;;;CAKA;;;;CAKA;;;;CAKA;;;;CAKA;;;;CAKA;;;;CAKA;;AACF"}
|
|
1
|
+
{"version":3,"file":"error-codes.mjs","names":[],"sources":["../../../../../../../core/src/http/error-codes.ts"],"sourcesContent":["/**\n * HTTP middleware error codes.\n *\n * Mirrors the convention used by `@warlock.js/auth`'s `AuthErrorCodes` —\n * stable string identifiers clients can branch on without parsing message text.\n *\n * Range `EC100..EC199` is reserved for `@warlock.js/core` HTTP middleware.\n * `EC001..EC099` belongs to `@warlock.js/auth`.\n */\nexport enum HttpErrorCodes {\n /**\n * Same idempotency key reused with a different request body.\n * Client likely has a bug — same intent must reuse the same body.\n */\n IdempotencyKeyConflict = \"EC100\",\n\n /**\n * Idempotency key header is malformed (wrong length / non-printable chars).\n */\n IdempotencyKeyInvalid = \"EC101\",\n\n /**\n * Per-route rate limit exceeded. Different from the global `@fastify/rate-limit` 429.\n */\n RateLimitExceeded = \"EC102\",\n\n /**\n * Per-route concurrency cap reached — too many in-flight requests against this endpoint.\n */\n ConcurrencyLimitReached = \"EC103\",\n\n /**\n * Request `Content-Length` exceeds the per-route body cap.\n */\n BodyTooLarge = \"EC104\",\n\n /**\n * Client IP failed the `ipFilter()` allow/deny check.\n */\n IpForbidden = \"EC105\",\n\n /**\n * Application is in maintenance mode and the request did not match the allowlist.\n */\n Maintenance = \"EC106\",\n\n /**\n * Default core CSRF-Origin guard (SECURITY, card 8a752ab2, `./csrf-default-guard.ts`)\n * refused an unsafe-method, cookie-carrying request whose `Origin`/`Referer`\n * did not name the request's own origin or an entry in\n * `auth.csrf.allowedOrigins`.\n *\n * Deliberately reuses `@warlock.js/auth`'s `AuthErrorCodes.CsrfOriginMismatch`\n * value (`EC006`), NOT a fresh `EC1xx` core code: this is the exact same\n * failure class `authMiddleware(\"cookie:*\")`'s own CSRF-Origin check\n * reports — one client-visible identifier for \"CSRF Origin check failed\",\n * regardless of which seam caught it.\n */\n CsrfOriginMismatch = \"EC006\",\n}\n"],"mappings":";;;;;;;;;;AASA,IAAY,iBAAL;;;;;CAKL;;;;CAKA;;;;CAKA;;;;CAKA;;;;CAKA;;;;CAKA;;;;CAKA;;;;;;;;;;;;;CAcA;;AACF"}
|
package/esm/http/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PipeableReactStream, StreamReactResponseOptions, streamReactResponse } from "./stream-react-response.mjs";
|
|
2
2
|
import { XMLable } from "./xmlable.mjs";
|
|
3
3
|
import { CookieOptions, Response, ResponseStatus, SendBufferOptions, SendFileOptions } from "./response.mjs";
|
|
4
|
-
import { FileNamingStrategy, ImageTransformCallback, ImageTransformConfig, PrefixConfig, PrefixOptions, SaveAsOptions, SaveOptions, UploadedFileImageOptions, UploadsConfigurations } from "./uploads-types.mjs";
|
|
4
|
+
import { FileNamingStrategy, ImageTransformCallback, ImageTransformConfig, ImageVariantDefinition, ImageVariantFit, ImageVariantOutputFormat, PrefixConfig, PrefixOptions, SaveAsOptions, SaveOptions, UploadedFileImageOptions, UploadsConfigurations, UploadsImagesConfigurations } from "./uploads-types.mjs";
|
|
5
5
|
import { FileValidationOptions, UploadedFile, UploadedFileJson } from "./uploaded-file.mjs";
|
|
6
6
|
import { Request } from "./request.mjs";
|
|
7
7
|
import { CspConfig, DEFAULT_CSP_DIRECTIVES, InvalidCspDirectiveError, applyCspHeader, buildCspHeaderValue, mergeCspDirectives, resolveCspConfig, serializeCspDirectives, validateCspConfigAtBoot, validateCspDirectives } from "./csp.mjs";
|
|
@@ -9,12 +9,18 @@ import { HttpTracingConfig, TracingContext, TracingHooks, TracingPhaseInfo, Trac
|
|
|
9
9
|
import { DecodedAccessToken, HttpConfigurations, PartialMiddleware, RequestEvent, RequestLocals, ResponseEvent, ResponseSSEController, ResponseStreamController, ReturnedResponse } from "./types.mjs";
|
|
10
10
|
import { defaultHttpConfigurations, httpConfig } from "./config.mjs";
|
|
11
11
|
import { createHttpApplication, stopHttpApplication } from "./createHttpApplication.mjs";
|
|
12
|
+
import { requiresDefaultCsrfGuard, runDefaultCsrfGuard } from "./csrf-default-guard.mjs";
|
|
13
|
+
import { CsrfOriginCheckReason, CsrfOriginVerdict, isAllowedCsrfOrigin, normalizeOrigin, originOf, ownOrigin, resolveCsrfOriginVerdict } from "./csrf-origin-policy.mjs";
|
|
12
14
|
import { RequestLog } from "./database/RequestLog.mjs";
|
|
13
15
|
import { HttpErrorCodes } from "./error-codes.mjs";
|
|
14
16
|
import { logResponse, wrapResponseInDataKey } from "./events.mjs";
|
|
15
17
|
import { HealthCheck, HealthStatus, health } from "./health.mjs";
|
|
16
18
|
import { RequestController } from "./request-controller.mjs";
|
|
17
19
|
import { UPLOADS_DEFAULTS, uploadsConfig } from "./uploads-config.mjs";
|
|
20
|
+
import { DetectedImageFormat, GeneratedImageDescriptor, GeneratedImageVariant, ResolvedImageVariantsConfig, UploadedFileQuery, VariantOutputFormat, VariantSourceFormat } from "./uploads/image-variant-types.mjs";
|
|
21
|
+
import { GenerateImageVariantsOptions, generateImageVariants } from "./uploads/generate-image-variants.mjs";
|
|
22
|
+
import { ImageVariantsConfigError } from "./uploads/image-variants-config-error.mjs";
|
|
23
|
+
import { uploadedFileController } from "./uploads/uploaded-file.controller.mjs";
|
|
18
24
|
import { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
|
|
19
25
|
import { CacheMiddlewareOptions } from "./middleware/cache-response-middleware.mjs";
|
|
20
26
|
import { ConcurrencyLimitOptions } from "./middleware/concurrency-limit.middleware.mjs";
|
package/esm/http/index.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { requestContext, useCurrentUser, useRequest, useRequestStore } from "./context/request-context.mjs";
|
|
2
2
|
import { DEFAULT_CSP_DIRECTIVES, InvalidCspDirectiveError, applyCspHeader, buildCspHeaderValue, mergeCspDirectives, resolveCspConfig, serializeCspDirectives, validateCspConfigAtBoot, validateCspDirectives } from "./csp.mjs";
|
|
3
|
+
import { HttpErrorCodes } from "./error-codes.mjs";
|
|
4
|
+
import { isAllowedCsrfOrigin, normalizeOrigin, originOf, ownOrigin, resolveCsrfOriginVerdict } from "./csrf-origin-policy.mjs";
|
|
5
|
+
import { requiresDefaultCsrfGuard, runDefaultCsrfGuard } from "./csrf-default-guard.mjs";
|
|
3
6
|
import { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
|
|
4
7
|
import { deriveTraceId, parseTraceparentTraceId } from "./tracing/trace-id.mjs";
|
|
5
8
|
import { buildTracingContext, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, isTracingEnabled, resetTracingConfigForTests, resolveTracingConfig } from "./tracing/tracing-dispatcher.mjs";
|
|
@@ -15,10 +18,13 @@ import { registerHttpPlugins } from "./plugins.mjs";
|
|
|
15
18
|
import { closeServerWithTimeout, getHttpServer, startHttpServer } from "./server.mjs";
|
|
16
19
|
import { createHttpApplication, stopHttpApplication } from "./createHttpApplication.mjs";
|
|
17
20
|
import { RequestLog } from "./database/RequestLog.mjs";
|
|
18
|
-
import { HttpErrorCodes } from "./error-codes.mjs";
|
|
19
21
|
import { logResponse, wrapResponseInDataKey } from "./events.mjs";
|
|
20
22
|
import { health } from "./health.mjs";
|
|
21
23
|
import { RequestController } from "./request-controller.mjs";
|
|
24
|
+
import { ImageVariantsConfigError } from "./uploads/image-variants-config-error.mjs";
|
|
25
|
+
import { generateImageVariants } from "./uploads/generate-image-variants.mjs";
|
|
26
|
+
import { uploadedFileController } from "./uploads/uploaded-file.controller.mjs";
|
|
27
|
+
import "./uploads/index.mjs";
|
|
22
28
|
import { buildIdempotencyCacheKey, hashBody, isValidIdempotencyKey } from "./middleware/utils/idempotency-key.mjs";
|
|
23
29
|
import { anyMatch, ipMatches } from "./middleware/utils/cidr-match.mjs";
|
|
24
30
|
import { parseSize } from "./middleware/utils/parse-size.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t } from "./inject-request-context.mjs";
|
|
2
1
|
import { HttpErrorCodes } from "../error-codes.mjs";
|
|
2
|
+
import { t } from "./inject-request-context.mjs";
|
|
3
3
|
|
|
4
4
|
//#region ../core/src/http/middleware/concurrency-limit.middleware.ts
|
|
5
5
|
const counters = /* @__PURE__ */ new Map();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t } from "./inject-request-context.mjs";
|
|
2
1
|
import { HttpErrorCodes } from "../error-codes.mjs";
|
|
2
|
+
import { t } from "./inject-request-context.mjs";
|
|
3
3
|
import { buildIdempotencyCacheKey, hashBody, isValidIdempotencyKey } from "./utils/idempotency-key.mjs";
|
|
4
4
|
import config from "@mongez/config";
|
|
5
5
|
import { log } from "@warlock.js/logger";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inject-request-context.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/inject-request-context.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"inject-request-context.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/inject-request-context.ts"],"mappings":";;;;;;;AA8CA;;;iBAAgB,kBAAA,CACd,OAAA,EAAS,OAAA,OACT,QAAA,EAAU,QAAA,GACT,OAAA,CAAQ,gBAAA;;;;iBAoIK,CAAA,CAAE,OAAA,UAAiB,YAAkB"}
|
|
@@ -2,6 +2,7 @@ import { environment } from "../../utils/environment.mjs";
|
|
|
2
2
|
import { requestContext } from "../context/request-context.mjs";
|
|
3
3
|
import "../../utils/index.mjs";
|
|
4
4
|
import { applyCspHeader } from "../csp.mjs";
|
|
5
|
+
import { runDefaultCsrfGuard } from "../csrf-default-guard.mjs";
|
|
5
6
|
import { HttpError } from "../errors/errors.mjs";
|
|
6
7
|
import { buildTracingContext, dispatchPhase, isTracingEnabled } from "../tracing/tracing-dispatcher.mjs";
|
|
7
8
|
import "../tracing/index.mjs";
|
|
@@ -9,6 +10,7 @@ import config from "@mongez/config";
|
|
|
9
10
|
import { trans } from "@mongez/localization";
|
|
10
11
|
import { DatabaseWriterValidationError } from "@warlock.js/cascade";
|
|
11
12
|
import { contextManager } from "@warlock.js/context";
|
|
13
|
+
import { log } from "@warlock.js/logger";
|
|
12
14
|
|
|
13
15
|
//#region ../core/src/http/middleware/inject-request-context.ts
|
|
14
16
|
/**
|
|
@@ -44,6 +46,10 @@ function createRequestStore(request, response) {
|
|
|
44
46
|
});
|
|
45
47
|
return contextManager.runAll(httpContextStore, async () => {
|
|
46
48
|
try {
|
|
49
|
+
const csrfGuardResponse = await runDefaultCsrfGuard(request, response, t, (reason) => {
|
|
50
|
+
log.error("http", "csrf-default-guard", /* @__PURE__ */ new Error(`Default CSRF Origin guard refused request: ${reason}`));
|
|
51
|
+
});
|
|
52
|
+
if (csrfGuardResponse) return csrfGuardResponse;
|
|
47
53
|
const result = await request.runMiddleware();
|
|
48
54
|
if (result) return result;
|
|
49
55
|
request.trigger("executingAction", request.route);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inject-request-context.mjs","names":["requestContextInstance"],"sources":["../../../../../../../../core/src/http/middleware/inject-request-context.ts"],"sourcesContent":["/**\n * Request Context Middleware\n *\n * Creates a unified context for each request using the ContextManager.\n * All framework contexts (request, storage, database) are available throughout the request lifecycle.\n */\nimport { trans } from \"@mongez/localization\";\nimport { type GenericObject } from \"@mongez/reinforcements\";\nimport { DatabaseWriterValidationError } from \"@warlock.js/cascade\";\nimport { contextManager } from \"@warlock.js/context\";\nimport config from \"@mongez/config\";\nimport { environment } from \"../../utils\";\nimport { requestContext as requestContextInstance } from \"../context/request-context\";\nimport { applyCspHeader } from \"../csp\";\nimport { HttpError } from \"../errors\";\nimport { type Request } from \"../request\";\nimport { type Response } from \"../response\";\nimport { buildTracingContext, dispatchPhase, isTracingEnabled } from \"../tracing\";\nimport { type ReturnedResponse } from \"./../types\";\n\n// Contexts are now registered in core/context/init-contexts.ts via initializeContexts()\n\n/**\n * Echo `request.id` back as a response header so the FE / proxies / log\n * aggregators can correlate by the same value the server logs against.\n *\n * Reads the header name from `http.requestId.header` (default `X-Request-Id`).\n * Skip when `http.requestId.enabled` is explicitly false.\n */\nfunction stampRequestIdHeader(request: Request, response: Response) {\n const requestIdConfig = config.get(\"http.requestId\", {} as Record<string, any>);\n\n if (requestIdConfig.enabled === false) return;\n\n const headerName = requestIdConfig.header || \"X-Request-Id\";\n\n response.header(headerName, request.id);\n}\n\n/**\n * Create request store and execute middleware + handler\n *\n * Runs all registered contexts together using ContextManager.\n */\nexport function createRequestStore(\n request: Request<any>,\n response: Response,\n): Promise<ReturnedResponse> {\n stampRequestIdHeader(request, response);\n\n // Stamped this early — before any middleware or handler runs — so the\n // header carries the SAME nonce the web layer later reads off\n // `request.nonce` for its `<script>` tags: `request.nonce` caches on first\n // read, and this is deliberately the first read. A no-op when the app\n // hasn't opted into `http.csp` (see `csp.ts`).\n applyCspHeader(request, response);\n\n // Build all context stores using the immutable API\n // Each context defines its own store initialization via buildStore()\n const httpContextStore = contextManager.buildStores({ request, response });\n\n // Run all contexts together!\n return contextManager.runAll(httpContextStore, async () => {\n try {\n // Run middleware chain\n const result = await request.runMiddleware();\n\n if (result) {\n return result as ReturnedResponse;\n }\n\n // Execute route handler\n request.trigger(\"executingAction\", request.route);\n\n const handler = request.getHandler();\n\n request.log(\"Executing Handler\", \"info\");\n\n const tracingEnabled = isTracingEnabled();\n const handlerStartedAt = tracingEnabled ? performance.now() : 0;\n\n const output = await handler({ request, response });\n\n if (tracingEnabled) {\n dispatchPhase(buildTracingContext(request), {\n name: \"handler\",\n durationMs: performance.now() - handlerStartedAt,\n });\n }\n\n request.log(\"Handler Executed Successfully\", \"success\");\n\n request.trigger(\"executedAction\", request.route);\n\n return output as ReturnedResponse;\n } catch (error) {\n request.log(error, \"error\");\n return handleRequestError(error, response);\n }\n });\n}\n\n/**\n * Handle request errors\n * @internal\n */\nfunction handleRequestError(error: unknown, response: Response): ReturnedResponse {\n // Availability floor, not a cache-policy nit: `handleRequestError` is the\n // single funnel every unhandled error in every Warlock app passes through\n // (`createRequestStore`'s catch above), and none of the branches below set\n // a `Cache-Control` header. Without this, an error response — a 500 as\n // much as a 401/403/404 carrying per-request/-user state — can be stored\n // by a shared cache or CDN and replayed to other requests/users long after\n // the condition that caused it is gone: a cached 500 becomes an outage\n // that outlives its cause; a cached 401/403 becomes a leak across users.\n // Set once, here, before any branch runs — not per branch — because the\n // branches below do not partition by status. `ResourceNotFoundError`,\n // `UnAuthorizedError`, `ForbiddenError`, `BadRequestError` and `ServerError`\n // all extend `HttpError`, so the `HttpError` branch answers for every one of\n // them, and a raw `HttpError` can carry any caller-chosen status, 4xx or 5xx.\n // Gating the floor on the eventual status would mean re-deriving that status\n // per branch — one rule meeting one form while others reach the same output.\n // Applying it once, unconditionally, is both simpler and safer.\n response.header(\"Cache-Control\", \"private, no-store\");\n\n if (error instanceof HttpError) {\n const payload: GenericObject = {\n error: error.message,\n };\n if (error.payload) {\n payload.payload = error.payload;\n }\n\n if (environment() === \"development\") {\n payload.stack = error.stack;\n }\n\n return response.setStatusCode(error.status).send(payload);\n }\n\n if (error instanceof DatabaseWriterValidationError) {\n return response.badRequest({\n errors: error.errors,\n });\n }\n\n // Last resort: the error matched none of the known shapes above, so the\n // client gets a deliberately opaque message. Without this line the error\n // itself is discarded here — no stack, no message, nothing in any log — and\n // an unrecognised failure becomes indistinguishable from a working server\n // returning 500. Never swallow the only copy of an error (`65e476ee`).\n console.error(\"[warlock] unhandled request error:\", error);\n\n return response.serverError({\n error: \"Internal server error.\",\n });\n}\n\n/**\n * Translate a keyword (uses request context for locale)\n */\nexport function t(keyword: string, placeholders?: any) {\n return (\n requestContextInstance.getRequest()?.trans(keyword, placeholders) ||\n trans(keyword, placeholders)\n );\n}\n\n// `fromRequest` was removed in v5. It cached computed values as dynamic\n// properties on the Request instance, which only compiled because of the\n// `[key: string]: any` index signature that v5 deletes (eed20184). Use\n// `requestMemo(key, fn)` from `../context/request-memo` instead — same\n// per-request lifetime, single-flight, and it never touches the Request object.\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"inject-request-context.mjs","names":["requestContextInstance"],"sources":["../../../../../../../../core/src/http/middleware/inject-request-context.ts"],"sourcesContent":["/**\n * Request Context Middleware\n *\n * Creates a unified context for each request using the ContextManager.\n * All framework contexts (request, storage, database) are available throughout the request lifecycle.\n */\nimport { trans } from \"@mongez/localization\";\nimport { type GenericObject } from \"@mongez/reinforcements\";\nimport { DatabaseWriterValidationError } from \"@warlock.js/cascade\";\nimport { contextManager } from \"@warlock.js/context\";\nimport { log } from \"@warlock.js/logger\";\nimport config from \"@mongez/config\";\nimport { environment } from \"../../utils\";\nimport { requestContext as requestContextInstance } from \"../context/request-context\";\nimport { applyCspHeader } from \"../csp\";\nimport { runDefaultCsrfGuard } from \"../csrf-default-guard\";\nimport { HttpError } from \"../errors\";\nimport { type Request } from \"../request\";\nimport { type Response } from \"../response\";\nimport { buildTracingContext, dispatchPhase, isTracingEnabled } from \"../tracing\";\nimport { type ReturnedResponse } from \"./../types\";\n\n// Contexts are now registered in core/context/init-contexts.ts via initializeContexts()\n\n/**\n * Echo `request.id` back as a response header so the FE / proxies / log\n * aggregators can correlate by the same value the server logs against.\n *\n * Reads the header name from `http.requestId.header` (default `X-Request-Id`).\n * Skip when `http.requestId.enabled` is explicitly false.\n */\nfunction stampRequestIdHeader(request: Request, response: Response) {\n const requestIdConfig = config.get(\"http.requestId\", {} as Record<string, any>);\n\n if (requestIdConfig.enabled === false) return;\n\n const headerName = requestIdConfig.header || \"X-Request-Id\";\n\n response.header(headerName, request.id);\n}\n\n/**\n * Create request store and execute middleware + handler\n *\n * Runs all registered contexts together using ContextManager.\n */\nexport function createRequestStore(\n request: Request<any>,\n response: Response,\n): Promise<ReturnedResponse> {\n stampRequestIdHeader(request, response);\n\n // Stamped this early — before any middleware or handler runs — so the\n // header carries the SAME nonce the web layer later reads off\n // `request.nonce` for its `<script>` tags: `request.nonce` caches on first\n // read, and this is deliberately the first read. A no-op when the app\n // hasn't opted into `http.csp` (see `csp.ts`).\n applyCspHeader(request, response);\n\n // Build all context stores using the immutable API\n // Each context defines its own store initialization via buildStore()\n const httpContextStore = contextManager.buildStores({ request, response });\n\n // Run all contexts together!\n return contextManager.runAll(httpContextStore, async () => {\n try {\n // Default CSRF-Origin guard (SECURITY, card 8a752ab2) — the earliest\n // seam common to every request, BEFORE the route's own middleware\n // (`request.runMiddleware()` below) and before any app handler, so it\n // applies even to a route that never attaches\n // `authMiddleware(\"cookie:*\")`. See `../csrf-default-guard.ts` for the\n // full scope rule and `{ csrf: false }` exemption.\n const csrfGuardResponse = await runDefaultCsrfGuard(request, response, t, (reason) => {\n log.error(\n \"http\",\n \"csrf-default-guard\",\n new Error(`Default CSRF Origin guard refused request: ${reason}`),\n );\n });\n\n if (csrfGuardResponse) {\n return csrfGuardResponse as ReturnedResponse;\n }\n\n // Run middleware chain\n const result = await request.runMiddleware();\n\n if (result) {\n return result as ReturnedResponse;\n }\n\n // Execute route handler\n request.trigger(\"executingAction\", request.route);\n\n const handler = request.getHandler();\n\n request.log(\"Executing Handler\", \"info\");\n\n const tracingEnabled = isTracingEnabled();\n const handlerStartedAt = tracingEnabled ? performance.now() : 0;\n\n const output = await handler({ request, response });\n\n if (tracingEnabled) {\n dispatchPhase(buildTracingContext(request), {\n name: \"handler\",\n durationMs: performance.now() - handlerStartedAt,\n });\n }\n\n request.log(\"Handler Executed Successfully\", \"success\");\n\n request.trigger(\"executedAction\", request.route);\n\n return output as ReturnedResponse;\n } catch (error) {\n request.log(error, \"error\");\n return handleRequestError(error, response);\n }\n });\n}\n\n/**\n * Handle request errors\n * @internal\n */\nfunction handleRequestError(error: unknown, response: Response): ReturnedResponse {\n // Availability floor, not a cache-policy nit: `handleRequestError` is the\n // single funnel every unhandled error in every Warlock app passes through\n // (`createRequestStore`'s catch above), and none of the branches below set\n // a `Cache-Control` header. Without this, an error response — a 500 as\n // much as a 401/403/404 carrying per-request/-user state — can be stored\n // by a shared cache or CDN and replayed to other requests/users long after\n // the condition that caused it is gone: a cached 500 becomes an outage\n // that outlives its cause; a cached 401/403 becomes a leak across users.\n // Set once, here, before any branch runs — not per branch — because the\n // branches below do not partition by status. `ResourceNotFoundError`,\n // `UnAuthorizedError`, `ForbiddenError`, `BadRequestError` and `ServerError`\n // all extend `HttpError`, so the `HttpError` branch answers for every one of\n // them, and a raw `HttpError` can carry any caller-chosen status, 4xx or 5xx.\n // Gating the floor on the eventual status would mean re-deriving that status\n // per branch — one rule meeting one form while others reach the same output.\n // Applying it once, unconditionally, is both simpler and safer.\n response.header(\"Cache-Control\", \"private, no-store\");\n\n if (error instanceof HttpError) {\n const payload: GenericObject = {\n error: error.message,\n };\n if (error.payload) {\n payload.payload = error.payload;\n }\n\n if (environment() === \"development\") {\n payload.stack = error.stack;\n }\n\n return response.setStatusCode(error.status).send(payload);\n }\n\n if (error instanceof DatabaseWriterValidationError) {\n return response.badRequest({\n errors: error.errors,\n });\n }\n\n // Last resort: the error matched none of the known shapes above, so the\n // client gets a deliberately opaque message. Without this line the error\n // itself is discarded here — no stack, no message, nothing in any log — and\n // an unrecognised failure becomes indistinguishable from a working server\n // returning 500. Never swallow the only copy of an error (`65e476ee`).\n console.error(\"[warlock] unhandled request error:\", error);\n\n return response.serverError({\n error: \"Internal server error.\",\n });\n}\n\n/**\n * Translate a keyword (uses request context for locale)\n */\nexport function t(keyword: string, placeholders?: any) {\n return (\n requestContextInstance.getRequest()?.trans(keyword, placeholders) ||\n trans(keyword, placeholders)\n );\n}\n\n// `fromRequest` was removed in v5. It cached computed values as dynamic\n// properties on the Request instance, which only compiled because of the\n// `[key: string]: any` index signature that v5 deletes (eed20184). Use\n// `requestMemo(key, fn)` from `../context/request-memo` instead — same\n// per-request lifetime, single-flight, and it never touches the Request object.\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAS,qBAAqB,SAAkB,UAAoB;CAClE,MAAM,kBAAkB,OAAO,IAAI,kBAAkB,CAAC,CAAwB;CAE9E,IAAI,gBAAgB,YAAY,OAAO;CAEvC,MAAM,aAAa,gBAAgB,UAAU;CAE7C,SAAS,OAAO,YAAY,QAAQ,EAAE;AACxC;;;;;;AAOA,SAAgB,mBACd,SACA,UAC2B;CAC3B,qBAAqB,SAAS,QAAQ;CAOtC,eAAe,SAAS,QAAQ;CAIhC,MAAM,mBAAmB,eAAe,YAAY;EAAE;EAAS;CAAS,CAAC;CAGzE,OAAO,eAAe,OAAO,kBAAkB,YAAY;EACzD,IAAI;GAOF,MAAM,oBAAoB,MAAM,oBAAoB,SAAS,UAAU,IAAI,WAAW;IACpF,IAAI,MACF,QACA,sCACA,IAAI,MAAM,8CAA8C,QAAQ,CAClE;GACF,CAAC;GAED,IAAI,mBACF,OAAO;GAIT,MAAM,SAAS,MAAM,QAAQ,cAAc;GAE3C,IAAI,QACF,OAAO;GAIT,QAAQ,QAAQ,mBAAmB,QAAQ,KAAK;GAEhD,MAAM,UAAU,QAAQ,WAAW;GAEnC,QAAQ,IAAI,qBAAqB,MAAM;GAEvC,MAAM,iBAAiB,iBAAiB;GACxC,MAAM,mBAAmB,iBAAiB,YAAY,IAAI,IAAI;GAE9D,MAAM,SAAS,MAAM,QAAQ;IAAE;IAAS;GAAS,CAAC;GAElD,IAAI,gBACF,cAAc,oBAAoB,OAAO,GAAG;IAC1C,MAAM;IACN,YAAY,YAAY,IAAI,IAAI;GAClC,CAAC;GAGH,QAAQ,IAAI,iCAAiC,SAAS;GAEtD,QAAQ,QAAQ,kBAAkB,QAAQ,KAAK;GAE/C,OAAO;EACT,SAAS,OAAO;GACd,QAAQ,IAAI,OAAO,OAAO;GAC1B,OAAO,mBAAmB,OAAO,QAAQ;EAC3C;CACF,CAAC;AACH;;;;;AAMA,SAAS,mBAAmB,OAAgB,UAAsC;CAiBhF,SAAS,OAAO,iBAAiB,mBAAmB;CAEpD,IAAI,iBAAiB,WAAW;EAC9B,MAAM,UAAyB,EAC7B,OAAO,MAAM,QACf;EACA,IAAI,MAAM,SACR,QAAQ,UAAU,MAAM;EAG1B,IAAI,YAAY,MAAM,eACpB,QAAQ,QAAQ,MAAM;EAGxB,OAAO,SAAS,cAAc,MAAM,MAAM,CAAC,CAAC,KAAK,OAAO;CAC1D;CAEA,IAAI,iBAAiB,+BACnB,OAAO,SAAS,WAAW,EACzB,QAAQ,MAAM,OAChB,CAAC;CAQH,QAAQ,MAAM,sCAAsC,KAAK;CAEzD,OAAO,SAAS,YAAY,EAC1B,OAAO,yBACT,CAAC;AACH;;;;AAKA,SAAgB,EAAE,SAAiB,cAAoB;CACrD,OACEA,eAAuB,WAAW,CAAC,EAAE,MAAM,SAAS,YAAY,KAChE,MAAM,SAAS,YAAY;AAE/B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t } from "./inject-request-context.mjs";
|
|
2
1
|
import { HttpErrorCodes } from "../error-codes.mjs";
|
|
2
|
+
import { t } from "./inject-request-context.mjs";
|
|
3
3
|
import { anyMatch } from "./utils/cidr-match.mjs";
|
|
4
4
|
|
|
5
5
|
//#region ../core/src/http/middleware/ip-filter.middleware.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t } from "./inject-request-context.mjs";
|
|
2
1
|
import { HttpErrorCodes } from "../error-codes.mjs";
|
|
2
|
+
import { t } from "./inject-request-context.mjs";
|
|
3
3
|
import { parseSize } from "./utils/parse-size.mjs";
|
|
4
4
|
|
|
5
5
|
//#region ../core/src/http/middleware/max-body-size.middleware.ts
|
package/esm/http/request.d.mts
CHANGED
|
@@ -210,8 +210,8 @@ declare class Request<RequestValidation = any> {
|
|
|
210
210
|
*/
|
|
211
211
|
protected cacheLocale(candidate: unknown): string;
|
|
212
212
|
/**
|
|
213
|
-
* Resolve the first present Mode B source
|
|
214
|
-
*
|
|
213
|
+
* Resolve the first present Mode B source, skipping unsupported browser
|
|
214
|
+
* preferences. Other unsupported values resolve to the configured default.
|
|
215
215
|
*/
|
|
216
216
|
protected resolveLocale(): string;
|
|
217
217
|
/**
|