@warlock.js/core 5.10.0 → 5.12.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 +33 -0
- package/esm/application/app.d.mts +3 -3
- package/esm/application/app.mjs +6 -6
- package/esm/application/app.mjs.map +1 -1
- package/esm/cli/cli-commands.utils.mjs +7 -3
- package/esm/cli/cli-commands.utils.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
- package/esm/connectors/socket-connector.mjs +13 -16
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/container/index.d.mts +20 -7
- package/esm/container/index.d.mts.map +1 -1
- package/esm/container/index.mjs +8 -5
- package/esm/container/index.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +15 -3
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +1 -1
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +50 -9
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts +8 -0
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs +26 -4
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +3 -3
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +16 -2
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/flags.mjs +16 -1
- package/esm/dev-server/flags.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs +27 -3
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/errors/container-key-missing-error.mjs +18 -6
- package/esm/errors/container-key-missing-error.mjs.map +1 -1
- package/esm/generations/stubs.mjs +10 -10
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/context/request-context.d.mts +14 -11
- package/esm/http/context/request-context.d.mts.map +1 -1
- package/esm/http/context/request-context.mjs +13 -9
- package/esm/http/context/request-context.mjs.map +1 -1
- package/esm/http/createHttpApplication.d.mts.map +1 -1
- package/esm/http/createHttpApplication.mjs +2 -0
- package/esm/http/createHttpApplication.mjs.map +1 -1
- package/esm/http/csp.d.mts +139 -0
- package/esm/http/csp.d.mts.map +1 -0
- package/esm/http/csp.mjs +134 -0
- package/esm/http/csp.mjs.map +1 -0
- package/esm/http/errors/errors.d.mts +17 -1
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +20 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +8 -3
- package/esm/http/index.mjs +6 -1
- package/esm/http/middleware/concurrency-limit.middleware.d.mts +2 -2
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +10 -0
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +13 -13
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/request-controller.d.mts +1 -1
- package/esm/http/request.d.mts +32 -44
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +52 -44
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +18 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +31 -0
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/stream-react-response.d.mts +65 -0
- package/esm/http/stream-react-response.d.mts.map +1 -0
- package/esm/http/stream-react-response.mjs +46 -0
- package/esm/http/stream-react-response.mjs.map +1 -0
- package/esm/http/tracing/index.mjs +4 -0
- package/esm/http/tracing/trace-id.d.mts +14 -0
- package/esm/http/tracing/trace-id.d.mts.map +1 -0
- package/esm/http/tracing/trace-id.mjs +37 -0
- package/esm/http/tracing/trace-id.mjs.map +1 -0
- package/esm/http/tracing/tracing-dispatcher.d.mts +42 -0
- package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -0
- package/esm/http/tracing/tracing-dispatcher.mjs +94 -0
- package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -0
- package/esm/http/tracing/tracing.type.d.mts +66 -0
- package/esm/http/tracing/tracing.type.d.mts.map +1 -0
- package/esm/http/types.d.mts +27 -21
- package/esm/http/types.d.mts.map +1 -1
- package/esm/index.d.mts +9 -4
- package/esm/index.mjs +6 -2
- package/esm/production/esbuild-preflight.mjs +47 -0
- package/esm/production/esbuild-preflight.mjs.map +1 -0
- package/esm/production/production-builder.mjs +19 -15
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/restful/restful.d.mts +1 -1
- package/esm/router/log-request-lifecycle.mjs +12 -2
- package/esm/router/log-request-lifecycle.mjs.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +10 -0
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +1 -1
- package/esm/socket/utils.mjs +1 -2
- package/esm/socket/utils.mjs.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs +2 -5
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +12 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +315 -25
- package/llms.txt +1 -0
- package/package.json +12 -12
- package/skills/add-connector/SKILL.md +1 -1
- package/skills/build-restful/SKILL.md +2 -2
- package/skills/configure-app/SKILL.md +31 -0
- package/skills/create-controller/SKILL.md +4 -4
- package/skills/request-tracing/SKILL.md +208 -0
- package/skills/send-response/SKILL.md +16 -0
- package/skills/store-file/SKILL.md +1 -1
- package/skills/upload-file/SKILL.md +14 -2
- package/skills/use-app-context/SKILL.md +2 -2
- package/skills/use-middleware/SKILL.md +2 -2
- package/skills/use-repository/SKILL.md +1 -1
- package/skills/use-request-locals/SKILL.md +1 -1
- package/skills/validate-input/SKILL.md +13 -0
- package/skills/wire-socket/SKILL.md +3 -3
- package/skills/write-middleware/SKILL.md +11 -5
- package/skills/write-use-case/SKILL.md +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createHttpApplication.mjs","names":[],"sources":["../../../../../../../core/src/http/createHttpApplication.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { log } from \"@warlock.js/logger\";\nimport { router } from \"../router\";\nimport { setBaseUrl } from \"../utils/urls\";\nimport { httpConfig } from \"./config\";\nimport { registerHttpPlugins } from \"./plugins\";\nimport { getHttpServer, startHttpServer } from \"./server\";\n\nexport async function createHttpApplication() {\n const server = startHttpServer();\n\n await registerHttpPlugins(server);\n\n router.scan(server);\n\n const port = httpConfig(\"port\");\n\n try {\n log.info(\"http\", \"server\", \"Connecting to the server\");\n // 👇🏻 We can use the url of the server\n await server.listen({\n port,\n host: httpConfig(\"host\"),\n });\n\n const baseUrl = config.get(\"app.baseUrl\");\n\n // update base url\n setBaseUrl(baseUrl);\n\n log.success(\"http\", \"server\", `Server is listening on ${baseUrl}`);\n } catch (error) {\n log.error(\"http\", \"server\", error);\n\n process.exit(1); // stop the process, exit with error\n }\n}\n\nexport async function stopHttpApplication() {\n log.info(\"http\", \"server\", \"Stopping the server\");\n const server = getHttpServer();\n\n await server?.close();\n\n log.success(\"http\", \"server\", \"Server is stopped\");\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"createHttpApplication.mjs","names":[],"sources":["../../../../../../../core/src/http/createHttpApplication.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { log } from \"@warlock.js/logger\";\nimport { router } from \"../router\";\nimport { setBaseUrl } from \"../utils/urls\";\nimport { httpConfig } from \"./config\";\nimport { validateCspConfigAtBoot } from \"./csp\";\nimport { registerHttpPlugins } from \"./plugins\";\nimport { getHttpServer, startHttpServer } from \"./server\";\n\nexport async function createHttpApplication() {\n // Fail loudly, before the server ever binds a port, when `http.csp` is\n // enabled with a malformed directive — never silently repair it and never\n // wait for the first request to discover it.\n validateCspConfigAtBoot();\n\n const server = startHttpServer();\n\n await registerHttpPlugins(server);\n\n router.scan(server);\n\n const port = httpConfig(\"port\");\n\n try {\n log.info(\"http\", \"server\", \"Connecting to the server\");\n // 👇🏻 We can use the url of the server\n await server.listen({\n port,\n host: httpConfig(\"host\"),\n });\n\n const baseUrl = config.get(\"app.baseUrl\");\n\n // update base url\n setBaseUrl(baseUrl);\n\n log.success(\"http\", \"server\", `Server is listening on ${baseUrl}`);\n } catch (error) {\n log.error(\"http\", \"server\", error);\n\n process.exit(1); // stop the process, exit with error\n }\n}\n\nexport async function stopHttpApplication() {\n log.info(\"http\", \"server\", \"Stopping the server\");\n const server = getHttpServer();\n\n await server?.close();\n\n log.success(\"http\", \"server\", \"Server is stopped\");\n}\n"],"mappings":";;;;;;;;;;;AASA,eAAsB,wBAAwB;CAI5C,wBAAwB;CAExB,MAAM,SAAS,gBAAgB;CAE/B,MAAM,oBAAoB,MAAM;CAEhC,OAAO,KAAK,MAAM;CAElB,MAAM,OAAO,WAAW,MAAM;CAE9B,IAAI;EACF,IAAI,KAAK,QAAQ,UAAU,0BAA0B;EAErD,MAAM,OAAO,OAAO;GAClB;GACA,MAAM,WAAW,MAAM;EACzB,CAAC;EAED,MAAM,UAAU,OAAO,IAAI,aAAa;EAGxC,WAAW,OAAO;EAElB,IAAI,QAAQ,QAAQ,UAAU,0BAA0B,SAAS;CACnE,SAAS,OAAO;EACd,IAAI,MAAM,QAAQ,UAAU,KAAK;EAEjC,QAAQ,KAAK,CAAC;CAChB;AACF;AAEA,eAAsB,sBAAsB;CAC1C,IAAI,KAAK,QAAQ,UAAU,qBAAqB;CAGhD,MAFe,cAEJ,CAAC,EAAE,MAAM;CAEpB,IAAI,QAAQ,QAAQ,UAAU,mBAAmB;AACnD"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Response } from "./response.mjs";
|
|
2
|
+
import { Request } from "./request.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../core/src/http/csp.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* App-facing `http.csp` configuration shape.
|
|
7
|
+
*
|
|
8
|
+
* Disabled by default: an app that never sets `http.csp` (or sets
|
|
9
|
+
* `enabled: false`) gets no `Content-Security-Policy` header at all — zero
|
|
10
|
+
* behaviour change for every existing app. Opting in only swaps `enabled` to
|
|
11
|
+
* `true`; `reportOnly` and `directives` are optional refinements on top.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts title="src/config/http.ts"
|
|
15
|
+
* const httpConfigurations: HttpConfigurations = {
|
|
16
|
+
* csp: {
|
|
17
|
+
* enabled: true,
|
|
18
|
+
* directives: {
|
|
19
|
+
* "img-src": ["'self'", "data:", "https://cdn.example.com"],
|
|
20
|
+
* },
|
|
21
|
+
* },
|
|
22
|
+
* };
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
type CspConfig = {
|
|
26
|
+
/**
|
|
27
|
+
* Turn the header on. Everything else in this type is inert while this is
|
|
28
|
+
* `false`/unset.
|
|
29
|
+
*/
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Emit `Content-Security-Policy-Report-Only` instead of the enforcing
|
|
33
|
+
* `Content-Security-Policy` header — the browser reports violations
|
|
34
|
+
* (via `report-to`/`report-uri`, if the app's directives configure one)
|
|
35
|
+
* without blocking anything. Use this to observe a policy safely before
|
|
36
|
+
* enforcing it.
|
|
37
|
+
*
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
reportOnly?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* App-supplied directive values. A directive named here REPLACES the
|
|
43
|
+
* framework's default list for that directive entirely (the two lists are
|
|
44
|
+
* never merged element-wise) — declare the full value list you want.
|
|
45
|
+
*
|
|
46
|
+
* `script-src` is special-cased regardless of whether it is declared here:
|
|
47
|
+
* the current request's CSP nonce (`'nonce-<value>'`) is always appended to
|
|
48
|
+
* it, so the framework's own inline/module scripts keep working.
|
|
49
|
+
*/
|
|
50
|
+
directives?: Record<string, string[]>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The policy every `http.csp`-enabled app starts from. Deliberately
|
|
54
|
+
* conservative — same-origin by default, no plugins, no framing by other
|
|
55
|
+
* sites — and documented here rather than only in the skill, since this is
|
|
56
|
+
* the literal set an app's `directives` entry replaces one key at a time.
|
|
57
|
+
*/
|
|
58
|
+
declare const DEFAULT_CSP_DIRECTIVES: Readonly<Record<string, readonly string[]>>;
|
|
59
|
+
/**
|
|
60
|
+
* Thrown at boot when `http.csp.directives` contains a value the framework
|
|
61
|
+
* refuses to ship as-is, rather than silently dropping or rewriting it.
|
|
62
|
+
*
|
|
63
|
+
* Two shapes are rejected:
|
|
64
|
+
* - a value containing `;` — CSP directives are `;`-delimited, so a `;`
|
|
65
|
+
* inside one value would prematurely close it and let the remainder be
|
|
66
|
+
* parsed as a new, attacker-uncontrolled-but-developer-typo'd directive.
|
|
67
|
+
* - a value with an odd number of `'` characters — every CSP keyword
|
|
68
|
+
* (`'self'`, `'none'`, a nonce/hash source) is single-quote-wrapped, so an
|
|
69
|
+
* unbalanced count means a keyword was truncated or malformed.
|
|
70
|
+
*/
|
|
71
|
+
declare class InvalidCspDirectiveError extends Error {
|
|
72
|
+
readonly directive: string;
|
|
73
|
+
readonly value: string;
|
|
74
|
+
constructor(directive: string, value: string, reason: string);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate every directive value an app supplied, throwing
|
|
78
|
+
* {@link InvalidCspDirectiveError} on the first offender.
|
|
79
|
+
*
|
|
80
|
+
* Exported so boot code and tests can call it directly, independent of
|
|
81
|
+
* whether `http.csp` happens to be enabled.
|
|
82
|
+
*/
|
|
83
|
+
declare function validateCspDirectives(directives: Record<string, string[]> | undefined): void;
|
|
84
|
+
/**
|
|
85
|
+
* Read the app's `http.csp` config, if any.
|
|
86
|
+
*/
|
|
87
|
+
declare function resolveCspConfig(): CspConfig | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Fail the boot loudly when `http.csp` is enabled with invalid directives.
|
|
90
|
+
*
|
|
91
|
+
* Called once, from {@link createHttpApplication} before the server starts
|
|
92
|
+
* listening — never per-request. A per-request validation would let an app
|
|
93
|
+
* boot successfully and only discover the bad config on first traffic;
|
|
94
|
+
* checking once at boot surfaces it immediately, the same way a malformed
|
|
95
|
+
* `warlock.config.ts` would.
|
|
96
|
+
*
|
|
97
|
+
* @throws {InvalidCspDirectiveError} when a directive value is malformed.
|
|
98
|
+
*/
|
|
99
|
+
declare function validateCspConfigAtBoot(): void;
|
|
100
|
+
/**
|
|
101
|
+
* Merge the app's directives over the framework defaults and fold in the
|
|
102
|
+
* request's nonce.
|
|
103
|
+
*
|
|
104
|
+
* Per-directive replacement, not element-wise merging: a directive the app
|
|
105
|
+
* names wins outright, everything else falls back to
|
|
106
|
+
* {@link DEFAULT_CSP_DIRECTIVES}. `script-src` always gains
|
|
107
|
+
* `'nonce-<nonce>'` on top, whichever list it started from, so the
|
|
108
|
+
* framework's own inline payload script and hydration module script are
|
|
109
|
+
* never blocked by an app's own policy.
|
|
110
|
+
*/
|
|
111
|
+
declare function mergeCspDirectives(appDirectives: Record<string, string[]> | undefined, nonce: string): Record<string, string[]>;
|
|
112
|
+
/**
|
|
113
|
+
* Serialize a merged directive map into a header value —
|
|
114
|
+
* `"default-src 'self'; script-src 'self' 'nonce-...'; ..."`.
|
|
115
|
+
*/
|
|
116
|
+
declare function serializeCspDirectives(directives: Record<string, string[]>): string;
|
|
117
|
+
/**
|
|
118
|
+
* Build the full `Content-Security-Policy` (or `-Report-Only`) header value
|
|
119
|
+
* for one request, using that request's own nonce.
|
|
120
|
+
*/
|
|
121
|
+
declare function buildCspHeaderValue(cspConfig: CspConfig, nonce: string): string;
|
|
122
|
+
/**
|
|
123
|
+
* Stamp the `Content-Security-Policy` (or, in `reportOnly` mode,
|
|
124
|
+
* `Content-Security-Policy-Report-Only`) header on the response, when the
|
|
125
|
+
* app opted in via `http.csp.enabled`.
|
|
126
|
+
*
|
|
127
|
+
* A no-op when `http.csp` is absent or `enabled` is falsy — the default,
|
|
128
|
+
* behaviour-preserving state for every app that hasn't opted in.
|
|
129
|
+
*
|
|
130
|
+
* Reads `request.nonce`, which lazily generates and caches the per-request
|
|
131
|
+
* nonce (`request.ts`) — calling this early in the request lifecycle (see
|
|
132
|
+
* `inject-request-context.ts`) means the SAME nonce this stamps into the
|
|
133
|
+
* header is the one still available later for the web layer's `<script>`
|
|
134
|
+
* tags to read off `request.nonce`.
|
|
135
|
+
*/
|
|
136
|
+
declare function applyCspHeader(request: Request, response: Response): void;
|
|
137
|
+
//#endregion
|
|
138
|
+
export { CspConfig, DEFAULT_CSP_DIRECTIVES, InvalidCspDirectiveError, applyCspHeader, buildCspHeaderValue, mergeCspDirectives, resolveCspConfig, serializeCspDirectives, validateCspConfigAtBoot, validateCspDirectives };
|
|
139
|
+
//# sourceMappingURL=csp.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csp.d.mts","names":[],"sources":["../../../../../../../core/src/http/csp.ts"],"mappings":";;;;;;AAwBA;;;;;;;;;AAyBqB;AASrB;;;;AAAoD;AAsBpD;;;KAxDY,SAAA;EAwDkC;;;;EAnD5C,OAAA;EAsDkB;;;AACF;AAkBlB;;;;AAAsF;EA/DpF,UAAA;EA2F8B;;;AAAa;AAe7C;;;;AAAuC;EAhGrC,UAAA,GAAa,MAAM;AAAA;;;;;;;cASR,sBAAA,EAAwB,QAAQ,CAAC,MAAA;AA6GrC;AAqCT;;;;AAA2E;AAU3E;;;;;;AA/CS,cAvFI,wBAAA,SAAiC,KAAK;EAAA,SAE/B,SAAA;EAAA,SACA,KAAA;cADA,SAAA,UACA,KAAA,UAChB,MAAA;AAAA;;;;;;;;iBAkBY,qBAAA,CAAsB,UAAgD,EAApC,MAAM;;;;iBA4BxC,gBAAA,IAAoB,SAAS;;;;;;;;;;;;iBAe7B,uBAAA;;;;;;;;;;;;iBAmBA,kBAAA,CACd,aAAA,EAAe,MAAA,gCACf,KAAA,WACC,MAAM;;;;;iBAqCO,sBAAA,CAAuB,UAAoC,EAAxB,MAAM;;;;;iBAUzC,mBAAA,CAAoB,SAAA,EAAW,SAAS,EAAE,KAAA;;;;;;;;;;;;;;;iBAkB1C,cAAA,CAAe,OAAA,EAAS,OAAA,EAAS,QAAA,EAAU,QAAQ"}
|
package/esm/http/csp.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import config from "@mongez/config";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/http/csp.ts
|
|
4
|
+
/**
|
|
5
|
+
* The policy every `http.csp`-enabled app starts from. Deliberately
|
|
6
|
+
* conservative — same-origin by default, no plugins, no framing by other
|
|
7
|
+
* sites — and documented here rather than only in the skill, since this is
|
|
8
|
+
* the literal set an app's `directives` entry replaces one key at a time.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CSP_DIRECTIVES = {
|
|
11
|
+
"default-src": ["'self'"],
|
|
12
|
+
"script-src": ["'self'"],
|
|
13
|
+
"style-src": ["'self'"],
|
|
14
|
+
"img-src": ["'self'", "data:"],
|
|
15
|
+
"object-src": ["'none'"],
|
|
16
|
+
"base-uri": ["'self'"],
|
|
17
|
+
"frame-ancestors": ["'self'"]
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Thrown at boot when `http.csp.directives` contains a value the framework
|
|
21
|
+
* refuses to ship as-is, rather than silently dropping or rewriting it.
|
|
22
|
+
*
|
|
23
|
+
* Two shapes are rejected:
|
|
24
|
+
* - a value containing `;` — CSP directives are `;`-delimited, so a `;`
|
|
25
|
+
* inside one value would prematurely close it and let the remainder be
|
|
26
|
+
* parsed as a new, attacker-uncontrolled-but-developer-typo'd directive.
|
|
27
|
+
* - a value with an odd number of `'` characters — every CSP keyword
|
|
28
|
+
* (`'self'`, `'none'`, a nonce/hash source) is single-quote-wrapped, so an
|
|
29
|
+
* unbalanced count means a keyword was truncated or malformed.
|
|
30
|
+
*/
|
|
31
|
+
var InvalidCspDirectiveError = class extends Error {
|
|
32
|
+
constructor(directive, value, reason) {
|
|
33
|
+
super(`Invalid "http.csp.directives" entry — directive "${directive}" has a value ${JSON.stringify(value)} that ${reason} Fix the value in your app's http config; the framework will not silently repair it.`);
|
|
34
|
+
this.directive = directive;
|
|
35
|
+
this.value = value;
|
|
36
|
+
this.name = "InvalidCspDirectiveError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Validate every directive value an app supplied, throwing
|
|
41
|
+
* {@link InvalidCspDirectiveError} on the first offender.
|
|
42
|
+
*
|
|
43
|
+
* Exported so boot code and tests can call it directly, independent of
|
|
44
|
+
* whether `http.csp` happens to be enabled.
|
|
45
|
+
*/
|
|
46
|
+
function validateCspDirectives(directives) {
|
|
47
|
+
if (!directives) return;
|
|
48
|
+
for (const [directive, values] of Object.entries(directives)) for (const value of values) {
|
|
49
|
+
if (value.includes(";")) throw new InvalidCspDirectiveError(directive, value, "contains a ';' character, which would truncate the directive.");
|
|
50
|
+
if ((value.match(/'/g) ?? []).length % 2 !== 0) throw new InvalidCspDirectiveError(directive, value, "has an unbalanced number of ' characters.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read the app's `http.csp` config, if any.
|
|
55
|
+
*/
|
|
56
|
+
function resolveCspConfig() {
|
|
57
|
+
return config.get("http.csp");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Fail the boot loudly when `http.csp` is enabled with invalid directives.
|
|
61
|
+
*
|
|
62
|
+
* Called once, from {@link createHttpApplication} before the server starts
|
|
63
|
+
* listening — never per-request. A per-request validation would let an app
|
|
64
|
+
* boot successfully and only discover the bad config on first traffic;
|
|
65
|
+
* checking once at boot surfaces it immediately, the same way a malformed
|
|
66
|
+
* `warlock.config.ts` would.
|
|
67
|
+
*
|
|
68
|
+
* @throws {InvalidCspDirectiveError} when a directive value is malformed.
|
|
69
|
+
*/
|
|
70
|
+
function validateCspConfigAtBoot() {
|
|
71
|
+
const cspConfig = resolveCspConfig();
|
|
72
|
+
if (!cspConfig?.enabled) return;
|
|
73
|
+
validateCspDirectives(cspConfig.directives);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Merge the app's directives over the framework defaults and fold in the
|
|
77
|
+
* request's nonce.
|
|
78
|
+
*
|
|
79
|
+
* Per-directive replacement, not element-wise merging: a directive the app
|
|
80
|
+
* names wins outright, everything else falls back to
|
|
81
|
+
* {@link DEFAULT_CSP_DIRECTIVES}. `script-src` always gains
|
|
82
|
+
* `'nonce-<nonce>'` on top, whichever list it started from, so the
|
|
83
|
+
* framework's own inline payload script and hydration module script are
|
|
84
|
+
* never blocked by an app's own policy.
|
|
85
|
+
*/
|
|
86
|
+
function mergeCspDirectives(appDirectives, nonce) {
|
|
87
|
+
const merged = {};
|
|
88
|
+
for (const [directive, defaultValues] of Object.entries(DEFAULT_CSP_DIRECTIVES)) merged[directive] = appDirectives?.[directive] ? [...appDirectives[directive]] : [...defaultValues];
|
|
89
|
+
if (appDirectives) {
|
|
90
|
+
for (const [directive, values] of Object.entries(appDirectives)) if (!(directive in merged)) merged[directive] = [...values];
|
|
91
|
+
}
|
|
92
|
+
const scriptSrc = merged["script-src"] ?? [];
|
|
93
|
+
const nonceSource = `'nonce-${nonce}'`;
|
|
94
|
+
merged["script-src"] = scriptSrc.includes(nonceSource) ? scriptSrc : [...scriptSrc, nonceSource];
|
|
95
|
+
return merged;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Serialize a merged directive map into a header value —
|
|
99
|
+
* `"default-src 'self'; script-src 'self' 'nonce-...'; ..."`.
|
|
100
|
+
*/
|
|
101
|
+
function serializeCspDirectives(directives) {
|
|
102
|
+
return Object.entries(directives).map(([directive, values]) => `${directive} ${values.join(" ")}`).join("; ");
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build the full `Content-Security-Policy` (or `-Report-Only`) header value
|
|
106
|
+
* for one request, using that request's own nonce.
|
|
107
|
+
*/
|
|
108
|
+
function buildCspHeaderValue(cspConfig, nonce) {
|
|
109
|
+
return serializeCspDirectives(mergeCspDirectives(cspConfig.directives, nonce));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Stamp the `Content-Security-Policy` (or, in `reportOnly` mode,
|
|
113
|
+
* `Content-Security-Policy-Report-Only`) header on the response, when the
|
|
114
|
+
* app opted in via `http.csp.enabled`.
|
|
115
|
+
*
|
|
116
|
+
* A no-op when `http.csp` is absent or `enabled` is falsy — the default,
|
|
117
|
+
* behaviour-preserving state for every app that hasn't opted in.
|
|
118
|
+
*
|
|
119
|
+
* Reads `request.nonce`, which lazily generates and caches the per-request
|
|
120
|
+
* nonce (`request.ts`) — calling this early in the request lifecycle (see
|
|
121
|
+
* `inject-request-context.ts`) means the SAME nonce this stamps into the
|
|
122
|
+
* header is the one still available later for the web layer's `<script>`
|
|
123
|
+
* tags to read off `request.nonce`.
|
|
124
|
+
*/
|
|
125
|
+
function applyCspHeader(request, response) {
|
|
126
|
+
const cspConfig = resolveCspConfig();
|
|
127
|
+
if (!cspConfig?.enabled) return;
|
|
128
|
+
const headerName = cspConfig.reportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy";
|
|
129
|
+
response.header(headerName, buildCspHeaderValue(cspConfig, request.nonce));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
export { DEFAULT_CSP_DIRECTIVES, InvalidCspDirectiveError, applyCspHeader, buildCspHeaderValue, mergeCspDirectives, resolveCspConfig, serializeCspDirectives, validateCspConfigAtBoot, validateCspDirectives };
|
|
134
|
+
//# sourceMappingURL=csp.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csp.mjs","names":[],"sources":["../../../../../../../core/src/http/csp.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport type { Request } from \"./request\";\nimport type { Response } from \"./response\";\n\n/**\n * App-facing `http.csp` configuration shape.\n *\n * Disabled by default: an app that never sets `http.csp` (or sets\n * `enabled: false`) gets no `Content-Security-Policy` header at all — zero\n * behaviour change for every existing app. Opting in only swaps `enabled` to\n * `true`; `reportOnly` and `directives` are optional refinements on top.\n *\n * @example\n * ```ts title=\"src/config/http.ts\"\n * const httpConfigurations: HttpConfigurations = {\n * csp: {\n * enabled: true,\n * directives: {\n * \"img-src\": [\"'self'\", \"data:\", \"https://cdn.example.com\"],\n * },\n * },\n * };\n * ```\n */\nexport type CspConfig = {\n /**\n * Turn the header on. Everything else in this type is inert while this is\n * `false`/unset.\n */\n enabled: boolean;\n /**\n * Emit `Content-Security-Policy-Report-Only` instead of the enforcing\n * `Content-Security-Policy` header — the browser reports violations\n * (via `report-to`/`report-uri`, if the app's directives configure one)\n * without blocking anything. Use this to observe a policy safely before\n * enforcing it.\n *\n * @default false\n */\n reportOnly?: boolean;\n /**\n * App-supplied directive values. A directive named here REPLACES the\n * framework's default list for that directive entirely (the two lists are\n * never merged element-wise) — declare the full value list you want.\n *\n * `script-src` is special-cased regardless of whether it is declared here:\n * the current request's CSP nonce (`'nonce-<value>'`) is always appended to\n * it, so the framework's own inline/module scripts keep working.\n */\n directives?: Record<string, string[]>;\n};\n\n/**\n * The policy every `http.csp`-enabled app starts from. Deliberately\n * conservative — same-origin by default, no plugins, no framing by other\n * sites — and documented here rather than only in the skill, since this is\n * the literal set an app's `directives` entry replaces one key at a time.\n */\nexport const DEFAULT_CSP_DIRECTIVES: Readonly<Record<string, readonly string[]>> = {\n \"default-src\": [\"'self'\"],\n \"script-src\": [\"'self'\"],\n \"style-src\": [\"'self'\"],\n \"img-src\": [\"'self'\", \"data:\"],\n \"object-src\": [\"'none'\"],\n \"base-uri\": [\"'self'\"],\n \"frame-ancestors\": [\"'self'\"],\n};\n\n/**\n * Thrown at boot when `http.csp.directives` contains a value the framework\n * refuses to ship as-is, rather than silently dropping or rewriting it.\n *\n * Two shapes are rejected:\n * - a value containing `;` — CSP directives are `;`-delimited, so a `;`\n * inside one value would prematurely close it and let the remainder be\n * parsed as a new, attacker-uncontrolled-but-developer-typo'd directive.\n * - a value with an odd number of `'` characters — every CSP keyword\n * (`'self'`, `'none'`, a nonce/hash source) is single-quote-wrapped, so an\n * unbalanced count means a keyword was truncated or malformed.\n */\nexport class InvalidCspDirectiveError extends Error {\n public constructor(\n public readonly directive: string,\n public readonly value: string,\n reason: string,\n ) {\n super(\n `Invalid \"http.csp.directives\" entry — directive \"${directive}\" has a value ` +\n `${JSON.stringify(value)} that ${reason} Fix the value in your app's http config; ` +\n `the framework will not silently repair it.`,\n );\n this.name = \"InvalidCspDirectiveError\";\n }\n}\n\n/**\n * Validate every directive value an app supplied, throwing\n * {@link InvalidCspDirectiveError} on the first offender.\n *\n * Exported so boot code and tests can call it directly, independent of\n * whether `http.csp` happens to be enabled.\n */\nexport function validateCspDirectives(directives: Record<string, string[]> | undefined): void {\n if (!directives) return;\n\n for (const [directive, values] of Object.entries(directives)) {\n for (const value of values) {\n if (value.includes(\";\")) {\n throw new InvalidCspDirectiveError(\n directive,\n value,\n \"contains a ';' character, which would truncate the directive.\",\n );\n }\n\n const quoteCount = (value.match(/'/g) ?? []).length;\n if (quoteCount % 2 !== 0) {\n throw new InvalidCspDirectiveError(\n directive,\n value,\n \"has an unbalanced number of ' characters.\",\n );\n }\n }\n }\n}\n\n/**\n * Read the app's `http.csp` config, if any.\n */\nexport function resolveCspConfig(): CspConfig | undefined {\n return config.get(\"http.csp\");\n}\n\n/**\n * Fail the boot loudly when `http.csp` is enabled with invalid directives.\n *\n * Called once, from {@link createHttpApplication} before the server starts\n * listening — never per-request. A per-request validation would let an app\n * boot successfully and only discover the bad config on first traffic;\n * checking once at boot surfaces it immediately, the same way a malformed\n * `warlock.config.ts` would.\n *\n * @throws {InvalidCspDirectiveError} when a directive value is malformed.\n */\nexport function validateCspConfigAtBoot(): void {\n const cspConfig = resolveCspConfig();\n\n if (!cspConfig?.enabled) return;\n\n validateCspDirectives(cspConfig.directives);\n}\n\n/**\n * Merge the app's directives over the framework defaults and fold in the\n * request's nonce.\n *\n * Per-directive replacement, not element-wise merging: a directive the app\n * names wins outright, everything else falls back to\n * {@link DEFAULT_CSP_DIRECTIVES}. `script-src` always gains\n * `'nonce-<nonce>'` on top, whichever list it started from, so the\n * framework's own inline payload script and hydration module script are\n * never blocked by an app's own policy.\n */\nexport function mergeCspDirectives(\n appDirectives: Record<string, string[]> | undefined,\n nonce: string,\n): Record<string, string[]> {\n const merged: Record<string, string[]> = {};\n\n for (const [directive, defaultValues] of Object.entries(DEFAULT_CSP_DIRECTIVES)) {\n merged[directive] = appDirectives?.[directive]\n ? [...appDirectives[directive]]\n : [...defaultValues];\n }\n\n if (appDirectives) {\n for (const [directive, values] of Object.entries(appDirectives)) {\n if (!(directive in merged)) {\n merged[directive] = [...values];\n }\n }\n }\n\n // `merged[\"script-src\"]` is always populated by the loop above —\n // `DEFAULT_CSP_DIRECTIVES` (:59-67) declares a `script-src` entry, and every\n // one of its keys is copied into `merged` unconditionally — but its type is\n // `Record<string, string[]>`, so `noUncheckedIndexedAccess` still widens the\n // read to `string[] | undefined`. Narrowed once, locally, rather than\n // asserted away.\n const scriptSrc = merged[\"script-src\"] ?? [];\n const nonceSource = `'nonce-${nonce}'`;\n\n merged[\"script-src\"] = scriptSrc.includes(nonceSource)\n ? scriptSrc\n : [...scriptSrc, nonceSource];\n\n return merged;\n}\n\n/**\n * Serialize a merged directive map into a header value —\n * `\"default-src 'self'; script-src 'self' 'nonce-...'; ...\"`.\n */\nexport function serializeCspDirectives(directives: Record<string, string[]>): string {\n return Object.entries(directives)\n .map(([directive, values]) => `${directive} ${values.join(\" \")}`)\n .join(\"; \");\n}\n\n/**\n * Build the full `Content-Security-Policy` (or `-Report-Only`) header value\n * for one request, using that request's own nonce.\n */\nexport function buildCspHeaderValue(cspConfig: CspConfig, nonce: string): string {\n return serializeCspDirectives(mergeCspDirectives(cspConfig.directives, nonce));\n}\n\n/**\n * Stamp the `Content-Security-Policy` (or, in `reportOnly` mode,\n * `Content-Security-Policy-Report-Only`) header on the response, when the\n * app opted in via `http.csp.enabled`.\n *\n * A no-op when `http.csp` is absent or `enabled` is falsy — the default,\n * behaviour-preserving state for every app that hasn't opted in.\n *\n * Reads `request.nonce`, which lazily generates and caches the per-request\n * nonce (`request.ts`) — calling this early in the request lifecycle (see\n * `inject-request-context.ts`) means the SAME nonce this stamps into the\n * header is the one still available later for the web layer's `<script>`\n * tags to read off `request.nonce`.\n */\nexport function applyCspHeader(request: Request, response: Response): void {\n const cspConfig = resolveCspConfig();\n\n if (!cspConfig?.enabled) return;\n\n const headerName = cspConfig.reportOnly\n ? \"Content-Security-Policy-Report-Only\"\n : \"Content-Security-Policy\";\n\n response.header(headerName, buildCspHeaderValue(cspConfig, request.nonce));\n}\n"],"mappings":";;;;;;;;;AA0DA,MAAa,yBAAsE;CACjF,eAAe,CAAC,QAAQ;CACxB,cAAc,CAAC,QAAQ;CACvB,aAAa,CAAC,QAAQ;CACtB,WAAW,CAAC,UAAU,OAAO;CAC7B,cAAc,CAAC,QAAQ;CACvB,YAAY,CAAC,QAAQ;CACrB,mBAAmB,CAAC,QAAQ;AAC9B;;;;;;;;;;;;;AAcA,IAAa,2BAAb,cAA8C,MAAM;CAClD,AAAO,YACL,AAAgB,WAChB,AAAgB,OAChB,QACA;EACA,MACE,oDAAoD,UAAU,gBACzD,KAAK,UAAU,KAAK,EAAE,QAAQ,OAAO,qFAE5C;EARgB;EACA;EAQhB,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAgB,sBAAsB,YAAwD;CAC5F,IAAI,CAAC,YAAY;CAEjB,KAAK,MAAM,CAAC,WAAW,WAAW,OAAO,QAAQ,UAAU,GACzD,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,MAAM,SAAS,GAAG,GACpB,MAAM,IAAI,yBACR,WACA,OACA,+DACF;EAIF,KADoB,MAAM,MAAM,IAAI,KAAK,CAAC,EAAC,CAAE,SAC5B,MAAM,GACrB,MAAM,IAAI,yBACR,WACA,OACA,2CACF;CAEJ;AAEJ;;;;AAKA,SAAgB,mBAA0C;CACxD,OAAO,OAAO,IAAI,UAAU;AAC9B;;;;;;;;;;;;AAaA,SAAgB,0BAAgC;CAC9C,MAAM,YAAY,iBAAiB;CAEnC,IAAI,CAAC,WAAW,SAAS;CAEzB,sBAAsB,UAAU,UAAU;AAC5C;;;;;;;;;;;;AAaA,SAAgB,mBACd,eACA,OAC0B;CAC1B,MAAM,SAAmC,CAAC;CAE1C,KAAK,MAAM,CAAC,WAAW,kBAAkB,OAAO,QAAQ,sBAAsB,GAC5E,OAAO,aAAa,gBAAgB,aAChC,CAAC,GAAG,cAAc,UAAU,IAC5B,CAAC,GAAG,aAAa;CAGvB,IAAI,eACF;OAAK,MAAM,CAAC,WAAW,WAAW,OAAO,QAAQ,aAAa,GAC5D,IAAI,EAAE,aAAa,SACjB,OAAO,aAAa,CAAC,GAAG,MAAM;CAElC;CASF,MAAM,YAAY,OAAO,iBAAiB,CAAC;CAC3C,MAAM,cAAc,UAAU,MAAM;CAEpC,OAAO,gBAAgB,UAAU,SAAS,WAAW,IACjD,YACA,CAAC,GAAG,WAAW,WAAW;CAE9B,OAAO;AACT;;;;;AAMA,SAAgB,uBAAuB,YAA8C;CACnF,OAAO,OAAO,QAAQ,UAAU,CAAC,CAC9B,KAAK,CAAC,WAAW,YAAY,GAAG,UAAU,GAAG,OAAO,KAAK,GAAG,GAAG,CAAC,CAChE,KAAK,IAAI;AACd;;;;;AAMA,SAAgB,oBAAoB,WAAsB,OAAuB;CAC/E,OAAO,uBAAuB,mBAAmB,UAAU,YAAY,KAAK,CAAC;AAC/E;;;;;;;;;;;;;;;AAgBA,SAAgB,eAAe,SAAkB,UAA0B;CACzE,MAAM,YAAY,iBAAiB;CAEnC,IAAI,CAAC,WAAW,SAAS;CAEzB,MAAM,aAAa,UAAU,aACzB,wCACA;CAEJ,SAAS,OAAO,YAAY,oBAAoB,WAAW,QAAQ,KAAK,CAAC;AAC3E"}
|
|
@@ -37,6 +37,22 @@ declare class NotAllowedError extends HttpError {
|
|
|
37
37
|
payload?: any | undefined;
|
|
38
38
|
constructor(message: string, payload?: any | undefined);
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Thrown by `Request.prototype.user` in development to catch a call site
|
|
42
|
+
* still reading the removed `request.user` getter/setter after the 5.12.0
|
|
43
|
+
* move: the authenticated user now lives at `request.locals.user`, written
|
|
44
|
+
* by `@warlock.js/auth`'s middleware.
|
|
45
|
+
*
|
|
46
|
+
* Development-only diagnostic, not a runtime contract other code should
|
|
47
|
+
* catch — the getter itself is typed `never`, so a caller that still
|
|
48
|
+
* compiles against `request.user` only does so via `any`/an outdated type.
|
|
49
|
+
* Kept in core (not auth) because `request.user` is a core `Request`
|
|
50
|
+
* accessor and core cannot depend on `@warlock.js/auth` to react to it.
|
|
51
|
+
* Slated for removal one release after 5.12.0 — see the CHANGELOG.
|
|
52
|
+
*/
|
|
53
|
+
declare class RequestUserMovedError extends Error {
|
|
54
|
+
constructor();
|
|
55
|
+
}
|
|
40
56
|
//#endregion
|
|
41
|
-
export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
|
|
57
|
+
export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
|
|
42
58
|
//# sourceMappingURL=errors.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"mappings":";cAAa,SAAA,SAAkB,KAAK;EAEzB,MAAA;EACA,OAAA;EACA,OAAA;cAFA,MAAA,UACA,OAAA,UACA,OAAA;AAAA;AAAA,cAOE,qBAAA,SAA8B,SAAS;EAGzC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,iBAAA,SAA0B,SAAS;EAGrC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,cAAA,SAAuB,SAAS;EAGlC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,WAAA,SAAoB,SAAS;EAG/B,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,aAAA,SAAsB,SAAS;EAGjC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,kBAAA,SAA2B,SAAS;EAGtC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA"}
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"mappings":";cAAa,SAAA,SAAkB,KAAK;EAEzB,MAAA;EACA,OAAA;EACA,OAAA;cAFA,MAAA,UACA,OAAA,UACA,OAAA;AAAA;AAAA,cAOE,qBAAA,SAA8B,SAAS;EAGzC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,iBAAA,SAA0B,SAAS;EAGrC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,cAAA,SAAuB,SAAS;EAGlC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,WAAA,SAAoB,SAAS;EAG/B,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,aAAA,SAAsB,SAAS;EAGjC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,kBAAA,SAA2B,SAAS;EAGtC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;;;;;;;;;;AAlDa;AAOxB;;;cA+Da,qBAAA,SAA8B,KAAK;EAAL,WAAA;AAAA"}
|
|
@@ -64,7 +64,26 @@ var NotAllowedError = class extends HttpError {
|
|
|
64
64
|
this.name = "NotAllowedError";
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Thrown by `Request.prototype.user` in development to catch a call site
|
|
69
|
+
* still reading the removed `request.user` getter/setter after the 5.12.0
|
|
70
|
+
* move: the authenticated user now lives at `request.locals.user`, written
|
|
71
|
+
* by `@warlock.js/auth`'s middleware.
|
|
72
|
+
*
|
|
73
|
+
* Development-only diagnostic, not a runtime contract other code should
|
|
74
|
+
* catch — the getter itself is typed `never`, so a caller that still
|
|
75
|
+
* compiles against `request.user` only does so via `any`/an outdated type.
|
|
76
|
+
* Kept in core (not auth) because `request.user` is a core `Request`
|
|
77
|
+
* accessor and core cannot depend on `@warlock.js/auth` to react to it.
|
|
78
|
+
* Slated for removal one release after 5.12.0 — see the CHANGELOG.
|
|
79
|
+
*/
|
|
80
|
+
var RequestUserMovedError = class extends Error {
|
|
81
|
+
constructor() {
|
|
82
|
+
super("request.user has been removed. The authenticated user now lives at request.locals.user, set by @warlock.js/auth's middleware.");
|
|
83
|
+
this.name = "RequestUserMovedError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
67
86
|
|
|
68
87
|
//#endregion
|
|
69
|
-
export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
|
|
88
|
+
export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
|
|
70
89
|
//# sourceMappingURL=errors.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"sourcesContent":["export class HttpError extends Error {\
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"sourcesContent":["export class HttpError extends Error {\n public constructor(\n public status: number,\n public message: string,\n public payload?: any,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\nexport class ResourceNotFoundError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(404, message, payload);\n this.name = \"ResourceNotFoundError\";\n }\n}\n\nexport class UnAuthorizedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(401, message, payload);\n this.name = \"UnAuthorizedError\";\n }\n}\n\nexport class ForbiddenError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(403, message, payload);\n this.name = \"ForbiddenError\";\n }\n}\n\nexport class BadRequestError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(400, message, payload);\n this.name = \"BadRequestError\";\n }\n}\n\nexport class ServerError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(500, message, payload);\n this.name = \"ServerError\";\n }\n}\n\nexport class ConflictError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(409, message, payload);\n this.name = \"ConflictError\";\n }\n}\n\nexport class NotAcceptableError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(406, message, payload);\n this.name = \"NotAcceptableError\";\n }\n}\n\nexport class NotAllowedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(405, message, payload);\n this.name = \"NotAllowedError\";\n }\n}\n\n/**\n * Thrown by `Request.prototype.user` in development to catch a call site\n * still reading the removed `request.user` getter/setter after the 5.12.0\n * move: the authenticated user now lives at `request.locals.user`, written\n * by `@warlock.js/auth`'s middleware.\n *\n * Development-only diagnostic, not a runtime contract other code should\n * catch — the getter itself is typed `never`, so a caller that still\n * compiles against `request.user` only does so via `any`/an outdated type.\n * Kept in core (not auth) because `request.user` is a core `Request`\n * accessor and core cannot depend on `@warlock.js/auth` to react to it.\n * Slated for removal one release after 5.12.0 — see the CHANGELOG.\n */\nexport class RequestUserMovedError extends Error {\n public constructor() {\n super(\n \"request.user has been removed. The authenticated user now lives at \" +\n \"request.locals.user, set by @warlock.js/auth's middleware.\",\n );\n this.name = \"RequestUserMovedError\";\n }\n}\n"],"mappings":";AAAA,IAAa,YAAb,cAA+B,MAAM;CACnC,AAAO,YACL,AAAO,QACP,AAAO,SACP,AAAO,SACP;EACA,MAAM,OAAO;EAJN;EACA;EACA;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,wBAAb,cAA2C,UAAU;CACnD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,oBAAb,cAAuC,UAAU;CAC/C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,iBAAb,cAAoC,UAAU;CAC5C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,cAAb,cAAiC,UAAU;CACzC,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,gBAAb,cAAmC,UAAU;CAC3C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,qBAAb,cAAwC,UAAU;CAChD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;AAeA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,AAAO,cAAc;EACnB,MACE,+HAEF;EACA,KAAK,OAAO;CACd;AACF"}
|
package/esm/http/index.d.mts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { PipeableReactStream, StreamReactResponseOptions, streamReactResponse } from "./stream-react-response.mjs";
|
|
2
|
+
import { CookieOptions, Response, ResponseStatus, SendBufferOptions, SendFileOptions } from "./response.mjs";
|
|
1
3
|
import { FileNamingStrategy, ImageTransformCallback, ImageTransformConfig, PrefixConfig, PrefixOptions, SaveAsOptions, SaveOptions, UploadedFileImageOptions, UploadsConfigurations } from "./uploads-types.mjs";
|
|
2
4
|
import { FileValidationOptions, UploadedFile, UploadedFileJson } from "./uploaded-file.mjs";
|
|
3
5
|
import { Request } from "./request.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
+
import { CspConfig, DEFAULT_CSP_DIRECTIVES, InvalidCspDirectiveError, applyCspHeader, buildCspHeaderValue, mergeCspDirectives, resolveCspConfig, serializeCspDirectives, validateCspConfigAtBoot, validateCspDirectives } from "./csp.mjs";
|
|
7
|
+
import { HttpTracingConfig, TracingContext, TracingHooks, TracingPhaseInfo, TracingRequestEndInfo } from "./tracing/tracing.type.mjs";
|
|
8
|
+
import { DecodedAccessToken, HttpConfigurations, PartialMiddleware, RequestEvent, RequestLocals, ResponseEvent, ResponseSSEController, ResponseStreamController, ReturnedResponse } from "./types.mjs";
|
|
6
9
|
import { defaultHttpConfigurations, httpConfig } from "./config.mjs";
|
|
7
10
|
import { createHttpApplication, stopHttpApplication } from "./createHttpApplication.mjs";
|
|
8
11
|
import { RequestLog } from "./database/RequestLog.mjs";
|
|
@@ -11,7 +14,7 @@ import { logResponse, wrapResponseInDataKey } from "./events.mjs";
|
|
|
11
14
|
import { HealthCheck, HealthStatus, health } from "./health.mjs";
|
|
12
15
|
import { RequestController } from "./request-controller.mjs";
|
|
13
16
|
import { UPLOADS_DEFAULTS, uploadsConfig } from "./uploads-config.mjs";
|
|
14
|
-
import { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
|
|
17
|
+
import { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
|
|
15
18
|
import { CacheMiddlewareOptions } from "./middleware/cache-response-middleware.mjs";
|
|
16
19
|
import { ConcurrencyLimitOptions } from "./middleware/concurrency-limit.middleware.mjs";
|
|
17
20
|
import { IdempotencyOptions } from "./middleware/idempotency.middleware.mjs";
|
|
@@ -28,5 +31,7 @@ import { registerHttpPlugins } from "./plugins.mjs";
|
|
|
28
31
|
import { assertConfiguredHttpPortIsFree, preflightConfiguredHttpPort, remedyLines, shouldPreflightHttpPort } from "./boot-port-preflight.mjs";
|
|
29
32
|
import { PortInUseError, assertPortIsAvailable, isPortAvailable } from "./port-preflight.mjs";
|
|
30
33
|
import { HttpReadyReport, getHttpReadyReport, resetHttpReadyReport, setHttpReadyReport } from "./ready-report.mjs";
|
|
34
|
+
import { deriveTraceId, parseTraceparentTraceId } from "./tracing/trace-id.mjs";
|
|
35
|
+
import { TracingContextSource, buildTracingContext, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, isTracingEnabled, resetTracingConfigForTests, resolveTracingConfig } from "./tracing/tracing-dispatcher.mjs";
|
|
31
36
|
import { RequestContextStore, requestContext, useCurrentUser, useRequest, useRequestStore } from "./context/request-context.mjs";
|
|
32
37
|
import { requestMemo } from "./context/request-memo.mjs";
|
package/esm/http/index.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { requestContext, useCurrentUser, useRequest, useRequestStore } from "./context/request-context.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_CSP_DIRECTIVES, InvalidCspDirectiveError, applyCspHeader, buildCspHeaderValue, mergeCspDirectives, resolveCspConfig, serializeCspDirectives, validateCspConfigAtBoot, validateCspDirectives } from "./csp.mjs";
|
|
3
|
+
import { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
|
|
4
|
+
import { deriveTraceId, parseTraceparentTraceId } from "./tracing/trace-id.mjs";
|
|
5
|
+
import { buildTracingContext, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, isTracingEnabled, resetTracingConfigForTests, resolveTracingConfig } from "./tracing/tracing-dispatcher.mjs";
|
|
6
|
+
import "./tracing/index.mjs";
|
|
3
7
|
import { createRequestStore, t } from "./middleware/inject-request-context.mjs";
|
|
4
8
|
import { defaultHttpConfigurations, httpConfig } from "./config.mjs";
|
|
9
|
+
import { streamReactResponse } from "./stream-react-response.mjs";
|
|
5
10
|
import { Response, ResponseStatus } from "./response.mjs";
|
|
6
11
|
import { UPLOADS_DEFAULTS, uploadsConfig } from "./uploads-config.mjs";
|
|
7
12
|
import { UploadedFile } from "./uploaded-file.mjs";
|
|
@@ -11,7 +11,7 @@ type ConcurrencyLimitOptions = {
|
|
|
11
11
|
* or per-tenant.
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
|
-
* keyGenerator: (request) => `${request.route.path}:${request.user?.id ?? request.ip}`,
|
|
14
|
+
* keyGenerator: (request) => `${request.route.path}:${request.locals.user?.id ?? request.ip}`,
|
|
15
15
|
*/
|
|
16
16
|
keyGenerator?: (request: Request) => string;
|
|
17
17
|
/**
|
|
@@ -42,7 +42,7 @@ type ConcurrencyLimitOptions = {
|
|
|
42
42
|
* router.post("/ai/summarize", summarizeController, {
|
|
43
43
|
* middleware: [
|
|
44
44
|
* middleware.concurrencyLimit(10, {
|
|
45
|
-
* keyGenerator: (request) => `ai:${request.user?.id ?? request.ip}`,
|
|
45
|
+
* keyGenerator: (request) => `ai:${request.locals.user?.id ?? request.ip}`,
|
|
46
46
|
* }),
|
|
47
47
|
* ],
|
|
48
48
|
* });
|
|
@@ -34,7 +34,7 @@ function release(key) {
|
|
|
34
34
|
* router.post("/ai/summarize", summarizeController, {
|
|
35
35
|
* middleware: [
|
|
36
36
|
* middleware.concurrencyLimit(10, {
|
|
37
|
-
* keyGenerator: (request) => `ai:${request.user?.id ?? request.ip}`,
|
|
37
|
+
* keyGenerator: (request) => `ai:${request.locals.user?.id ?? request.ip}`,
|
|
38
38
|
* }),
|
|
39
39
|
* ],
|
|
40
40
|
* });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"concurrency-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/concurrency-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the concurrency limit middleware.\n */\nexport type ConcurrencyLimitOptions = {\n /**\n * Group key generator. Defaults to `request.route.path` — i.e. the cap\n * applies across all callers of that route. Override to scope per-user\n * or per-tenant.\n *\n * @example\n * keyGenerator: (request) => `${request.route.path}:${request.user?.id ?? request.ip}`,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nconst counters = new Map<string, number>();\n\nfunction release(key: string) {\n const after = (counters.get(key) || 1) - 1;\n\n if (after <= 0) {\n counters.delete(key);\n\n return;\n }\n\n counters.set(key, after);\n}\n\n/**\n * Cap the number of in-flight requests against a route. Above the cap, new\n * requests get a fast 429 + `Retry-After: 1` — no queue, no timeout.\n *\n * Use for endpoints whose cost is unbounded per-request: report generation,\n * AI completions, image processing, expensive aggregations. Different from\n * `rateLimit()` — rate-limit caps requests-per-time, concurrency caps\n * in-flight requests at any instant.\n *\n * The counter is process-local. With `N` replicas the effective cap is\n * `N × max`. Document this in the route's behavior; if you need shared\n * concurrency across replicas, reach for a `@warlock.js/cache` lock instead.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/reports/generate\", reportController, {\n * middleware: [middleware.concurrencyLimit(3)],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * middleware.concurrencyLimit(10, {\n * keyGenerator: (request) => `ai:${request.user?.id ?? request.ip}`,\n * }),\n * ],\n * });\n */\nexport function concurrencyLimitMiddleware(\n max: number,\n options: ConcurrencyLimitOptions = {},\n): Middleware {\n return ({ request, response }) => {\n const key = options.keyGenerator?.(request) || request.route.path;\n const current = counters.get(key) || 0;\n\n if (current >= max) {\n response.header(\"Retry-After\", 1);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.concurrencyLimitReached\"),\n errorCode: HttpErrorCodes.ConcurrencyLimitReached,\n limit: max,\n });\n }\n\n counters.set(key, current + 1);\n\n let released = false;\n\n const releaseOnce = () => {\n if (released) {\n return;\n }\n\n released = true;\n release(key);\n };\n\n // `onSent` only fires from Response.send()/stream().end()/sse().end().\n // Bare-reply paths (noContent, redirect, sendFile, download, sendBuffer,\n // raw send) never trigger it, so the slot would leak and the route would\n // permanently 429 after `max` such requests. Bind to the raw socket\n // lifecycle as well so the slot frees regardless of response path —\n // `releaseOnce` is idempotent, so double-firing is harmless.\n response.onSent((_sentResponse: Response) => releaseOnce());\n response.baseResponse.raw.once(\"finish\", releaseOnce);\n response.baseResponse.raw.once(\"close\", releaseOnce);\n };\n}\n"],"mappings":";;;;AAyBA,MAAM,2BAAW,IAAI,IAAoB;AAEzC,SAAS,QAAQ,KAAa;CAC5B,MAAM,SAAS,SAAS,IAAI,GAAG,KAAK,KAAK;CAEzC,IAAI,SAAS,GAAG;EACd,SAAS,OAAO,GAAG;EAEnB;CACF;CAEA,SAAS,IAAI,KAAK,KAAK;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,2BACd,KACA,UAAmC,CAAC,GACxB;CACZ,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,QAAQ,eAAe,OAAO,KAAK,QAAQ,MAAM;EAC7D,MAAM,UAAU,SAAS,IAAI,GAAG,KAAK;EAErC,IAAI,WAAW,KAAK;GAClB,SAAS,OAAO,eAAe,CAAC;GAEhC,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,8BAA8B;IAC/D;IACA,OAAO;GACT,CAAC;EACH;EAEA,SAAS,IAAI,KAAK,UAAU,CAAC;EAE7B,IAAI,WAAW;EAEf,MAAM,oBAAoB;GACxB,IAAI,UACF;GAGF,WAAW;GACX,QAAQ,GAAG;EACb;EAQA,SAAS,QAAQ,kBAA4B,YAAY,CAAC;EAC1D,SAAS,aAAa,IAAI,KAAK,UAAU,WAAW;EACpD,SAAS,aAAa,IAAI,KAAK,SAAS,WAAW;CACrD;AACF"}
|
|
1
|
+
{"version":3,"file":"concurrency-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/concurrency-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the concurrency limit middleware.\n */\nexport type ConcurrencyLimitOptions = {\n /**\n * Group key generator. Defaults to `request.route.path` — i.e. the cap\n * applies across all callers of that route. Override to scope per-user\n * or per-tenant.\n *\n * @example\n * keyGenerator: (request) => `${request.route.path}:${request.locals.user?.id ?? request.ip}`,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nconst counters = new Map<string, number>();\n\nfunction release(key: string) {\n const after = (counters.get(key) || 1) - 1;\n\n if (after <= 0) {\n counters.delete(key);\n\n return;\n }\n\n counters.set(key, after);\n}\n\n/**\n * Cap the number of in-flight requests against a route. Above the cap, new\n * requests get a fast 429 + `Retry-After: 1` — no queue, no timeout.\n *\n * Use for endpoints whose cost is unbounded per-request: report generation,\n * AI completions, image processing, expensive aggregations. Different from\n * `rateLimit()` — rate-limit caps requests-per-time, concurrency caps\n * in-flight requests at any instant.\n *\n * The counter is process-local. With `N` replicas the effective cap is\n * `N × max`. Document this in the route's behavior; if you need shared\n * concurrency across replicas, reach for a `@warlock.js/cache` lock instead.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/reports/generate\", reportController, {\n * middleware: [middleware.concurrencyLimit(3)],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * middleware.concurrencyLimit(10, {\n * keyGenerator: (request) => `ai:${request.locals.user?.id ?? request.ip}`,\n * }),\n * ],\n * });\n */\nexport function concurrencyLimitMiddleware(\n max: number,\n options: ConcurrencyLimitOptions = {},\n): Middleware {\n return ({ request, response }) => {\n const key = options.keyGenerator?.(request) || request.route.path;\n const current = counters.get(key) || 0;\n\n if (current >= max) {\n response.header(\"Retry-After\", 1);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.concurrencyLimitReached\"),\n errorCode: HttpErrorCodes.ConcurrencyLimitReached,\n limit: max,\n });\n }\n\n counters.set(key, current + 1);\n\n let released = false;\n\n const releaseOnce = () => {\n if (released) {\n return;\n }\n\n released = true;\n release(key);\n };\n\n // `onSent` only fires from Response.send()/stream().end()/sse().end().\n // Bare-reply paths (noContent, redirect, sendFile, download, sendBuffer,\n // raw send) never trigger it, so the slot would leak and the route would\n // permanently 429 after `max` such requests. Bind to the raw socket\n // lifecycle as well so the slot frees regardless of response path —\n // `releaseOnce` is idempotent, so double-firing is harmless.\n response.onSent((_sentResponse: Response) => releaseOnce());\n response.baseResponse.raw.once(\"finish\", releaseOnce);\n response.baseResponse.raw.once(\"close\", releaseOnce);\n };\n}\n"],"mappings":";;;;AAyBA,MAAM,2BAAW,IAAI,IAAoB;AAEzC,SAAS,QAAQ,KAAa;CAC5B,MAAM,SAAS,SAAS,IAAI,GAAG,KAAK,KAAK;CAEzC,IAAI,SAAS,GAAG;EACd,SAAS,OAAO,GAAG;EAEnB;CACF;CAEA,SAAS,IAAI,KAAK,KAAK;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,2BACd,KACA,UAAmC,CAAC,GACxB;CACZ,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,QAAQ,eAAe,OAAO,KAAK,QAAQ,MAAM;EAC7D,MAAM,UAAU,SAAS,IAAI,GAAG,KAAK;EAErC,IAAI,WAAW,KAAK;GAClB,SAAS,OAAO,eAAe,CAAC;GAEhC,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,8BAA8B;IAC/D;IACA,OAAO;GACT,CAAC;EACH;EAEA,SAAS,IAAI,KAAK,UAAU,CAAC;EAE7B,IAAI,WAAW;EAEf,MAAM,oBAAoB;GACxB,IAAI,UACF;GAGF,WAAW;GACX,QAAQ,GAAG;EACb;EAQA,SAAS,QAAQ,kBAA4B,YAAY,CAAC;EAC1D,SAAS,aAAa,IAAI,KAAK,UAAU,WAAW;EACpD,SAAS,aAAa,IAAI,KAAK,SAAS,WAAW;CACrD;AACF"}
|
|
@@ -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":";;;;;;;AA4CA;;;iBAAgB,kBAAA,CACd,OAAA,EAAS,OAAA,OACT,QAAA,EAAU,QAAA,GACT,OAAA,CAAQ,gBAAA;;;;iBAkHK,CAAA,CAAE,OAAA,UAAiB,YAAkB"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { environment } from "../../utils/environment.mjs";
|
|
2
2
|
import { requestContext } from "../context/request-context.mjs";
|
|
3
3
|
import "../../utils/index.mjs";
|
|
4
|
+
import { applyCspHeader } from "../csp.mjs";
|
|
4
5
|
import { HttpError } from "../errors/errors.mjs";
|
|
6
|
+
import { buildTracingContext, dispatchPhase, isTracingEnabled } from "../tracing/tracing-dispatcher.mjs";
|
|
7
|
+
import "../tracing/index.mjs";
|
|
5
8
|
import config from "@mongez/config";
|
|
6
9
|
import { trans } from "@mongez/localization";
|
|
7
10
|
import { DatabaseWriterValidationError } from "@warlock.js/cascade";
|
|
@@ -34,6 +37,7 @@ function stampRequestIdHeader(request, response) {
|
|
|
34
37
|
*/
|
|
35
38
|
function createRequestStore(request, response) {
|
|
36
39
|
stampRequestIdHeader(request, response);
|
|
40
|
+
applyCspHeader(request, response);
|
|
37
41
|
const httpContextStore = contextManager.buildStores({
|
|
38
42
|
request,
|
|
39
43
|
response
|
|
@@ -45,10 +49,16 @@ function createRequestStore(request, response) {
|
|
|
45
49
|
request.trigger("executingAction", request.route);
|
|
46
50
|
const handler = request.getHandler();
|
|
47
51
|
request.log("Executing Handler", "info");
|
|
52
|
+
const tracingEnabled = isTracingEnabled();
|
|
53
|
+
const handlerStartedAt = tracingEnabled ? performance.now() : 0;
|
|
48
54
|
const output = await handler({
|
|
49
55
|
request,
|
|
50
56
|
response
|
|
51
57
|
});
|
|
58
|
+
if (tracingEnabled) dispatchPhase(buildTracingContext(request), {
|
|
59
|
+
name: "handler",
|
|
60
|
+
durationMs: performance.now() - handlerStartedAt
|
|
61
|
+
});
|
|
52
62
|
request.log("Handler Executed Successfully", "success");
|
|
53
63
|
request.trigger("executedAction", request.route);
|
|
54
64
|
return output;
|
|
@@ -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 { HttpError } from \"../errors\";\nimport { type Request } from \"../request\";\nimport { type Response } from \"../response\";\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 // 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 output = await handler({ request, response });\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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,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;GAEF,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"}
|
|
@@ -18,7 +18,7 @@ type RateLimitOptions = {
|
|
|
18
18
|
* per-organization, per-tenant, etc.
|
|
19
19
|
*
|
|
20
20
|
* @example
|
|
21
|
-
* keyGenerator: (request) => request.user?.id ?? request.ip,
|
|
21
|
+
* keyGenerator: (request) => request.locals.user?.id ?? request.ip,
|
|
22
22
|
*/
|
|
23
23
|
keyGenerator?: (request: Request) => string;
|
|
24
24
|
/**
|