@webpieces/core-context 0.4.597 → 0.4.599
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/package.json +2 -2
- package/src/PendingWireTrust.d.ts +74 -0
- package/src/PendingWireTrust.js +100 -0
- package/src/PendingWireTrust.js.map +1 -0
- package/src/RequestContext.d.ts +108 -19
- package/src/RequestContext.js +155 -42
- package/src/RequestContext.js.map +1 -1
- package/src/RequestContextApiCallContext.d.ts +3 -3
- package/src/RequestContextApiCallContext.js +2 -2
- package/src/RequestContextApiCallContext.js.map +1 -1
- package/src/RequestContextHeaders.d.ts +17 -0
- package/src/RequestContextHeaders.js +36 -11
- package/src/RequestContextHeaders.js.map +1 -1
- package/src/RequestContextReader.js +6 -4
- package/src/RequestContextReader.js.map +1 -1
- package/src/RequestContextTrustCompileAssertions.d.ts +27 -0
- package/src/RequestContextTrustCompileAssertions.js +56 -0
- package/src/RequestContextTrustCompileAssertions.js.map +1 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +4 -1
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/core-context",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.599",
|
|
4
4
|
"description": "AsyncLocalStorage-based context management for request-scoped data",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/core-util": "0.4.
|
|
25
|
+
"@webpieces/core-util": "0.4.599",
|
|
26
26
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
27
27
|
"inversify": "7.10.4",
|
|
28
28
|
"reflect-metadata": "0.2.2"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AnyTrustedContextKey } from '@webpieces/core-util';
|
|
2
|
+
/**
|
|
3
|
+
* One trusted key that ARRIVED ON THE WIRE and has not yet been vouched for. Data-only (a class,
|
|
4
|
+
* per the guidelines). Carries the key as well as the value so a rejection message can name the
|
|
5
|
+
* HTTP header the caller actually sent, not just the context name.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PendingTrustedValue {
|
|
8
|
+
readonly key: AnyTrustedContextKey;
|
|
9
|
+
readonly value: string;
|
|
10
|
+
constructor(key: AnyTrustedContextKey, value: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* PendingWireTrust - the holding pen between "a trusted key arrived on the wire" and "we know
|
|
14
|
+
* whether we may believe it".
|
|
15
|
+
*
|
|
16
|
+
* ## The hole this closes
|
|
17
|
+
*
|
|
18
|
+
* `RequestContextHeaders.fillFromRequest` runs at the TRANSPORT level (`ExpressWrapper`), and
|
|
19
|
+
* `AuthFilter` runs later, as a filter. So the wire always gets to write first and the authenticator
|
|
20
|
+
* second. If the inbound loop wrote trusted keys straight into the context, then for the entire
|
|
21
|
+
* window before AuthFilter ran — and forever after, on any route where the authenticator does not
|
|
22
|
+
* happen to stamp that particular key — a value typed by whoever sent the request would be sitting
|
|
23
|
+
* in the slot that `getTrusted` reads. `curl -H 'x-user-id: victim'` would be an authenticated
|
|
24
|
+
* identity.
|
|
25
|
+
*
|
|
26
|
+
* That is not hypothetical on this codebase: the framework's own {@link DefaultJwtHook} returns an
|
|
27
|
+
* EMPTY `AuthValues.entries`, so a fully verified `@AuthJwt` request stamps no context entries at
|
|
28
|
+
* all and would leave a forged `x-user-id` completely unopposed.
|
|
29
|
+
*
|
|
30
|
+
* ## The fix
|
|
31
|
+
*
|
|
32
|
+
* Inbound trusted values never enter the context. They are stashed HERE, and `AuthFilter` decides
|
|
33
|
+
* what to do with them once it knows who the caller is:
|
|
34
|
+
*
|
|
35
|
+
* - `@AuthOidc` / `@AuthSharedSecret` — the CALLER's own identity was verified, so this is an
|
|
36
|
+
* internal service passing along context it already holds. Admit the pending values as trusted.
|
|
37
|
+
* This is what makes service-to-service propagation of a verified userId work, and it is the whole
|
|
38
|
+
* reason trusted keys are allowed to have an `httpHeader` at all.
|
|
39
|
+
* - `@AuthJwt` / public — the caller may be a browser or anyone with curl. A pending value is
|
|
40
|
+
* admitted ONLY if the authenticator independently derived the SAME value. Anything else — a
|
|
41
|
+
* different value, or a value nothing vouched for — rejects the request.
|
|
42
|
+
*
|
|
43
|
+
* Note the deliberate asymmetry with a "strip it and carry on" design: a mismatch is not merely
|
|
44
|
+
* neutralized, it FAILS. Rate limiters commonly bucket on the inbound header rather than on the JWT,
|
|
45
|
+
* so a request whose header says `alice` and whose JWT says `bob` was rate-limited as the wrong
|
|
46
|
+
* principal. Letting the JWT quietly win would turn every forged header into a free rate-limit
|
|
47
|
+
* bypass. There is no legitimate caller that sends a header contradicting its own credential.
|
|
48
|
+
*
|
|
49
|
+
* ## If nothing reconciles
|
|
50
|
+
*
|
|
51
|
+
* `AuthFilter` is auto-installed on every route, so reconciliation always happens on a webpieces
|
|
52
|
+
* server. Should a non-webpieces transport ever call `fillFromRequest` without one, the pending
|
|
53
|
+
* values simply never arrive — the trusted key reads as absent. That is the fail-SAFE direction, and
|
|
54
|
+
* it is intentional: silence beats an unvouched value.
|
|
55
|
+
*
|
|
56
|
+
* Module-global instance (like {@link RequestContext} itself) rather than a DI singleton — it is
|
|
57
|
+
* stateless plumbing over the ambient context, used by one class on each side of the boundary.
|
|
58
|
+
*/
|
|
59
|
+
declare class PendingWireTrustImpl {
|
|
60
|
+
/**
|
|
61
|
+
* Hold an inbound wire value for a trusted key. Called by the inbound fill INSTEAD of writing it
|
|
62
|
+
* into the context.
|
|
63
|
+
*/
|
|
64
|
+
stash(key: AnyTrustedContextKey, value: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Everything stashed for this request, and CLEAR the pen in the same step — so reconciliation
|
|
67
|
+
* cannot run twice and a value cannot be re-admitted later by anything else.
|
|
68
|
+
*/
|
|
69
|
+
takeAll(): PendingTrustedValue[];
|
|
70
|
+
private current;
|
|
71
|
+
}
|
|
72
|
+
/** The process-wide holding pen. See {@link PendingWireTrustImpl}. */
|
|
73
|
+
export declare const PendingWireTrust: PendingWireTrustImpl;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PendingWireTrust = exports.PendingTrustedValue = void 0;
|
|
4
|
+
const RequestContext_1 = require("./RequestContext");
|
|
5
|
+
/**
|
|
6
|
+
* Reserved (deliberately UNREGISTERED) slot holding the pending map. Not a ContextKey: it is
|
|
7
|
+
* framework plumbing that exists only between the inbound fill and the AuthFilter, and giving it a
|
|
8
|
+
* ContextKey would make it transferrable/loggable, neither of which it should ever be.
|
|
9
|
+
*/
|
|
10
|
+
const PENDING_WIRE_TRUST_KEY = '__webpieces_pending_wire_trust__';
|
|
11
|
+
/**
|
|
12
|
+
* One trusted key that ARRIVED ON THE WIRE and has not yet been vouched for. Data-only (a class,
|
|
13
|
+
* per the guidelines). Carries the key as well as the value so a rejection message can name the
|
|
14
|
+
* HTTP header the caller actually sent, not just the context name.
|
|
15
|
+
*/
|
|
16
|
+
class PendingTrustedValue {
|
|
17
|
+
key;
|
|
18
|
+
value;
|
|
19
|
+
constructor(key, value) {
|
|
20
|
+
this.key = key;
|
|
21
|
+
this.value = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.PendingTrustedValue = PendingTrustedValue;
|
|
25
|
+
/**
|
|
26
|
+
* PendingWireTrust - the holding pen between "a trusted key arrived on the wire" and "we know
|
|
27
|
+
* whether we may believe it".
|
|
28
|
+
*
|
|
29
|
+
* ## The hole this closes
|
|
30
|
+
*
|
|
31
|
+
* `RequestContextHeaders.fillFromRequest` runs at the TRANSPORT level (`ExpressWrapper`), and
|
|
32
|
+
* `AuthFilter` runs later, as a filter. So the wire always gets to write first and the authenticator
|
|
33
|
+
* second. If the inbound loop wrote trusted keys straight into the context, then for the entire
|
|
34
|
+
* window before AuthFilter ran — and forever after, on any route where the authenticator does not
|
|
35
|
+
* happen to stamp that particular key — a value typed by whoever sent the request would be sitting
|
|
36
|
+
* in the slot that `getTrusted` reads. `curl -H 'x-user-id: victim'` would be an authenticated
|
|
37
|
+
* identity.
|
|
38
|
+
*
|
|
39
|
+
* That is not hypothetical on this codebase: the framework's own {@link DefaultJwtHook} returns an
|
|
40
|
+
* EMPTY `AuthValues.entries`, so a fully verified `@AuthJwt` request stamps no context entries at
|
|
41
|
+
* all and would leave a forged `x-user-id` completely unopposed.
|
|
42
|
+
*
|
|
43
|
+
* ## The fix
|
|
44
|
+
*
|
|
45
|
+
* Inbound trusted values never enter the context. They are stashed HERE, and `AuthFilter` decides
|
|
46
|
+
* what to do with them once it knows who the caller is:
|
|
47
|
+
*
|
|
48
|
+
* - `@AuthOidc` / `@AuthSharedSecret` — the CALLER's own identity was verified, so this is an
|
|
49
|
+
* internal service passing along context it already holds. Admit the pending values as trusted.
|
|
50
|
+
* This is what makes service-to-service propagation of a verified userId work, and it is the whole
|
|
51
|
+
* reason trusted keys are allowed to have an `httpHeader` at all.
|
|
52
|
+
* - `@AuthJwt` / public — the caller may be a browser or anyone with curl. A pending value is
|
|
53
|
+
* admitted ONLY if the authenticator independently derived the SAME value. Anything else — a
|
|
54
|
+
* different value, or a value nothing vouched for — rejects the request.
|
|
55
|
+
*
|
|
56
|
+
* Note the deliberate asymmetry with a "strip it and carry on" design: a mismatch is not merely
|
|
57
|
+
* neutralized, it FAILS. Rate limiters commonly bucket on the inbound header rather than on the JWT,
|
|
58
|
+
* so a request whose header says `alice` and whose JWT says `bob` was rate-limited as the wrong
|
|
59
|
+
* principal. Letting the JWT quietly win would turn every forged header into a free rate-limit
|
|
60
|
+
* bypass. There is no legitimate caller that sends a header contradicting its own credential.
|
|
61
|
+
*
|
|
62
|
+
* ## If nothing reconciles
|
|
63
|
+
*
|
|
64
|
+
* `AuthFilter` is auto-installed on every route, so reconciliation always happens on a webpieces
|
|
65
|
+
* server. Should a non-webpieces transport ever call `fillFromRequest` without one, the pending
|
|
66
|
+
* values simply never arrive — the trusted key reads as absent. That is the fail-SAFE direction, and
|
|
67
|
+
* it is intentional: silence beats an unvouched value.
|
|
68
|
+
*
|
|
69
|
+
* Module-global instance (like {@link RequestContext} itself) rather than a DI singleton — it is
|
|
70
|
+
* stateless plumbing over the ambient context, used by one class on each side of the boundary.
|
|
71
|
+
*/
|
|
72
|
+
class PendingWireTrustImpl {
|
|
73
|
+
/**
|
|
74
|
+
* Hold an inbound wire value for a trusted key. Called by the inbound fill INSTEAD of writing it
|
|
75
|
+
* into the context.
|
|
76
|
+
*/
|
|
77
|
+
stash(key, value) {
|
|
78
|
+
const pending = this.current() ?? new Map();
|
|
79
|
+
pending.set(key.name, new PendingTrustedValue(key, value));
|
|
80
|
+
RequestContext_1.RequestContext.put(PENDING_WIRE_TRUST_KEY, pending);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Everything stashed for this request, and CLEAR the pen in the same step — so reconciliation
|
|
84
|
+
* cannot run twice and a value cannot be re-admitted later by anything else.
|
|
85
|
+
*/
|
|
86
|
+
takeAll() {
|
|
87
|
+
const pending = this.current();
|
|
88
|
+
if (!pending) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
RequestContext_1.RequestContext.remove(PENDING_WIRE_TRUST_KEY);
|
|
92
|
+
return Array.from(pending.values());
|
|
93
|
+
}
|
|
94
|
+
current() {
|
|
95
|
+
return RequestContext_1.RequestContext.get(PENDING_WIRE_TRUST_KEY);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** The process-wide holding pen. See {@link PendingWireTrustImpl}. */
|
|
99
|
+
exports.PendingWireTrust = new PendingWireTrustImpl();
|
|
100
|
+
//# sourceMappingURL=PendingWireTrust.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PendingWireTrust.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/PendingWireTrust.ts"],"names":[],"mappings":";;;AACA,qDAAkD;AAElD;;;;GAIG;AACH,MAAM,sBAAsB,GAAG,kCAAkC,CAAC;AAElE;;;;GAIG;AACH,MAAa,mBAAmB;IAER;IACA;IAFpB,YACoB,GAAyB,EACzB,KAAa;QADb,QAAG,GAAH,GAAG,CAAsB;QACzB,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AALD,kDAKC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,oBAAoB;IACtB;;;OAGG;IACH,KAAK,CAAC,GAAyB,EAAE,KAAa;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,GAAG,EAA+B,CAAC;QACzE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QAC3D,+BAAc,CAAC,GAAG,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC;IACxD,CAAC;IAED;;;OAGG;IACH,OAAO;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/B,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QACD,+BAAc,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAC9C,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACxC,CAAC;IAEO,OAAO;QACX,OAAO,+BAAc,CAAC,GAAG,CAAmC,sBAAsB,CAAC,CAAC;IACxF,CAAC;CACJ;AAED,sEAAsE;AACzD,QAAA,gBAAgB,GAAG,IAAI,oBAAoB,EAAE,CAAC","sourcesContent":["import { AnyTrustedContextKey } from '@webpieces/core-util';\nimport { RequestContext } from './RequestContext';\n\n/**\n * Reserved (deliberately UNREGISTERED) slot holding the pending map. Not a ContextKey: it is\n * framework plumbing that exists only between the inbound fill and the AuthFilter, and giving it a\n * ContextKey would make it transferrable/loggable, neither of which it should ever be.\n */\nconst PENDING_WIRE_TRUST_KEY = '__webpieces_pending_wire_trust__';\n\n/**\n * One trusted key that ARRIVED ON THE WIRE and has not yet been vouched for. Data-only (a class,\n * per the guidelines). Carries the key as well as the value so a rejection message can name the\n * HTTP header the caller actually sent, not just the context name.\n */\nexport class PendingTrustedValue {\n constructor(\n public readonly key: AnyTrustedContextKey,\n public readonly value: string,\n ) {}\n}\n\n/**\n * PendingWireTrust - the holding pen between \"a trusted key arrived on the wire\" and \"we know\n * whether we may believe it\".\n *\n * ## The hole this closes\n *\n * `RequestContextHeaders.fillFromRequest` runs at the TRANSPORT level (`ExpressWrapper`), and\n * `AuthFilter` runs later, as a filter. So the wire always gets to write first and the authenticator\n * second. If the inbound loop wrote trusted keys straight into the context, then for the entire\n * window before AuthFilter ran — and forever after, on any route where the authenticator does not\n * happen to stamp that particular key — a value typed by whoever sent the request would be sitting\n * in the slot that `getTrusted` reads. `curl -H 'x-user-id: victim'` would be an authenticated\n * identity.\n *\n * That is not hypothetical on this codebase: the framework's own {@link DefaultJwtHook} returns an\n * EMPTY `AuthValues.entries`, so a fully verified `@AuthJwt` request stamps no context entries at\n * all and would leave a forged `x-user-id` completely unopposed.\n *\n * ## The fix\n *\n * Inbound trusted values never enter the context. They are stashed HERE, and `AuthFilter` decides\n * what to do with them once it knows who the caller is:\n *\n * - `@AuthOidc` / `@AuthSharedSecret` — the CALLER's own identity was verified, so this is an\n * internal service passing along context it already holds. Admit the pending values as trusted.\n * This is what makes service-to-service propagation of a verified userId work, and it is the whole\n * reason trusted keys are allowed to have an `httpHeader` at all.\n * - `@AuthJwt` / public — the caller may be a browser or anyone with curl. A pending value is\n * admitted ONLY if the authenticator independently derived the SAME value. Anything else — a\n * different value, or a value nothing vouched for — rejects the request.\n *\n * Note the deliberate asymmetry with a \"strip it and carry on\" design: a mismatch is not merely\n * neutralized, it FAILS. Rate limiters commonly bucket on the inbound header rather than on the JWT,\n * so a request whose header says `alice` and whose JWT says `bob` was rate-limited as the wrong\n * principal. Letting the JWT quietly win would turn every forged header into a free rate-limit\n * bypass. There is no legitimate caller that sends a header contradicting its own credential.\n *\n * ## If nothing reconciles\n *\n * `AuthFilter` is auto-installed on every route, so reconciliation always happens on a webpieces\n * server. Should a non-webpieces transport ever call `fillFromRequest` without one, the pending\n * values simply never arrive — the trusted key reads as absent. That is the fail-SAFE direction, and\n * it is intentional: silence beats an unvouched value.\n *\n * Module-global instance (like {@link RequestContext} itself) rather than a DI singleton — it is\n * stateless plumbing over the ambient context, used by one class on each side of the boundary.\n */\nclass PendingWireTrustImpl {\n /**\n * Hold an inbound wire value for a trusted key. Called by the inbound fill INSTEAD of writing it\n * into the context.\n */\n stash(key: AnyTrustedContextKey, value: string): void {\n const pending = this.current() ?? new Map<string, PendingTrustedValue>();\n pending.set(key.name, new PendingTrustedValue(key, value));\n RequestContext.put(PENDING_WIRE_TRUST_KEY, pending);\n }\n\n /**\n * Everything stashed for this request, and CLEAR the pen in the same step — so reconciliation\n * cannot run twice and a value cannot be re-admitted later by anything else.\n */\n takeAll(): PendingTrustedValue[] {\n const pending = this.current();\n if (!pending) {\n return [];\n }\n RequestContext.remove(PENDING_WIRE_TRUST_KEY);\n return Array.from(pending.values());\n }\n\n private current(): Map<string, PendingTrustedValue> | undefined {\n return RequestContext.get<Map<string, PendingTrustedValue>>(PENDING_WIRE_TRUST_KEY);\n }\n}\n\n/** The process-wide holding pen. See {@link PendingWireTrustImpl}. */\nexport const PendingWireTrust = new PendingWireTrustImpl();\n"]}
|
package/src/RequestContext.d.ts
CHANGED
|
@@ -32,29 +32,80 @@ declare class RequestContextImpl {
|
|
|
32
32
|
*/
|
|
33
33
|
run<T>(fn: () => T): T;
|
|
34
34
|
/**
|
|
35
|
-
* Run a function with a specific context
|
|
35
|
+
* Run a function with a specific context — the restore half of {@link copyContext}, used to carry
|
|
36
|
+
* a context across an async boundary (XPromise).
|
|
37
|
+
*
|
|
38
|
+
* FRAMEWORK-INTERNAL. Unlike the raw string accessors, this deliberately CANNOT be guarded against
|
|
39
|
+
* registered key names: a restored context legitimately contains trusted values, since restoring
|
|
40
|
+
* them is the entire purpose. So the guarantee here is narrower and worth stating plainly — the
|
|
41
|
+
* Map must be one this class produced via `copyContext()`, never one assembled by hand. Handing it
|
|
42
|
+
* a hand-built Map forges whatever it contains, and no type or check will stop you.
|
|
36
43
|
*/
|
|
37
44
|
runWithContext<T>(context: Map<string, any>, fn: () => T): T;
|
|
38
45
|
/**
|
|
39
|
-
* Read
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
46
|
+
* Read a value the framework PROVED — a verified JWT claim, or a fact an app derived from a
|
|
47
|
+
* verified credential. Does not compile for an untrusted key, so a reader can never mistake a
|
|
48
|
+
* caller-asserted value for an authenticated one.
|
|
49
|
+
*
|
|
50
|
+
* This is the ONLY read that is safe to feed into an authorization decision. If you find
|
|
51
|
+
* yourself wanting `getUntrusted` for that, the fix is to make the key trusted and have an
|
|
52
|
+
* authenticator vouch for it — not to use the other verb.
|
|
53
|
+
*
|
|
54
|
+
* The return type is the key's OWN value type `V` — `string` for wire/log keys, `ApiCallInfo`
|
|
55
|
+
* for the api tag, `TestCaseRecorder` for the recorder — INFERRED from the key, never asserted
|
|
56
|
+
* by the caller. This is the typed public surface over the deliberately type-erased backing Map.
|
|
43
57
|
*/
|
|
44
|
-
|
|
58
|
+
getTrusted<V>(key: ContextKey<V, 'trusted'>): V | undefined;
|
|
45
59
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
60
|
+
* Read a value a caller merely ASSERTED — a browser-minted actionId, a recording flag, an
|
|
61
|
+
* in-process log tag. Does not compile for a trusted key: reading a proven fact through the
|
|
62
|
+
* untrusted verb would under-claim and hide, at the call site, that the value IS reliable.
|
|
63
|
+
*
|
|
64
|
+
* Treat everything this returns as attacker-controlled. It is fine for logging, tracing,
|
|
65
|
+
* routing hints and rate-limit bucketing; it is never an input to "may they do this?".
|
|
48
66
|
*/
|
|
49
|
-
|
|
50
|
-
/** Clear one context key. Used by the api-tag seam's set → log → remove span (see LogApiCall). */
|
|
51
|
-
removeHeader(key: AnyContextKey): void;
|
|
52
|
-
hasHeader(key: AnyContextKey): boolean;
|
|
67
|
+
getUntrusted<V>(key: ContextKey<V, 'untrusted'>): V | undefined;
|
|
53
68
|
/**
|
|
69
|
+
* Store a value the framework PROVED. A distinct, greppable verb precisely so that writing a
|
|
70
|
+
* trusted value is something code has to do ON PURPOSE — `grep -rn putTrusted` lists every place
|
|
71
|
+
* in the repo that claims to have proven something, which is a reviewable set.
|
|
72
|
+
*
|
|
73
|
+
* Callers are the framework `AuthFilter` (stamping {@link ContextTuple}s an app's JwtHook derived
|
|
74
|
+
* from a verified credential) and app code that has itself verified something out-of-band — the
|
|
75
|
+
* signed-webhook case: Twilio/WhatsApp proves the phone number, the app looks up the userId, and
|
|
76
|
+
* that userId is every bit as proven as a JWT claim.
|
|
77
|
+
*
|
|
78
|
+
* Does not compile for an untrusted key.
|
|
79
|
+
*/
|
|
80
|
+
putTrusted<V>(key: ContextKey<V, 'trusted'>, value: V): void;
|
|
81
|
+
/**
|
|
82
|
+
* Store a caller-asserted value. `value` is type-checked against the key's value type `V`, so you
|
|
83
|
+
* cannot put a number under a `ContextKey<string>` or a raw object under a typed key.
|
|
84
|
+
*
|
|
85
|
+
* Does not compile for a trusted key — which is what stops the inbound-header path, the api-tag
|
|
86
|
+
* seam and ordinary app code from being side doors that forge a trusted value.
|
|
87
|
+
*/
|
|
88
|
+
putUntrusted<V>(key: ContextKey<V, 'untrusted'>, value: V): void;
|
|
89
|
+
/**
|
|
90
|
+
* Read a key of ANY trust level and ANY value type, as `unknown`.
|
|
91
|
+
*
|
|
92
|
+
* FRAMEWORK SERIALIZATION ONLY — the log-field builders below, the outbound header builder, and
|
|
93
|
+
* the {@link ContextReader} seam. Those loop over `HeaderRegistry` key arrays that are mixed in
|
|
94
|
+
* both value type and trust, and they are not making a trust DECISION: they are copying values to
|
|
95
|
+
* a log line or to the wire.
|
|
96
|
+
*
|
|
97
|
+
* It is deliberately read-only and has no write twin. A `putAny` would re-open the exact hole the
|
|
98
|
+
* typed verbs close, because forging a trusted value is the dangerous direction; reading one
|
|
99
|
+
* without saying `getTrusted` only costs you the `unknown` return type.
|
|
100
|
+
*/
|
|
101
|
+
getAny(key: AnyContextKey): unknown;
|
|
102
|
+
/** Clear one context key. Used by the api-tag seam's set → log → remove span (see LogApiCall). */
|
|
103
|
+
removeKey(key: AnyContextKey): void;
|
|
104
|
+
hasKey(key: AnyContextKey): boolean;
|
|
54
105
|
/**
|
|
55
106
|
* Build the masked field map for LOGGING: every logged key in the global
|
|
56
107
|
* {@link HeaderRegistry} read straight from this context, secured values
|
|
57
|
-
* masked (via {@link ContextKey.
|
|
108
|
+
* masked (via {@link ContextKey.maskForLogs}), keyed by each key's `name`.
|
|
58
109
|
*
|
|
59
110
|
* Callers: RecordingFilter + NodeProxyClient.recordCall, which snapshot the context into a
|
|
60
111
|
* test FIXTURE. The @webpieces/winston and @webpieces/bunyan backends also stamp these fields
|
|
@@ -96,19 +147,47 @@ declare class RequestContextImpl {
|
|
|
96
147
|
setRequest(request: HttpRequest): void;
|
|
97
148
|
/** The current {@link HttpRequest}, or undefined if none was set for this context. */
|
|
98
149
|
getRequest(): HttpRequest | undefined;
|
|
99
|
-
getHeaders(keys: AnyContextKey[]): unknown[];
|
|
100
150
|
/**
|
|
101
|
-
* Store a value
|
|
151
|
+
* Store a value under a RAW STRING key — the escape hatch for the framework's own reserved,
|
|
152
|
+
* UNREGISTERED slots ('__webpieces_http_request__', the AuthFilter principal, the Cloud Tasks
|
|
153
|
+
* schedule frame). Those are internal plumbing, not context keys, so they have no ContextKey and
|
|
154
|
+
* no trust level.
|
|
155
|
+
*
|
|
156
|
+
* REJECTS any name that belongs to a registered {@link ContextKey}. Without that check this
|
|
157
|
+
* method is a complete bypass of the trust system — `put('userId', req.body.userId)` would forge
|
|
158
|
+
* a trusted value while never typing `putTrusted`, and an agent picks whatever compiles. The
|
|
159
|
+
* check is necessarily a RUNTIME one: the registry is populated at `configure()` time, so "is
|
|
160
|
+
* this string a registered key name" is not a fact a type can express.
|
|
161
|
+
*
|
|
162
|
+
* @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.
|
|
102
163
|
*/
|
|
103
164
|
put(key: string, value: any): void;
|
|
104
165
|
/**
|
|
105
|
-
* Retrieve a value
|
|
166
|
+
* Retrieve a value stored under a RAW STRING key. Same reserved-slot purpose, and the same
|
|
167
|
+
* rejection, as {@link put} — reading `get('userId')` would hand back a trusted value without the
|
|
168
|
+
* call site ever saying `getTrusted`, which is exactly the ambiguity this whole change removes.
|
|
169
|
+
*
|
|
170
|
+
* @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.
|
|
106
171
|
*/
|
|
107
172
|
get<T = any>(key: string): T | undefined;
|
|
108
173
|
/**
|
|
109
|
-
* Remove a value
|
|
174
|
+
* Remove a value stored under a RAW STRING key. Registered names are rejected here too: deleting
|
|
175
|
+
* a trusted key out from under a reader is a trust decision, so it goes through {@link removeKey}
|
|
176
|
+
* with the key in hand.
|
|
177
|
+
*
|
|
178
|
+
* @throws Error when `key` is a registered ContextKey name.
|
|
110
179
|
*/
|
|
111
180
|
remove(key: string): void;
|
|
181
|
+
/**
|
|
182
|
+
* The guard behind the three raw-string accessors above. Silent (a no-op) until
|
|
183
|
+
* `HeaderRegistry.configure(...)` has run, which is correct rather than lax: with no registry
|
|
184
|
+
* there are no registered keys, so there is no trusted value to launder.
|
|
185
|
+
*/
|
|
186
|
+
private rejectRegisteredName;
|
|
187
|
+
/** The type-erased read. Every typed verb above funnels here; nothing else reads the store. */
|
|
188
|
+
private readByName;
|
|
189
|
+
/** The type-erased write. Every typed verb above funnels here; nothing else writes the store. */
|
|
190
|
+
private writeByName;
|
|
112
191
|
/**
|
|
113
192
|
* Clear all values from the current context.
|
|
114
193
|
*/
|
|
@@ -119,8 +198,11 @@ declare class RequestContextImpl {
|
|
|
119
198
|
*/
|
|
120
199
|
copyContext(): Map<string, any>;
|
|
121
200
|
/**
|
|
122
|
-
* Set the entire context from a Map.
|
|
123
|
-
*
|
|
201
|
+
* Set the entire context from a Map. Used by XPromise to restore context.
|
|
202
|
+
*
|
|
203
|
+
* Same FRAMEWORK-INTERNAL caveat as {@link runWithContext}: the Map must have come from
|
|
204
|
+
* `copyContext()`. It cannot be trust-checked, because a faithful restore has to reinstate the
|
|
205
|
+
* trusted values the original scope had proven.
|
|
124
206
|
*/
|
|
125
207
|
setContext(context: Map<string, any>): void;
|
|
126
208
|
/**
|
|
@@ -130,6 +212,13 @@ declare class RequestContextImpl {
|
|
|
130
212
|
/**
|
|
131
213
|
* Check if a key exists in the context.
|
|
132
214
|
*/
|
|
215
|
+
/**
|
|
216
|
+
* Presence of a value under a RAW STRING key. Guarded like its three siblings: `has('userId')`
|
|
217
|
+
* alongside `hasKey(WebpiecesCoreHeaders.USER_ID)` would be a second spelling of one question,
|
|
218
|
+
* and the string form is the one that says nothing about whether the value can be believed.
|
|
219
|
+
*
|
|
220
|
+
* @throws Error when `key` is a registered ContextKey name.
|
|
221
|
+
*/
|
|
133
222
|
has(key: string): boolean;
|
|
134
223
|
/**
|
|
135
224
|
* Check if RequestContext is currently active.
|
package/src/RequestContext.js
CHANGED
|
@@ -48,39 +48,97 @@ class RequestContextImpl {
|
|
|
48
48
|
return this.storage.run(store, fn);
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Run a function with a specific context
|
|
51
|
+
* Run a function with a specific context — the restore half of {@link copyContext}, used to carry
|
|
52
|
+
* a context across an async boundary (XPromise).
|
|
53
|
+
*
|
|
54
|
+
* FRAMEWORK-INTERNAL. Unlike the raw string accessors, this deliberately CANNOT be guarded against
|
|
55
|
+
* registered key names: a restored context legitimately contains trusted values, since restoring
|
|
56
|
+
* them is the entire purpose. So the guarantee here is narrower and worth stating plainly — the
|
|
57
|
+
* Map must be one this class produced via `copyContext()`, never one assembled by hand. Handing it
|
|
58
|
+
* a hand-built Map forges whatever it contains, and no type or check will stop you.
|
|
52
59
|
*/
|
|
53
60
|
runWithContext(context, fn) {
|
|
54
61
|
return this.storage.run(context, fn);
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
57
|
-
* Read
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
64
|
+
* Read a value the framework PROVED — a verified JWT claim, or a fact an app derived from a
|
|
65
|
+
* verified credential. Does not compile for an untrusted key, so a reader can never mistake a
|
|
66
|
+
* caller-asserted value for an authenticated one.
|
|
67
|
+
*
|
|
68
|
+
* This is the ONLY read that is safe to feed into an authorization decision. If you find
|
|
69
|
+
* yourself wanting `getUntrusted` for that, the fix is to make the key trusted and have an
|
|
70
|
+
* authenticator vouch for it — not to use the other verb.
|
|
71
|
+
*
|
|
72
|
+
* The return type is the key's OWN value type `V` — `string` for wire/log keys, `ApiCallInfo`
|
|
73
|
+
* for the api tag, `TestCaseRecorder` for the recorder — INFERRED from the key, never asserted
|
|
74
|
+
* by the caller. This is the typed public surface over the deliberately type-erased backing Map.
|
|
61
75
|
*/
|
|
62
|
-
|
|
63
|
-
return this.
|
|
76
|
+
getTrusted(key) {
|
|
77
|
+
return this.readByName(key.name);
|
|
64
78
|
}
|
|
65
79
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
80
|
+
* Read a value a caller merely ASSERTED — a browser-minted actionId, a recording flag, an
|
|
81
|
+
* in-process log tag. Does not compile for a trusted key: reading a proven fact through the
|
|
82
|
+
* untrusted verb would under-claim and hide, at the call site, that the value IS reliable.
|
|
83
|
+
*
|
|
84
|
+
* Treat everything this returns as attacker-controlled. It is fine for logging, tracing,
|
|
85
|
+
* routing hints and rate-limit bucketing; it is never an input to "may they do this?".
|
|
68
86
|
*/
|
|
69
|
-
|
|
70
|
-
this.
|
|
87
|
+
getUntrusted(key) {
|
|
88
|
+
return this.readByName(key.name);
|
|
71
89
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Store a value the framework PROVED. A distinct, greppable verb precisely so that writing a
|
|
92
|
+
* trusted value is something code has to do ON PURPOSE — `grep -rn putTrusted` lists every place
|
|
93
|
+
* in the repo that claims to have proven something, which is a reviewable set.
|
|
94
|
+
*
|
|
95
|
+
* Callers are the framework `AuthFilter` (stamping {@link ContextTuple}s an app's JwtHook derived
|
|
96
|
+
* from a verified credential) and app code that has itself verified something out-of-band — the
|
|
97
|
+
* signed-webhook case: Twilio/WhatsApp proves the phone number, the app looks up the userId, and
|
|
98
|
+
* that userId is every bit as proven as a JWT claim.
|
|
99
|
+
*
|
|
100
|
+
* Does not compile for an untrusted key.
|
|
101
|
+
*/
|
|
102
|
+
putTrusted(key, value) {
|
|
103
|
+
this.writeByName(key.name, value);
|
|
75
104
|
}
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Store a caller-asserted value. `value` is type-checked against the key's value type `V`, so you
|
|
107
|
+
* cannot put a number under a `ContextKey<string>` or a raw object under a typed key.
|
|
108
|
+
*
|
|
109
|
+
* Does not compile for a trusted key — which is what stops the inbound-header path, the api-tag
|
|
110
|
+
* seam and ordinary app code from being side doors that forge a trusted value.
|
|
111
|
+
*/
|
|
112
|
+
putUntrusted(key, value) {
|
|
113
|
+
this.writeByName(key.name, value);
|
|
78
114
|
}
|
|
79
115
|
/**
|
|
116
|
+
* Read a key of ANY trust level and ANY value type, as `unknown`.
|
|
117
|
+
*
|
|
118
|
+
* FRAMEWORK SERIALIZATION ONLY — the log-field builders below, the outbound header builder, and
|
|
119
|
+
* the {@link ContextReader} seam. Those loop over `HeaderRegistry` key arrays that are mixed in
|
|
120
|
+
* both value type and trust, and they are not making a trust DECISION: they are copying values to
|
|
121
|
+
* a log line or to the wire.
|
|
122
|
+
*
|
|
123
|
+
* It is deliberately read-only and has no write twin. A `putAny` would re-open the exact hole the
|
|
124
|
+
* typed verbs close, because forging a trusted value is the dangerous direction; reading one
|
|
125
|
+
* without saying `getTrusted` only costs you the `unknown` return type.
|
|
126
|
+
*/
|
|
127
|
+
// webpieces-disable no-any-unknown -- key-agnostic serialization read: the key array is mixed in value type, so unknown is the honest return
|
|
128
|
+
getAny(key) {
|
|
129
|
+
return this.readByName(key.name);
|
|
130
|
+
}
|
|
131
|
+
/** Clear one context key. Used by the api-tag seam's set → log → remove span (see LogApiCall). */
|
|
132
|
+
removeKey(key) {
|
|
133
|
+
this.storage.getStore()?.delete(key.name);
|
|
134
|
+
}
|
|
135
|
+
hasKey(key) {
|
|
136
|
+
return this.storage.getStore()?.has(key.name) ?? false;
|
|
137
|
+
}
|
|
80
138
|
/**
|
|
81
139
|
* Build the masked field map for LOGGING: every logged key in the global
|
|
82
140
|
* {@link HeaderRegistry} read straight from this context, secured values
|
|
83
|
-
* masked (via {@link ContextKey.
|
|
141
|
+
* masked (via {@link ContextKey.maskForLogs}), keyed by each key's `name`.
|
|
84
142
|
*
|
|
85
143
|
* Callers: RecordingFilter + NodeProxyClient.recordCall, which snapshot the context into a
|
|
86
144
|
* test FIXTURE. The @webpieces/winston and @webpieces/bunyan backends also stamp these fields
|
|
@@ -101,11 +159,12 @@ class RequestContextImpl {
|
|
|
101
159
|
// typeof-string check; objects ride buildStructuredLogFields instead. (Was a HeaderRegistry
|
|
102
160
|
// method taking a read callback; only the server ever called it, so the seam was dead weight.)
|
|
103
161
|
for (const key of core_util_1.HeaderRegistry.get().getLoggedKeys()) {
|
|
104
|
-
// getLoggedKeys() is AnyContextKey[]
|
|
105
|
-
//
|
|
106
|
-
|
|
162
|
+
// getLoggedKeys() is AnyContextKey[] — mixed in BOTH value type and trust — so this reads
|
|
163
|
+
// through getAny (serialization, not a trust decision) and narrows with the typeof-string
|
|
164
|
+
// guard rather than asserting a value type per key.
|
|
165
|
+
const value = this.getAny(key);
|
|
107
166
|
if (typeof value === 'string' && value) {
|
|
108
|
-
fields.set(key.name, key.
|
|
167
|
+
fields.set(key.name, key.maskForLogs(value));
|
|
109
168
|
}
|
|
110
169
|
}
|
|
111
170
|
return fields;
|
|
@@ -156,13 +215,13 @@ class RequestContextImpl {
|
|
|
156
215
|
// masked per key; non-string primitives are ignored rather than String()-flattened. (Inlined
|
|
157
216
|
// from HeaderRegistry for the same reason as buildLogFields — only the server called it.)
|
|
158
217
|
for (const key of core_util_1.HeaderRegistry.get().getLoggedKeys()) {
|
|
159
|
-
const value = this.
|
|
218
|
+
const value = this.getAny(key);
|
|
160
219
|
if (value === undefined || value === null) {
|
|
161
220
|
continue;
|
|
162
221
|
}
|
|
163
222
|
if (typeof value === 'string') {
|
|
164
223
|
if (value) {
|
|
165
|
-
fields.set(key.name, key.
|
|
224
|
+
fields.set(key.name, key.maskForLogs(value));
|
|
166
225
|
}
|
|
167
226
|
}
|
|
168
227
|
else if (typeof value === 'object') {
|
|
@@ -184,33 +243,77 @@ class RequestContextImpl {
|
|
|
184
243
|
getRequest() {
|
|
185
244
|
return this.get(HTTP_REQUEST_KEY);
|
|
186
245
|
}
|
|
187
|
-
// webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)
|
|
188
|
-
getHeaders(keys) {
|
|
189
|
-
return keys.map(key => this.getHeader(key));
|
|
190
|
-
}
|
|
191
246
|
/**
|
|
192
|
-
* Store a value
|
|
247
|
+
* Store a value under a RAW STRING key — the escape hatch for the framework's own reserved,
|
|
248
|
+
* UNREGISTERED slots ('__webpieces_http_request__', the AuthFilter principal, the Cloud Tasks
|
|
249
|
+
* schedule frame). Those are internal plumbing, not context keys, so they have no ContextKey and
|
|
250
|
+
* no trust level.
|
|
251
|
+
*
|
|
252
|
+
* REJECTS any name that belongs to a registered {@link ContextKey}. Without that check this
|
|
253
|
+
* method is a complete bypass of the trust system — `put('userId', req.body.userId)` would forge
|
|
254
|
+
* a trusted value while never typing `putTrusted`, and an agent picks whatever compiles. The
|
|
255
|
+
* check is necessarily a RUNTIME one: the registry is populated at `configure()` time, so "is
|
|
256
|
+
* this string a registered key name" is not a fact a type can express.
|
|
257
|
+
*
|
|
258
|
+
* @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.
|
|
193
259
|
*/
|
|
260
|
+
// webpieces-disable no-any-unknown -- reserved-slot values are heterogeneous (HttpRequest, principal, schedule frame)
|
|
194
261
|
put(key, value) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
throw new Error('No context available. Did you call Context.run() first?');
|
|
198
|
-
}
|
|
199
|
-
store.set(key, value);
|
|
262
|
+
this.rejectRegisteredName(key, 'putTrusted / putUntrusted');
|
|
263
|
+
this.writeByName(key, value);
|
|
200
264
|
}
|
|
201
265
|
/**
|
|
202
|
-
* Retrieve a value
|
|
266
|
+
* Retrieve a value stored under a RAW STRING key. Same reserved-slot purpose, and the same
|
|
267
|
+
* rejection, as {@link put} — reading `get('userId')` would hand back a trusted value without the
|
|
268
|
+
* call site ever saying `getTrusted`, which is exactly the ambiguity this whole change removes.
|
|
269
|
+
*
|
|
270
|
+
* @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.
|
|
203
271
|
*/
|
|
272
|
+
// webpieces-disable no-any-unknown -- reserved-slot values are heterogeneous; callers name the concrete type
|
|
204
273
|
get(key) {
|
|
205
|
-
|
|
206
|
-
return
|
|
274
|
+
this.rejectRegisteredName(key, 'getTrusted / getUntrusted / getAny');
|
|
275
|
+
return this.readByName(key);
|
|
207
276
|
}
|
|
208
277
|
/**
|
|
209
|
-
* Remove a value
|
|
278
|
+
* Remove a value stored under a RAW STRING key. Registered names are rejected here too: deleting
|
|
279
|
+
* a trusted key out from under a reader is a trust decision, so it goes through {@link removeKey}
|
|
280
|
+
* with the key in hand.
|
|
281
|
+
*
|
|
282
|
+
* @throws Error when `key` is a registered ContextKey name.
|
|
210
283
|
*/
|
|
211
284
|
remove(key) {
|
|
285
|
+
this.rejectRegisteredName(key, 'removeKey(key)');
|
|
286
|
+
this.storage.getStore()?.delete(key);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The guard behind the three raw-string accessors above. Silent (a no-op) until
|
|
290
|
+
* `HeaderRegistry.configure(...)` has run, which is correct rather than lax: with no registry
|
|
291
|
+
* there are no registered keys, so there is no trusted value to launder.
|
|
292
|
+
*/
|
|
293
|
+
rejectRegisteredName(name, useInstead) {
|
|
294
|
+
if (!core_util_1.HeaderRegistry.isConfigured()) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const key = core_util_1.HeaderRegistry.get().findByName(name);
|
|
298
|
+
if (key) {
|
|
299
|
+
throw new Error(`RequestContext string accessors cannot touch '${name}' — it is a registered ` +
|
|
300
|
+
`ContextKey (trust: '${key.trust}'). The raw string form hides whether the value is ` +
|
|
301
|
+
`a proven fact or something a caller asserted, so it is a bypass of the trust ` +
|
|
302
|
+
`system. Use ${useInstead} with the ContextKey itself.`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** The type-erased read. Every typed verb above funnels here; nothing else reads the store. */
|
|
306
|
+
readByName(name) {
|
|
307
|
+
return this.storage.getStore()?.get(name);
|
|
308
|
+
}
|
|
309
|
+
/** The type-erased write. Every typed verb above funnels here; nothing else writes the store. */
|
|
310
|
+
// webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)
|
|
311
|
+
writeByName(name, value) {
|
|
212
312
|
const store = this.storage.getStore();
|
|
213
|
-
store
|
|
313
|
+
if (!store) {
|
|
314
|
+
throw new Error('No context available. Did you call Context.run() first?');
|
|
315
|
+
}
|
|
316
|
+
store.set(name, value);
|
|
214
317
|
}
|
|
215
318
|
/**
|
|
216
319
|
* Clear all values from the current context.
|
|
@@ -231,8 +334,11 @@ class RequestContextImpl {
|
|
|
231
334
|
return new Map(store);
|
|
232
335
|
}
|
|
233
336
|
/**
|
|
234
|
-
* Set the entire context from a Map.
|
|
235
|
-
*
|
|
337
|
+
* Set the entire context from a Map. Used by XPromise to restore context.
|
|
338
|
+
*
|
|
339
|
+
* Same FRAMEWORK-INTERNAL caveat as {@link runWithContext}: the Map must have come from
|
|
340
|
+
* `copyContext()`. It cannot be trust-checked, because a faithful restore has to reinstate the
|
|
341
|
+
* trusted values the original scope had proven.
|
|
236
342
|
*/
|
|
237
343
|
setContext(context) {
|
|
238
344
|
const store = this.storage.getStore();
|
|
@@ -254,9 +360,16 @@ class RequestContextImpl {
|
|
|
254
360
|
/**
|
|
255
361
|
* Check if a key exists in the context.
|
|
256
362
|
*/
|
|
363
|
+
/**
|
|
364
|
+
* Presence of a value under a RAW STRING key. Guarded like its three siblings: `has('userId')`
|
|
365
|
+
* alongside `hasKey(WebpiecesCoreHeaders.USER_ID)` would be a second spelling of one question,
|
|
366
|
+
* and the string form is the one that says nothing about whether the value can be believed.
|
|
367
|
+
*
|
|
368
|
+
* @throws Error when `key` is a registered ContextKey name.
|
|
369
|
+
*/
|
|
257
370
|
has(key) {
|
|
258
|
-
|
|
259
|
-
return
|
|
371
|
+
this.rejectRegisteredName(key, 'hasKey(key)');
|
|
372
|
+
return this.storage.getStore()?.has(key) ?? false;
|
|
260
373
|
}
|
|
261
374
|
/**
|
|
262
375
|
* Check if RequestContext is currently active.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequestContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContext.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'async_hooks';\nimport { ContextKey, AnyContextKey, HeaderRegistry, ServiceInfo } from '@webpieces/core-util';\nimport { HttpRequest } from './HttpRequest';\n\n/** Reserved context key under which the current HttpRequest is stored. */\nconst HTTP_REQUEST_KEY = '__webpieces_http_request__';\n\n/**\n * Context management using AsyncLocalStorage.\n * Similar to Java WebPieces Context class that uses ThreadLocal.\n *\n * This allows storing request-scoped data that is automatically available\n * throughout the async call chain, similar to MDC (Mapped Diagnostic Context).\n *\n * Example usage:\n * ```typescript\n * Context.put('REQUEST_ID', '12345');\n * await someAsyncOperation();\n * const id = Context.get('REQUEST_ID'); // Still available!\n * ```\n */\nclass RequestContextImpl {\n private storage: AsyncLocalStorage<Map<string, any>>;\n\n constructor() {\n this.storage = new AsyncLocalStorage<Map<string, any>>();\n }\n\n /**\n * Open THE request scope. A transport calls this once, at the beginning of a request.\n *\n * Nesting is a bug, not a feature, so it throws. AsyncLocalStorage would happily let a second\n * `run()` install a fresh empty Map that SHADOWS the outer one: every value the outer scope\n * holds becomes invisible, `fillFromRequest` mints a second request id, and the two halves of a\n * request end up in different traces. Nothing would tell you.\n *\n * With this guard the setup is right or it is loud. It mirrors\n * `RequestContextHeaders.fillFromRequest()`, which throws when there is NO active scope.\n *\n * @throws Error when a RequestContext is already active.\n */\n run<T>(fn: () => T): T {\n if (this.isActive()) {\n throw new Error(\n 'RequestContext.run(...) called inside an active RequestContext. Nesting installs a ' +\n 'fresh empty context that shadows the outer one: its values go invisible and a second ' +\n 'request id is minted. Exactly ONE scope per request — the transport opens it.',\n );\n }\n // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)\n const store = new Map<string, any>();\n return this.storage.run(store, fn);\n }\n\n /**\n * Run a function with a specific context.\n */\n runWithContext<T>(context: Map<string, any>, fn: () => T): T {\n return this.storage.run(context, fn);\n }\n\n /**\n * Read the value stored under a {@link ContextKey}. The return type is the key's OWN value type\n * `V` — `string` for wire/log keys, `ApiCallInfo` for the api tag, `TestCaseRecorder` for the\n * recorder — INFERRED from the key, never asserted by the caller. This is the typed public\n * surface over the deliberately type-erased backing Map.\n */\n getHeader<V>(key: ContextKey<V>): V | undefined {\n return this.get<V>(key.name);\n }\n\n /**\n * Store a value under a {@link ContextKey}. `value` is type-checked against the key's value type\n * `V`, so you cannot put a number under a `ContextKey<string>` or a raw object under a typed key.\n */\n putHeader<V>(key: ContextKey<V>, value: V): void {\n this.put(key.name, value);\n }\n\n /** Clear one context key. Used by the api-tag seam's set → log → remove span (see LogApiCall). */\n removeHeader(key: AnyContextKey): void {\n this.remove(key.name);\n }\n\n hasHeader(key: AnyContextKey): boolean {\n return this.has(key.name);\n }\n\n /**\n /**\n * Build the masked field map for LOGGING: every logged key in the global\n * {@link HeaderRegistry} read straight from this context, secured values\n * masked (via {@link ContextKey.maskIfSecured}), keyed by each key's `name`.\n *\n * Callers: RecordingFilter + NodeProxyClient.recordCall, which snapshot the context into a\n * test FIXTURE. The @webpieces/winston and @webpieces/bunyan backends also stamp these fields\n * onto every record, and they own the \"log emitted outside RequestContext.run(...)\" complaint —\n * reporting it HERE would recurse (the error line itself re-enters buildLogFields).\n *\n * Returns an EMPTY map outside a `run(...)` block rather than throwing: a fixture snapshot or a\n * log line is never worth crashing a request over.\n */\n buildLogFields(): Map<string, string> {\n const fields = new Map<string, string>();\n if (!this.isActive()) {\n return fields;\n }\n // The registry owns WHICH keys log (getLoggedKeys); we read each straight from THIS context\n // and each ContextKey masks its own secured value. String-only — this map feeds wire/MDC +\n // recorder fixtures — so an object-valued key (API_CALL_INFO) is guarded out by the\n // typeof-string check; objects ride buildStructuredLogFields instead. (Was a HeaderRegistry\n // method taking a read callback; only the server ever called it, so the seam was dead weight.)\n for (const key of HeaderRegistry.get().getLoggedKeys()) {\n // getLoggedKeys() is AnyContextKey[] (mixed value types), so read by name and\n // narrow with the typeof-string guard rather than asserting a value type per key.\n const value = this.get<string>(key.name);\n if (typeof value === 'string' && value) {\n fields.set(key.name, key.maskIfSecured(value));\n }\n }\n return fields;\n }\n\n /**\n * The STRUCTURED field map for the node logging backends: like {@link buildLogFields}, but values\n * may be OBJECTS, so an object-valued logged key ({@link WebpiecesCoreHeaders.API_CALL_INFO} holding\n * an {@link ApiCallInfo}) survives as an object and the winston/bunyan backends nest it into\n * `jsonPayload.api`. Reads values UNTYPED (not `<string>`) so the object comes through intact.\n *\n * Outside a `run(...)` block it returns just the `svcName` + `version` entries below (not a fully\n * empty map): a log line is never worth crashing over, and startup/background lines must still say\n * which service and build emitted them.\n *\n * PLUS this service's `svcName` and this build's `version` from {@link ServiceInfo}. Neither is a\n * {@link ContextKey} — they are process-global identity facts, added HERE (BEFORE the active-context\n * check) so EVERY log line of BOTH node backends (winston/bunyan read this one map) says which\n * service and build emitted it — request path, startup, and background jobs alike — with no\n * per-backend duplication. This is the SINGLE place both are stamped, keeping the two backends\n * symmetrical (jsonPayload.svcName + jsonPayload.version). Read via the non-throwing\n * {@link ServiceInfo.getName} / {@link ServiceInfo.getVersion}, so each is simply ABSENT until\n * `setInfo` has run — logging keeps working before the service is identified, then the fields start\n * appearing. Caller-set `svcName`/`version` headers (there are none by convention) would be\n * overwritten here; that is intentional — the ServiceInfo identity is authoritative.\n */\n buildStructuredLogFields(): Map<string, string | object> {\n const fields = new Map<string, string | object>();\n // This service's `svcName` + this build's `version` from ServiceInfo — NOT ContextKeys, they are\n // process-global identity facts. Added FIRST, BEFORE the active-context check, so they ride EVERY\n // line of both node backends (they read this one map) — including startup and background-job lines\n // emitted with NO active RequestContext. Treated identically and read per-record via the\n // non-throwing getters, so each is simply ABSENT until setInfo has run, then starts appearing —\n // even if setInfo runs after a backend was constructed. This is the ONE place both facts are\n // stamped, so winston and bunyan stay symmetrical (jsonPayload.svcName + jsonPayload.version).\n const svcName = ServiceInfo.getName();\n if (svcName) {\n fields.set('svcName', svcName);\n }\n const version = ServiceInfo.getVersion();\n if (version) {\n fields.set('version', version);\n }\n if (!this.isActive()) {\n return fields;\n }\n // Like buildLogFields, but values may be OBJECTS (API_CALL_INFO): read UNTYPED so the object\n // survives and winston/bunyan nest it into jsonPayload.<name>. Secured STRING values are still\n // masked per key; non-string primitives are ignored rather than String()-flattened. (Inlined\n // from HeaderRegistry for the same reason as buildLogFields — only the server called it.)\n for (const key of HeaderRegistry.get().getLoggedKeys()) {\n const value = this.getHeader(key);\n if (value === undefined || value === null) {\n continue;\n }\n if (typeof value === 'string') {\n if (value) {\n fields.set(key.name, key.maskIfSecured(value));\n }\n } else if (typeof value === 'object') {\n fields.set(key.name, value);\n }\n }\n return fields;\n }\n\n\n /**\n * Store the transport-neutral {@link HttpRequest} for this request. Called once, above the\n * api boundary, by whichever transport is driving the router (the express adapter, or the\n * in-process client). Filters/auth read it back via {@link getRequest} so they never touch\n * express — the same chain then runs over HTTP and in-process.\n */\n setRequest(request: HttpRequest): void {\n this.put(HTTP_REQUEST_KEY, request);\n }\n\n /** The current {@link HttpRequest}, or undefined if none was set for this context. */\n getRequest(): HttpRequest | undefined {\n return this.get<HttpRequest>(HTTP_REQUEST_KEY);\n }\n\n // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)\n getHeaders(keys: AnyContextKey[]): unknown[] {\n return keys.map(key => this.getHeader(key));\n }\n\n /**\n * Store a value in the current context.\n */\n put(key: string, value: any): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error('No context available. Did you call Context.run() first?');\n }\n store.set(key, value);\n }\n\n /**\n * Retrieve a value from the current context.\n */\n get<T = any>(key: string): T | undefined {\n const store = this.storage.getStore();\n return store?.get(key);\n }\n\n /**\n * Remove a value from the current context.\n */\n remove(key: string): void {\n const store = this.storage.getStore();\n store?.delete(key);\n }\n\n /**\n * Clear all values from the current context.\n */\n clear(): void {\n const store = this.storage.getStore();\n store?.clear();\n }\n\n /**\n * Copy the current context to a new Map.\n * Used by XPromise to preserve context across async boundaries.\n */\n copyContext(): Map<string, any> {\n const store = this.storage.getStore();\n if (!store) {\n return new Map();\n }\n return new Map(store);\n }\n\n /**\n * Set the entire context from a Map.\n * Used by XPromise to restore context.\n */\n setContext(context: Map<string, any>): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error('No context available. Did you call Context.run() first?');\n }\n store.clear();\n context.forEach((value, key) => {\n store.set(key, value);\n });\n }\n\n /**\n * Get all context entries.\n */\n getAll(): Map<string, any> {\n const store = this.storage.getStore();\n return store ? new Map(store) : new Map();\n }\n\n /**\n * Check if a key exists in the context.\n */\n has(key: string): boolean {\n const store = this.storage.getStore();\n return store?.has(key) ?? false;\n }\n\n /**\n * Check if RequestContext is currently active.\n * Returns true if we're inside a RequestContext.run() block, false otherwise.\n *\n * Useful for tests to verify context is set up before making API calls.\n */\n isActive(): boolean {\n return this.storage.getStore() !== undefined;\n }\n\n}\n\n\n\n/**\n * Global singleton instance of RequestContext.\n * Use this throughout your application.\n */\nexport const RequestContext = new RequestContextImpl();\n"]}
|
|
1
|
+
{"version":3,"file":"RequestContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContext.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'async_hooks';\nimport { ContextKey, AnyContextKey, HeaderRegistry, ServiceInfo } from '@webpieces/core-util';\nimport { HttpRequest } from './HttpRequest';\n\n/** Reserved context key under which the current HttpRequest is stored. */\nconst HTTP_REQUEST_KEY = '__webpieces_http_request__';\n\n/**\n * Context management using AsyncLocalStorage.\n * Similar to Java WebPieces Context class that uses ThreadLocal.\n *\n * This allows storing request-scoped data that is automatically available\n * throughout the async call chain, similar to MDC (Mapped Diagnostic Context).\n *\n * Example usage:\n * ```typescript\n * Context.put('REQUEST_ID', '12345');\n * await someAsyncOperation();\n * const id = Context.get('REQUEST_ID'); // Still available!\n * ```\n */\nclass RequestContextImpl {\n private storage: AsyncLocalStorage<Map<string, any>>;\n\n constructor() {\n this.storage = new AsyncLocalStorage<Map<string, any>>();\n }\n\n /**\n * Open THE request scope. A transport calls this once, at the beginning of a request.\n *\n * Nesting is a bug, not a feature, so it throws. AsyncLocalStorage would happily let a second\n * `run()` install a fresh empty Map that SHADOWS the outer one: every value the outer scope\n * holds becomes invisible, `fillFromRequest` mints a second request id, and the two halves of a\n * request end up in different traces. Nothing would tell you.\n *\n * With this guard the setup is right or it is loud. It mirrors\n * `RequestContextHeaders.fillFromRequest()`, which throws when there is NO active scope.\n *\n * @throws Error when a RequestContext is already active.\n */\n run<T>(fn: () => T): T {\n if (this.isActive()) {\n throw new Error(\n 'RequestContext.run(...) called inside an active RequestContext. Nesting installs a ' +\n 'fresh empty context that shadows the outer one: its values go invisible and a second ' +\n 'request id is minted. Exactly ONE scope per request — the transport opens it.',\n );\n }\n // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)\n const store = new Map<string, any>();\n return this.storage.run(store, fn);\n }\n\n /**\n * Run a function with a specific context — the restore half of {@link copyContext}, used to carry\n * a context across an async boundary (XPromise).\n *\n * FRAMEWORK-INTERNAL. Unlike the raw string accessors, this deliberately CANNOT be guarded against\n * registered key names: a restored context legitimately contains trusted values, since restoring\n * them is the entire purpose. So the guarantee here is narrower and worth stating plainly — the\n * Map must be one this class produced via `copyContext()`, never one assembled by hand. Handing it\n * a hand-built Map forges whatever it contains, and no type or check will stop you.\n */\n runWithContext<T>(context: Map<string, any>, fn: () => T): T {\n return this.storage.run(context, fn);\n }\n\n /**\n * Read a value the framework PROVED — a verified JWT claim, or a fact an app derived from a\n * verified credential. Does not compile for an untrusted key, so a reader can never mistake a\n * caller-asserted value for an authenticated one.\n *\n * This is the ONLY read that is safe to feed into an authorization decision. If you find\n * yourself wanting `getUntrusted` for that, the fix is to make the key trusted and have an\n * authenticator vouch for it — not to use the other verb.\n *\n * The return type is the key's OWN value type `V` — `string` for wire/log keys, `ApiCallInfo`\n * for the api tag, `TestCaseRecorder` for the recorder — INFERRED from the key, never asserted\n * by the caller. This is the typed public surface over the deliberately type-erased backing Map.\n */\n getTrusted<V>(key: ContextKey<V, 'trusted'>): V | undefined {\n return this.readByName<V>(key.name);\n }\n\n /**\n * Read a value a caller merely ASSERTED — a browser-minted actionId, a recording flag, an\n * in-process log tag. Does not compile for a trusted key: reading a proven fact through the\n * untrusted verb would under-claim and hide, at the call site, that the value IS reliable.\n *\n * Treat everything this returns as attacker-controlled. It is fine for logging, tracing,\n * routing hints and rate-limit bucketing; it is never an input to \"may they do this?\".\n */\n getUntrusted<V>(key: ContextKey<V, 'untrusted'>): V | undefined {\n return this.readByName<V>(key.name);\n }\n\n /**\n * Store a value the framework PROVED. A distinct, greppable verb precisely so that writing a\n * trusted value is something code has to do ON PURPOSE — `grep -rn putTrusted` lists every place\n * in the repo that claims to have proven something, which is a reviewable set.\n *\n * Callers are the framework `AuthFilter` (stamping {@link ContextTuple}s an app's JwtHook derived\n * from a verified credential) and app code that has itself verified something out-of-band — the\n * signed-webhook case: Twilio/WhatsApp proves the phone number, the app looks up the userId, and\n * that userId is every bit as proven as a JWT claim.\n *\n * Does not compile for an untrusted key.\n */\n putTrusted<V>(key: ContextKey<V, 'trusted'>, value: V): void {\n this.writeByName(key.name, value);\n }\n\n /**\n * Store a caller-asserted value. `value` is type-checked against the key's value type `V`, so you\n * cannot put a number under a `ContextKey<string>` or a raw object under a typed key.\n *\n * Does not compile for a trusted key — which is what stops the inbound-header path, the api-tag\n * seam and ordinary app code from being side doors that forge a trusted value.\n */\n putUntrusted<V>(key: ContextKey<V, 'untrusted'>, value: V): void {\n this.writeByName(key.name, value);\n }\n\n /**\n * Read a key of ANY trust level and ANY value type, as `unknown`.\n *\n * FRAMEWORK SERIALIZATION ONLY — the log-field builders below, the outbound header builder, and\n * the {@link ContextReader} seam. Those loop over `HeaderRegistry` key arrays that are mixed in\n * both value type and trust, and they are not making a trust DECISION: they are copying values to\n * a log line or to the wire.\n *\n * It is deliberately read-only and has no write twin. A `putAny` would re-open the exact hole the\n * typed verbs close, because forging a trusted value is the dangerous direction; reading one\n * without saying `getTrusted` only costs you the `unknown` return type.\n */\n // webpieces-disable no-any-unknown -- key-agnostic serialization read: the key array is mixed in value type, so unknown is the honest return\n getAny(key: AnyContextKey): unknown {\n return this.readByName<unknown>(key.name);\n }\n\n /** Clear one context key. Used by the api-tag seam's set → log → remove span (see LogApiCall). */\n removeKey(key: AnyContextKey): void {\n this.storage.getStore()?.delete(key.name);\n }\n\n hasKey(key: AnyContextKey): boolean {\n return this.storage.getStore()?.has(key.name) ?? false;\n }\n\n /**\n * Build the masked field map for LOGGING: every logged key in the global\n * {@link HeaderRegistry} read straight from this context, secured values\n * masked (via {@link ContextKey.maskForLogs}), keyed by each key's `name`.\n *\n * Callers: RecordingFilter + NodeProxyClient.recordCall, which snapshot the context into a\n * test FIXTURE. The @webpieces/winston and @webpieces/bunyan backends also stamp these fields\n * onto every record, and they own the \"log emitted outside RequestContext.run(...)\" complaint —\n * reporting it HERE would recurse (the error line itself re-enters buildLogFields).\n *\n * Returns an EMPTY map outside a `run(...)` block rather than throwing: a fixture snapshot or a\n * log line is never worth crashing a request over.\n */\n buildLogFields(): Map<string, string> {\n const fields = new Map<string, string>();\n if (!this.isActive()) {\n return fields;\n }\n // The registry owns WHICH keys log (getLoggedKeys); we read each straight from THIS context\n // and each ContextKey masks its own secured value. String-only — this map feeds wire/MDC +\n // recorder fixtures — so an object-valued key (API_CALL_INFO) is guarded out by the\n // typeof-string check; objects ride buildStructuredLogFields instead. (Was a HeaderRegistry\n // method taking a read callback; only the server ever called it, so the seam was dead weight.)\n for (const key of HeaderRegistry.get().getLoggedKeys()) {\n // getLoggedKeys() is AnyContextKey[] — mixed in BOTH value type and trust — so this reads\n // through getAny (serialization, not a trust decision) and narrows with the typeof-string\n // guard rather than asserting a value type per key.\n const value = this.getAny(key);\n if (typeof value === 'string' && value) {\n fields.set(key.name, key.maskForLogs(value));\n }\n }\n return fields;\n }\n\n /**\n * The STRUCTURED field map for the node logging backends: like {@link buildLogFields}, but values\n * may be OBJECTS, so an object-valued logged key ({@link WebpiecesCoreHeaders.API_CALL_INFO} holding\n * an {@link ApiCallInfo}) survives as an object and the winston/bunyan backends nest it into\n * `jsonPayload.api`. Reads values UNTYPED (not `<string>`) so the object comes through intact.\n *\n * Outside a `run(...)` block it returns just the `svcName` + `version` entries below (not a fully\n * empty map): a log line is never worth crashing over, and startup/background lines must still say\n * which service and build emitted them.\n *\n * PLUS this service's `svcName` and this build's `version` from {@link ServiceInfo}. Neither is a\n * {@link ContextKey} — they are process-global identity facts, added HERE (BEFORE the active-context\n * check) so EVERY log line of BOTH node backends (winston/bunyan read this one map) says which\n * service and build emitted it — request path, startup, and background jobs alike — with no\n * per-backend duplication. This is the SINGLE place both are stamped, keeping the two backends\n * symmetrical (jsonPayload.svcName + jsonPayload.version). Read via the non-throwing\n * {@link ServiceInfo.getName} / {@link ServiceInfo.getVersion}, so each is simply ABSENT until\n * `setInfo` has run — logging keeps working before the service is identified, then the fields start\n * appearing. Caller-set `svcName`/`version` headers (there are none by convention) would be\n * overwritten here; that is intentional — the ServiceInfo identity is authoritative.\n */\n buildStructuredLogFields(): Map<string, string | object> {\n const fields = new Map<string, string | object>();\n // This service's `svcName` + this build's `version` from ServiceInfo — NOT ContextKeys, they are\n // process-global identity facts. Added FIRST, BEFORE the active-context check, so they ride EVERY\n // line of both node backends (they read this one map) — including startup and background-job lines\n // emitted with NO active RequestContext. Treated identically and read per-record via the\n // non-throwing getters, so each is simply ABSENT until setInfo has run, then starts appearing —\n // even if setInfo runs after a backend was constructed. This is the ONE place both facts are\n // stamped, so winston and bunyan stay symmetrical (jsonPayload.svcName + jsonPayload.version).\n const svcName = ServiceInfo.getName();\n if (svcName) {\n fields.set('svcName', svcName);\n }\n const version = ServiceInfo.getVersion();\n if (version) {\n fields.set('version', version);\n }\n if (!this.isActive()) {\n return fields;\n }\n // Like buildLogFields, but values may be OBJECTS (API_CALL_INFO): read UNTYPED so the object\n // survives and winston/bunyan nest it into jsonPayload.<name>. Secured STRING values are still\n // masked per key; non-string primitives are ignored rather than String()-flattened. (Inlined\n // from HeaderRegistry for the same reason as buildLogFields — only the server called it.)\n for (const key of HeaderRegistry.get().getLoggedKeys()) {\n const value = this.getAny(key);\n if (value === undefined || value === null) {\n continue;\n }\n if (typeof value === 'string') {\n if (value) {\n fields.set(key.name, key.maskForLogs(value));\n }\n } else if (typeof value === 'object') {\n fields.set(key.name, value);\n }\n }\n return fields;\n }\n\n\n /**\n * Store the transport-neutral {@link HttpRequest} for this request. Called once, above the\n * api boundary, by whichever transport is driving the router (the express adapter, or the\n * in-process client). Filters/auth read it back via {@link getRequest} so they never touch\n * express — the same chain then runs over HTTP and in-process.\n */\n setRequest(request: HttpRequest): void {\n this.put(HTTP_REQUEST_KEY, request);\n }\n\n /** The current {@link HttpRequest}, or undefined if none was set for this context. */\n getRequest(): HttpRequest | undefined {\n return this.get<HttpRequest>(HTTP_REQUEST_KEY);\n }\n\n /**\n * Store a value under a RAW STRING key — the escape hatch for the framework's own reserved,\n * UNREGISTERED slots ('__webpieces_http_request__', the AuthFilter principal, the Cloud Tasks\n * schedule frame). Those are internal plumbing, not context keys, so they have no ContextKey and\n * no trust level.\n *\n * REJECTS any name that belongs to a registered {@link ContextKey}. Without that check this\n * method is a complete bypass of the trust system — `put('userId', req.body.userId)` would forge\n * a trusted value while never typing `putTrusted`, and an agent picks whatever compiles. The\n * check is necessarily a RUNTIME one: the registry is populated at `configure()` time, so \"is\n * this string a registered key name\" is not a fact a type can express.\n *\n * @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.\n */\n // webpieces-disable no-any-unknown -- reserved-slot values are heterogeneous (HttpRequest, principal, schedule frame)\n put(key: string, value: any): void {\n this.rejectRegisteredName(key, 'putTrusted / putUntrusted');\n this.writeByName(key, value);\n }\n\n /**\n * Retrieve a value stored under a RAW STRING key. Same reserved-slot purpose, and the same\n * rejection, as {@link put} — reading `get('userId')` would hand back a trusted value without the\n * call site ever saying `getTrusted`, which is exactly the ambiguity this whole change removes.\n *\n * @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.\n */\n // webpieces-disable no-any-unknown -- reserved-slot values are heterogeneous; callers name the concrete type\n get<T = any>(key: string): T | undefined {\n this.rejectRegisteredName(key, 'getTrusted / getUntrusted / getAny');\n return this.readByName<T>(key);\n }\n\n /**\n * Remove a value stored under a RAW STRING key. Registered names are rejected here too: deleting\n * a trusted key out from under a reader is a trust decision, so it goes through {@link removeKey}\n * with the key in hand.\n *\n * @throws Error when `key` is a registered ContextKey name.\n */\n remove(key: string): void {\n this.rejectRegisteredName(key, 'removeKey(key)');\n this.storage.getStore()?.delete(key);\n }\n\n /**\n * The guard behind the three raw-string accessors above. Silent (a no-op) until\n * `HeaderRegistry.configure(...)` has run, which is correct rather than lax: with no registry\n * there are no registered keys, so there is no trusted value to launder.\n */\n private rejectRegisteredName(name: string, useInstead: string): void {\n if (!HeaderRegistry.isConfigured()) {\n return;\n }\n const key = HeaderRegistry.get().findByName(name);\n if (key) {\n throw new Error(\n `RequestContext string accessors cannot touch '${name}' — it is a registered ` +\n `ContextKey (trust: '${key.trust}'). The raw string form hides whether the value is ` +\n `a proven fact or something a caller asserted, so it is a bypass of the trust ` +\n `system. Use ${useInstead} with the ContextKey itself.`,\n );\n }\n }\n\n /** The type-erased read. Every typed verb above funnels here; nothing else reads the store. */\n private readByName<T>(name: string): T | undefined {\n return this.storage.getStore()?.get(name);\n }\n\n /** The type-erased write. Every typed verb above funnels here; nothing else writes the store. */\n // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)\n private writeByName(name: string, value: any): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error('No context available. Did you call Context.run() first?');\n }\n store.set(name, value);\n }\n\n /**\n * Clear all values from the current context.\n */\n clear(): void {\n const store = this.storage.getStore();\n store?.clear();\n }\n\n /**\n * Copy the current context to a new Map.\n * Used by XPromise to preserve context across async boundaries.\n */\n copyContext(): Map<string, any> {\n const store = this.storage.getStore();\n if (!store) {\n return new Map();\n }\n return new Map(store);\n }\n\n /**\n * Set the entire context from a Map. Used by XPromise to restore context.\n *\n * Same FRAMEWORK-INTERNAL caveat as {@link runWithContext}: the Map must have come from\n * `copyContext()`. It cannot be trust-checked, because a faithful restore has to reinstate the\n * trusted values the original scope had proven.\n */\n setContext(context: Map<string, any>): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error('No context available. Did you call Context.run() first?');\n }\n store.clear();\n context.forEach((value, key) => {\n store.set(key, value);\n });\n }\n\n /**\n * Get all context entries.\n */\n getAll(): Map<string, any> {\n const store = this.storage.getStore();\n return store ? new Map(store) : new Map();\n }\n\n /**\n * Check if a key exists in the context.\n */\n /**\n * Presence of a value under a RAW STRING key. Guarded like its three siblings: `has('userId')`\n * alongside `hasKey(WebpiecesCoreHeaders.USER_ID)` would be a second spelling of one question,\n * and the string form is the one that says nothing about whether the value can be believed.\n *\n * @throws Error when `key` is a registered ContextKey name.\n */\n has(key: string): boolean {\n this.rejectRegisteredName(key, 'hasKey(key)');\n return this.storage.getStore()?.has(key) ?? false;\n }\n\n /**\n * Check if RequestContext is currently active.\n * Returns true if we're inside a RequestContext.run() block, false otherwise.\n *\n * Useful for tests to verify context is set up before making API calls.\n */\n isActive(): boolean {\n return this.storage.getStore() !== undefined;\n }\n\n}\n\n\n\n/**\n * Global singleton instance of RequestContext.\n * Use this throughout your application.\n */\nexport const RequestContext = new RequestContextImpl();\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiCallContext,
|
|
1
|
+
import { ApiCallContext, AnyUntrustedContextKey } from '@webpieces/core-util';
|
|
2
2
|
/**
|
|
3
3
|
* RequestContextApiCallContext - the SERVER (Node) implementation of the {@link ApiCallContext} seam,
|
|
4
4
|
* backing it with the ambient {@link RequestContext} (AsyncLocalStorage). {@link LogApiCall} — which
|
|
@@ -16,6 +16,6 @@ import { ApiCallContext, AnyContextKey } from '@webpieces/core-util';
|
|
|
16
16
|
export declare class RequestContextApiCallContext implements ApiCallContext {
|
|
17
17
|
/** A live request scope is required to stamp; LogApiCall checks this and throws when false. */
|
|
18
18
|
isActive(): boolean;
|
|
19
|
-
set(contextKey:
|
|
20
|
-
remove(contextKey:
|
|
19
|
+
set(contextKey: AnyUntrustedContextKey, value: unknown): void;
|
|
20
|
+
remove(contextKey: AnyUntrustedContextKey): void;
|
|
21
21
|
}
|
|
@@ -23,10 +23,10 @@ class RequestContextApiCallContext {
|
|
|
23
23
|
}
|
|
24
24
|
// webpieces-disable no-any-unknown -- a context value is heterogeneous (the api struct here; strings elsewhere)
|
|
25
25
|
set(contextKey, value) {
|
|
26
|
-
RequestContext_1.RequestContext.
|
|
26
|
+
RequestContext_1.RequestContext.putUntrusted(contextKey, value);
|
|
27
27
|
}
|
|
28
28
|
remove(contextKey) {
|
|
29
|
-
RequestContext_1.RequestContext.
|
|
29
|
+
RequestContext_1.RequestContext.removeKey(contextKey);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
exports.RequestContextApiCallContext = RequestContextApiCallContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequestContextApiCallContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextApiCallContext.ts"],"names":[],"mappings":";;;AACA,qDAAkD;AAElD;;;;;;;;;;;;;GAaG;AACH,MAAa,4BAA4B;IACrC,+FAA+F;IAC/F,QAAQ;QACJ,OAAO,+BAAc,CAAC,QAAQ,EAAE,CAAC;IACrC,CAAC;IAED,gHAAgH;IAChH,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"RequestContextApiCallContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextApiCallContext.ts"],"names":[],"mappings":";;;AACA,qDAAkD;AAElD;;;;;;;;;;;;;GAaG;AACH,MAAa,4BAA4B;IACrC,+FAA+F;IAC/F,QAAQ;QACJ,OAAO,+BAAc,CAAC,QAAQ,EAAE,CAAC;IACrC,CAAC;IAED,gHAAgH;IAChH,GAAG,CAAC,UAAkC,EAAE,KAAc;QAClD,+BAAc,CAAC,YAAY,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,CAAC,UAAkC;QACrC,+BAAc,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IACzC,CAAC;CACJ;AAdD,oEAcC","sourcesContent":["import { ApiCallContext, AnyUntrustedContextKey } from '@webpieces/core-util';\nimport { RequestContext } from './RequestContext';\n\n/**\n * RequestContextApiCallContext - the SERVER (Node) implementation of the {@link ApiCallContext} seam,\n * backing it with the ambient {@link RequestContext} (AsyncLocalStorage). {@link LogApiCall} — which\n * lives in browser-safe core-util and cannot import RequestContext — stamps its `api` tag through this.\n *\n * Installed ONCE at server startup by `setupRuntime` (http-routing), beside `HeaderRegistry.configure`\n * and `LogManager.setFactory` — the one place that runs on every server, so BOTH inbound (LogApiFilter)\n * and outbound (clients) get the tag. A browser never runs setupRuntime, so it installs its own\n * module-global impl. This is the same \"impl in the env-specific package, bound to a core-util seam at\n * startup\" pattern as LogManager.setFactory + the winston/bunyan backends.\n *\n * WRITE-ONLY: the logging backends read the stamped key back off RequestContext\n * (`buildStructuredLogFields`) on every record, so this seam only needs to set it.\n */\nexport class RequestContextApiCallContext implements ApiCallContext {\n /** A live request scope is required to stamp; LogApiCall checks this and throws when false. */\n isActive(): boolean {\n return RequestContext.isActive();\n }\n\n // webpieces-disable no-any-unknown -- a context value is heterogeneous (the api struct here; strings elsewhere)\n set(contextKey: AnyUntrustedContextKey, value: unknown): void {\n RequestContext.putUntrusted(contextKey, value);\n }\n\n remove(contextKey: AnyUntrustedContextKey): void {\n RequestContext.removeKey(contextKey);\n }\n}\n"]}
|
|
@@ -48,6 +48,23 @@ export declare class RequestContextHeaders {
|
|
|
48
48
|
* @throws Error when called outside `RequestContext.run(...)`.
|
|
49
49
|
*/
|
|
50
50
|
fillFromRequest(request: HttpRequest): void;
|
|
51
|
+
/**
|
|
52
|
+
* ONE inbound header -> the context, routed by the key's TRUST.
|
|
53
|
+
*
|
|
54
|
+
* An untrusted key goes straight in — nobody was ever going to make a security decision on it.
|
|
55
|
+
*
|
|
56
|
+
* A TRUSTED key does NOT. This transport-level fill runs BEFORE any filter, so at this instant
|
|
57
|
+
* nothing has verified who the caller is; writing the value now would mean `getTrusted` could
|
|
58
|
+
* return a header a stranger typed. It is stashed in {@link PendingWireTrust} instead and
|
|
59
|
+
* admitted (or rejected) by `AuthFilter`, which knows the route's auth mode. See that class for
|
|
60
|
+
* the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`
|
|
61
|
+
* and therefore the reason service-to-service identity propagation works at all.
|
|
62
|
+
*
|
|
63
|
+
* The cast is the one place trust is narrowed from the registry's mixed `AnyContextKey`: the
|
|
64
|
+
* runtime `isTrusted()` check IS the evidence for it, and it is confined to this single line
|
|
65
|
+
* rather than spread across every caller.
|
|
66
|
+
*/
|
|
67
|
+
private acceptInbound;
|
|
51
68
|
/**
|
|
52
69
|
* Record that WE minted the id — only ever called from the generate branch above, so the key is
|
|
53
70
|
* ABSENT on a hop that inherited the caller's id. Present == this service is the trace's origin.
|
|
@@ -4,6 +4,7 @@ exports.RequestContextHeaders = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const core_util_1 = require("@webpieces/core-util");
|
|
6
6
|
const frameworkProvide_1 = require("./frameworkProvide");
|
|
7
|
+
const PendingWireTrust_1 = require("./PendingWireTrust");
|
|
7
8
|
const RequestContext_1 = require("./RequestContext");
|
|
8
9
|
/**
|
|
9
10
|
* RequestContextHeaders - the magic context ↔ the wire, for a SERVER. Both directions live here:
|
|
@@ -39,10 +40,11 @@ let RequestContextHeaders = class RequestContextHeaders {
|
|
|
39
40
|
const headers = new Map();
|
|
40
41
|
// getTransferredKeys() is precomputed at configure() time.
|
|
41
42
|
for (const key of core_util_1.HeaderRegistry.get().getTransferredKeys()) {
|
|
42
|
-
// getTransferredKeys() is AnyContextKey[]
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// getTransferredKeys() is AnyContextKey[] — mixed in both value type and trust — so this
|
|
44
|
+
// reads through getAny (serialization to the wire, not a trust decision) and narrows with
|
|
45
|
+
// the typeof-string guard; every transferred value is a wire string.
|
|
46
|
+
const value = RequestContext_1.RequestContext.getAny(key);
|
|
47
|
+
if (typeof value === 'string' && value !== '') {
|
|
46
48
|
headers.set(key.httpHeader, value);
|
|
47
49
|
}
|
|
48
50
|
}
|
|
@@ -83,20 +85,43 @@ let RequestContextHeaders = class RequestContextHeaders {
|
|
|
83
85
|
// Stamp the inbound method+path as top-level logged keys (jsonPayload.httpMethod / requestPath)
|
|
84
86
|
// so EVERY log line of this request carries them. Sourced from the just-published HttpRequest;
|
|
85
87
|
// NOT transferred over the wire, so a downstream hop stamps its own inbound values.
|
|
86
|
-
RequestContext_1.RequestContext.
|
|
87
|
-
RequestContext_1.RequestContext.
|
|
88
|
+
RequestContext_1.RequestContext.putUntrusted(core_util_1.WebpiecesCoreHeaders.HTTP_METHOD, request.method);
|
|
89
|
+
RequestContext_1.RequestContext.putUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_PATH, request.path);
|
|
88
90
|
// getTransferredKeys() is precomputed at configure() time.
|
|
89
91
|
for (const key of core_util_1.HeaderRegistry.get().getTransferredKeys()) {
|
|
90
92
|
const values = request.getHeaderValues(key);
|
|
91
93
|
if (values && values.length > 0) {
|
|
92
|
-
|
|
94
|
+
this.acceptInbound(key, values[0]);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
|
-
if (!RequestContext_1.RequestContext.
|
|
96
|
-
RequestContext_1.RequestContext.
|
|
97
|
+
if (!RequestContext_1.RequestContext.hasKey(core_util_1.WebpiecesCoreHeaders.REQUEST_ID)) {
|
|
98
|
+
RequestContext_1.RequestContext.putUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_ID, this.generateRequestId());
|
|
97
99
|
this.stampRequestIdSource();
|
|
98
100
|
}
|
|
99
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* ONE inbound header -> the context, routed by the key's TRUST.
|
|
104
|
+
*
|
|
105
|
+
* An untrusted key goes straight in — nobody was ever going to make a security decision on it.
|
|
106
|
+
*
|
|
107
|
+
* A TRUSTED key does NOT. This transport-level fill runs BEFORE any filter, so at this instant
|
|
108
|
+
* nothing has verified who the caller is; writing the value now would mean `getTrusted` could
|
|
109
|
+
* return a header a stranger typed. It is stashed in {@link PendingWireTrust} instead and
|
|
110
|
+
* admitted (or rejected) by `AuthFilter`, which knows the route's auth mode. See that class for
|
|
111
|
+
* the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`
|
|
112
|
+
* and therefore the reason service-to-service identity propagation works at all.
|
|
113
|
+
*
|
|
114
|
+
* The cast is the one place trust is narrowed from the registry's mixed `AnyContextKey`: the
|
|
115
|
+
* runtime `isTrusted()` check IS the evidence for it, and it is confined to this single line
|
|
116
|
+
* rather than spread across every caller.
|
|
117
|
+
*/
|
|
118
|
+
acceptInbound(key, value) {
|
|
119
|
+
if (key.isTrusted()) {
|
|
120
|
+
PendingWireTrust_1.PendingWireTrust.stash(key, value);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
RequestContext_1.RequestContext.putUntrusted(key, value);
|
|
124
|
+
}
|
|
100
125
|
/**
|
|
101
126
|
* Record that WE minted the id — only ever called from the generate branch above, so the key is
|
|
102
127
|
* ABSENT on a hop that inherited the caller's id. Present == this service is the trace's origin.
|
|
@@ -109,7 +134,7 @@ let RequestContextHeaders = class RequestContextHeaders {
|
|
|
109
134
|
stampRequestIdSource() {
|
|
110
135
|
const svcName = core_util_1.ServiceInfo.getName();
|
|
111
136
|
if (svcName) {
|
|
112
|
-
RequestContext_1.RequestContext.
|
|
137
|
+
RequestContext_1.RequestContext.putUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_ID_SOURCE, svcName);
|
|
113
138
|
}
|
|
114
139
|
}
|
|
115
140
|
/** The id every log line of this request, and every downstream hop, will carry. */
|
|
@@ -125,7 +150,7 @@ let RequestContextHeaders = class RequestContextHeaders {
|
|
|
125
150
|
if (!RequestContext_1.RequestContext.isActive()) {
|
|
126
151
|
return undefined;
|
|
127
152
|
}
|
|
128
|
-
return RequestContext_1.RequestContext.
|
|
153
|
+
return RequestContext_1.RequestContext.getUntrusted(core_util_1.RecorderKeys.RECORDER);
|
|
129
154
|
}
|
|
130
155
|
/** Guard both directions: no ambient request scope means there is no context to fill or read. */
|
|
131
156
|
requireActiveContext() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequestContextHeaders.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextHeaders.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n HeaderRegistry,\n RecorderKeys,\n ServiceInfo,\n TestCaseRecorder,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton } from './frameworkProvide';\nimport { HttpRequest } from './HttpRequest';\nimport { RequestContext } from './RequestContext';\n\n/**\n * RequestContextHeaders - the magic context ↔ the wire, for a SERVER. Both directions live here:\n *\n * inbound {@link fillFromRequest} the published HttpRequest's headers -> the context\n * outbound {@link buildOutboundHeaders} the context -> the next hop's headers\n *\n * Reads the AsyncLocalStorage-backed {@link RequestContext} straight through — no ContextReader,\n * no ContextMgr, no abstract base. A server has exactly one place its context lives, and the\n * indirection only hid the failure below. (The browser's answer is `ContextMgr` in\n * @webpieces/core-util, which reads an app-held store because a browser has no ambient scope.)\n *\n * FAILS FAST outside a RequestContext. Silently sending an outbound call with NO request id or\n * tenant is far worse than a loud error — the trace just disappears and you find out in production. Every server-side client (RPC and Cloud Tasks) therefore only works\n * inside `RequestContext.run(...)`, which a top-level server filter normally establishes for you.\n *\n * Stateless once built, so it binds as a framework singleton every server-side client shares.\n */\n@provideFrameworkSingleton()\nexport class RequestContextHeaders {\n /**\n * EVERY transferred key with a non-empty value, under its wire name. Nothing is rewritten.\n *\n * That includes `x-request-id`, which propagates unchanged: one id correlates the whole call\n * tree, so the callee keeps ours rather than minting its own. ({@link fillFromRequest} only\n * generates an id when the inbound request carries none.)\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n *\n * @throws Error when called outside `RequestContext.run(...)` — see the class doc.\n */\n buildOutboundHeaders(): Map<string, string> {\n this.requireActiveContext();\n\n const headers = new Map<string, string>();\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n // getTransferredKeys() is AnyContextKey[]; every transferred value is a wire\n // string, so read by name rather than asserting a value type on a generically-typed key.\n const value = RequestContext.get<string>(key.name);\n if (value !== undefined && value !== null && value !== '') {\n headers.set(key.httpHeader!, value);\n }\n }\n\n // CLIENT_VERSION is transferred, but each hop sends ITS OWN build version (not the inherited\n // one) so a downstream server logs which build actually called it. Overwrite whatever the loop\n // copied from an inbound clientVersion with ours; if THIS service has no version, drop it\n // rather than forward the caller's as if it were ours. Non-throwing read — absent before setInfo.\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n headers.set(clientVersionHeader, myVersion);\n } else {\n headers.delete(clientVersionHeader);\n }\n\n return headers;\n }\n\n /**\n * INBOUND — the exact inverse of {@link buildOutboundHeaders}. Publish the request, move every\n * transferrable header off it into the context (read by wire name, stored under the key's\n * `name`), and mint an `x-request-id` if the caller sent none.\n *\n * The request is a PARAMETER, not something we fish back out of the context. Publishing and\n * filling are therefore one atomic step that cannot be half-done or done out of order — the\n * older `setRequest()` + `fillContext()` pair could silently skip the transfer entirely when a\n * caller forgot the first half.\n *\n * This is a PRECONDITION of calling into http-routing, and it belongs ABOVE the api boundary.\n * `WebpiecesMiddleware` does it for every HTTP request; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. The api proxy only checks that a\n * request scope exists — it never builds one.\n *\n * @throws Error when called outside `RequestContext.run(...)`.\n */\n fillFromRequest(request: HttpRequest): void {\n this.requireActiveContext();\n\n RequestContext.setRequest(request);\n\n // Stamp the inbound method+path as top-level logged keys (jsonPayload.httpMethod / requestPath)\n // so EVERY log line of this request carries them. Sourced from the just-published HttpRequest;\n // NOT transferred over the wire, so a downstream hop stamps its own inbound values.\n RequestContext.putHeader(WebpiecesCoreHeaders.HTTP_METHOD, request.method);\n RequestContext.putHeader(WebpiecesCoreHeaders.REQUEST_PATH, request.path);\n\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n const values = request.getHeaderValues(key);\n if (values && values.length > 0) {\n RequestContext.putHeader(key, values[0]);\n }\n }\n\n if (!RequestContext.hasHeader(WebpiecesCoreHeaders.REQUEST_ID)) {\n RequestContext.putHeader(WebpiecesCoreHeaders.REQUEST_ID, this.generateRequestId());\n this.stampRequestIdSource();\n }\n }\n\n /**\n * Record that WE minted the id — only ever called from the generate branch above, so the key is\n * ABSENT on a hop that inherited the caller's id. Present == this service is the trace's origin.\n *\n * Uses the non-throwing `getName()`: this runs PER REQUEST, and a missing log field must not 500\n * live traffic. A server that booted already ran `setupRuntime`, which calls `ServiceInfo.setInfo`\n * with its required name+version, so the name is always there in practice; only a test driving the\n * context directly sees undefined.\n */\n private stampRequestIdSource(): void {\n const svcName = ServiceInfo.getName();\n if (svcName) {\n RequestContext.putHeader(WebpiecesCoreHeaders.REQUEST_ID_SOURCE, svcName);\n }\n }\n\n /** The id every log line of this request, and every downstream hop, will carry. */\n private generateRequestId(): string {\n return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;\n }\n\n /**\n * The recorder travelling in the context, when a test is recording this call. Absent in normal\n * operation, and ALWAYS absent in a browser — which is why recording lives on the server-side\n * client and never in the isomorphic core.\n */\n findRecorder(): TestCaseRecorder | undefined {\n if (!RequestContext.isActive()) {\n return undefined;\n }\n return RequestContext.getHeader<TestCaseRecorder>(RecorderKeys.RECORDER);\n }\n\n /** Guard both directions: no ambient request scope means there is no context to fill or read. */\n private requireActiveContext(): void {\n if (!RequestContext.isActive()) {\n throw new Error(\n 'No active RequestContext. A webpieces server-side client only works inside ' +\n 'RequestContext.run(...), which a top-level server filter normally establishes. ' +\n 'In a test, wrap the call: await RequestContext.run(async () => client.foo(req));',\n );\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"RequestContextHeaders.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextHeaders.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AnyContextKey,\n AnyTrustedContextKey,\n AnyUntrustedContextKey,\n HeaderRegistry,\n RecorderKeys,\n ServiceInfo,\n TestCaseRecorder,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton } from './frameworkProvide';\nimport { PendingWireTrust } from './PendingWireTrust';\nimport { HttpRequest } from './HttpRequest';\nimport { RequestContext } from './RequestContext';\n\n/**\n * RequestContextHeaders - the magic context ↔ the wire, for a SERVER. Both directions live here:\n *\n * inbound {@link fillFromRequest} the published HttpRequest's headers -> the context\n * outbound {@link buildOutboundHeaders} the context -> the next hop's headers\n *\n * Reads the AsyncLocalStorage-backed {@link RequestContext} straight through — no ContextReader,\n * no ContextMgr, no abstract base. A server has exactly one place its context lives, and the\n * indirection only hid the failure below. (The browser's answer is `ContextMgr` in\n * @webpieces/core-util, which reads an app-held store because a browser has no ambient scope.)\n *\n * FAILS FAST outside a RequestContext. Silently sending an outbound call with NO request id or\n * tenant is far worse than a loud error — the trace just disappears and you find out in production. Every server-side client (RPC and Cloud Tasks) therefore only works\n * inside `RequestContext.run(...)`, which a top-level server filter normally establishes for you.\n *\n * Stateless once built, so it binds as a framework singleton every server-side client shares.\n */\n@provideFrameworkSingleton()\nexport class RequestContextHeaders {\n /**\n * EVERY transferred key with a non-empty value, under its wire name. Nothing is rewritten.\n *\n * That includes `x-request-id`, which propagates unchanged: one id correlates the whole call\n * tree, so the callee keeps ours rather than minting its own. ({@link fillFromRequest} only\n * generates an id when the inbound request carries none.)\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n *\n * @throws Error when called outside `RequestContext.run(...)` — see the class doc.\n */\n buildOutboundHeaders(): Map<string, string> {\n this.requireActiveContext();\n\n const headers = new Map<string, string>();\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n // getTransferredKeys() is AnyContextKey[] — mixed in both value type and trust — so this\n // reads through getAny (serialization to the wire, not a trust decision) and narrows with\n // the typeof-string guard; every transferred value is a wire string.\n const value = RequestContext.getAny(key);\n if (typeof value === 'string' && value !== '') {\n headers.set(key.httpHeader!, value);\n }\n }\n\n // CLIENT_VERSION is transferred, but each hop sends ITS OWN build version (not the inherited\n // one) so a downstream server logs which build actually called it. Overwrite whatever the loop\n // copied from an inbound clientVersion with ours; if THIS service has no version, drop it\n // rather than forward the caller's as if it were ours. Non-throwing read — absent before setInfo.\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n headers.set(clientVersionHeader, myVersion);\n } else {\n headers.delete(clientVersionHeader);\n }\n\n return headers;\n }\n\n /**\n * INBOUND — the exact inverse of {@link buildOutboundHeaders}. Publish the request, move every\n * transferrable header off it into the context (read by wire name, stored under the key's\n * `name`), and mint an `x-request-id` if the caller sent none.\n *\n * The request is a PARAMETER, not something we fish back out of the context. Publishing and\n * filling are therefore one atomic step that cannot be half-done or done out of order — the\n * older `setRequest()` + `fillContext()` pair could silently skip the transfer entirely when a\n * caller forgot the first half.\n *\n * This is a PRECONDITION of calling into http-routing, and it belongs ABOVE the api boundary.\n * `WebpiecesMiddleware` does it for every HTTP request; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. The api proxy only checks that a\n * request scope exists — it never builds one.\n *\n * @throws Error when called outside `RequestContext.run(...)`.\n */\n fillFromRequest(request: HttpRequest): void {\n this.requireActiveContext();\n\n RequestContext.setRequest(request);\n\n // Stamp the inbound method+path as top-level logged keys (jsonPayload.httpMethod / requestPath)\n // so EVERY log line of this request carries them. Sourced from the just-published HttpRequest;\n // NOT transferred over the wire, so a downstream hop stamps its own inbound values.\n RequestContext.putUntrusted(WebpiecesCoreHeaders.HTTP_METHOD, request.method);\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_PATH, request.path);\n\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n const values = request.getHeaderValues(key);\n if (values && values.length > 0) {\n this.acceptInbound(key, values[0]);\n }\n }\n\n if (!RequestContext.hasKey(WebpiecesCoreHeaders.REQUEST_ID)) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID, this.generateRequestId());\n this.stampRequestIdSource();\n }\n }\n\n /**\n * ONE inbound header -> the context, routed by the key's TRUST.\n *\n * An untrusted key goes straight in — nobody was ever going to make a security decision on it.\n *\n * A TRUSTED key does NOT. This transport-level fill runs BEFORE any filter, so at this instant\n * nothing has verified who the caller is; writing the value now would mean `getTrusted` could\n * return a header a stranger typed. It is stashed in {@link PendingWireTrust} instead and\n * admitted (or rejected) by `AuthFilter`, which knows the route's auth mode. See that class for\n * the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`\n * and therefore the reason service-to-service identity propagation works at all.\n *\n * The cast is the one place trust is narrowed from the registry's mixed `AnyContextKey`: the\n * runtime `isTrusted()` check IS the evidence for it, and it is confined to this single line\n * rather than spread across every caller.\n */\n private acceptInbound(key: AnyContextKey, value: string): void {\n if (key.isTrusted()) {\n PendingWireTrust.stash(key as AnyTrustedContextKey, value);\n return;\n }\n RequestContext.putUntrusted(key as AnyUntrustedContextKey, value);\n }\n\n /**\n * Record that WE minted the id — only ever called from the generate branch above, so the key is\n * ABSENT on a hop that inherited the caller's id. Present == this service is the trace's origin.\n *\n * Uses the non-throwing `getName()`: this runs PER REQUEST, and a missing log field must not 500\n * live traffic. A server that booted already ran `setupRuntime`, which calls `ServiceInfo.setInfo`\n * with its required name+version, so the name is always there in practice; only a test driving the\n * context directly sees undefined.\n */\n private stampRequestIdSource(): void {\n const svcName = ServiceInfo.getName();\n if (svcName) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID_SOURCE, svcName);\n }\n }\n\n /** The id every log line of this request, and every downstream hop, will carry. */\n private generateRequestId(): string {\n return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;\n }\n\n /**\n * The recorder travelling in the context, when a test is recording this call. Absent in normal\n * operation, and ALWAYS absent in a browser — which is why recording lives on the server-side\n * client and never in the isomorphic core.\n */\n findRecorder(): TestCaseRecorder | undefined {\n if (!RequestContext.isActive()) {\n return undefined;\n }\n return RequestContext.getUntrusted<TestCaseRecorder>(RecorderKeys.RECORDER);\n }\n\n /** Guard both directions: no ambient request scope means there is no context to fill or read. */\n private requireActiveContext(): void {\n if (!RequestContext.isActive()) {\n throw new Error(\n 'No active RequestContext. A webpieces server-side client only works inside ' +\n 'RequestContext.run(...), which a top-level server filter normally establishes. ' +\n 'In a test, wrap the call: await RequestContext.run(async () => client.foo(req));',\n );\n }\n }\n}\n"]}
|
|
@@ -16,9 +16,11 @@ const RequestContext_1 = require("./RequestContext");
|
|
|
16
16
|
class RequestContextReader {
|
|
17
17
|
/** Read a string context-key value from the active RequestContext. */
|
|
18
18
|
read(key) {
|
|
19
|
-
// `key` is a generically-typed AnyContextKey here (the reader is key-agnostic)
|
|
20
|
-
//
|
|
21
|
-
|
|
19
|
+
// `key` is a generically-typed AnyContextKey here (the reader is key-agnostic) — mixed in
|
|
20
|
+
// both value type and trust — so it reads through getAny (serialization, not a trust
|
|
21
|
+
// decision) and narrows to this method's string-only contract.
|
|
22
|
+
const value = RequestContext_1.RequestContext.getAny(key);
|
|
23
|
+
return typeof value === 'string' ? value : undefined;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Read a non-string context value (e.g. the active TestCaseRecorder under
|
|
@@ -27,7 +29,7 @@ class RequestContextReader {
|
|
|
27
29
|
*/
|
|
28
30
|
// webpieces-disable no-any-unknown -- context values are heterogeneous (recorder, meta objects)
|
|
29
31
|
readValue(key) {
|
|
30
|
-
return RequestContext_1.RequestContext.
|
|
32
|
+
return RequestContext_1.RequestContext.getAny(key);
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
exports.RequestContextReader = RequestContextReader;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequestContextReader.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextReader.ts"],"names":[],"mappings":";;;AACA,qDAAkD;AAElD;;;;;;;;;;GAUG;AACH,MAAa,oBAAoB;IAC7B,sEAAsE;IACtE,IAAI,CAAC,GAAkB;QACnB,
|
|
1
|
+
{"version":3,"file":"RequestContextReader.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextReader.ts"],"names":[],"mappings":";;;AACA,qDAAkD;AAElD;;;;;;;;;;GAUG;AACH,MAAa,oBAAoB;IAC7B,sEAAsE;IACtE,IAAI,CAAC,GAAkB;QACnB,0FAA0F;QAC1F,qFAAqF;QACrF,+DAA+D;QAC/D,MAAM,KAAK,GAAG,+BAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACzD,CAAC;IAED;;;;OAIG;IACH,gGAAgG;IAChG,SAAS,CAAC,GAAkB;QACxB,OAAO,+BAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACtC,CAAC;CACJ;AAnBD,oDAmBC","sourcesContent":["import { AnyContextKey, ContextReader } from '@webpieces/core-util';\nimport { RequestContext } from './RequestContext';\n\n/**\n * RequestContextReader - the NODE/server ContextReader. Reads context-key values\n * from the AsyncLocalStorage-backed RequestContext.\n *\n * Only works in Node.js with an active RequestContext. Lives in\n * @webpieces/core-context (Node-only) alongside the RequestContext it reads from,\n * so libraries can build a context-propagating ContextMgr without pulling in\n * @webpieces/http-routing.\n *\n * For browsers, use MutableContextStore from @webpieces/http-client.\n */\nexport class RequestContextReader implements ContextReader {\n /** Read a string context-key value from the active RequestContext. */\n read(key: AnyContextKey): string | undefined {\n // `key` is a generically-typed AnyContextKey here (the reader is key-agnostic) — mixed in\n // both value type and trust — so it reads through getAny (serialization, not a trust\n // decision) and narrows to this method's string-only contract.\n const value = RequestContext.getAny(key);\n return typeof value === 'string' ? value : undefined;\n }\n\n /**\n * Read a non-string context value (e.g. the active TestCaseRecorder under\n * RecorderKeys.RECORDER). Lets the isomorphic http-client find server-side\n * context without importing core-context itself.\n */\n // webpieces-disable no-any-unknown -- context values are heterogeneous (recorder, meta objects)\n readValue(key: AnyContextKey): unknown {\n return RequestContext.getAny(key);\n }\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMPILE-TIME assertions that the four trust verbs reject the wrong kind of key.
|
|
3
|
+
*
|
|
4
|
+
* This is the half of the guarantee that a runtime check could not give you: a reader cannot
|
|
5
|
+
* accidentally treat a caller-asserted value as proven, because the call does not compile. Each
|
|
6
|
+
* `@ts-expect-error` fails the build (TS2578) if its line ever starts compiling.
|
|
7
|
+
*
|
|
8
|
+
* In COMPILED source deliberately — a `@ts-expect-error` inside a `.spec.ts` is inert here
|
|
9
|
+
* (tsconfig.lib.json excludes specs; vitest strips types with esbuild), so the tripwire would be a
|
|
10
|
+
* no-op. See `ContextKeyTrustCompileAssertions` for the key-construction half.
|
|
11
|
+
*/
|
|
12
|
+
export declare class RequestContextTrustCompileAssertions {
|
|
13
|
+
private readonly trusted;
|
|
14
|
+
private readonly untrusted;
|
|
15
|
+
/** Reading an untrusted value as if it were proven is the bug this whole change exists to stop. */
|
|
16
|
+
cannotReadUntrustedAsTrusted(): void;
|
|
17
|
+
/** And the reverse, so a call site always states which kind of value it believes it has. */
|
|
18
|
+
cannotReadTrustedAsUntrusted(): void;
|
|
19
|
+
/** FORGERY: writing a trusted key through the untrusted verb must not compile. */
|
|
20
|
+
cannotForgeTrustedViaUntrustedWrite(): void;
|
|
21
|
+
/** Claiming proof for an untrusted key must not compile either — trust is not a caller's choice. */
|
|
22
|
+
cannotClaimProofForUntrustedKey(): void;
|
|
23
|
+
/** POSITIVE: the matching pairs must keep compiling, with the value type inferred from the key. */
|
|
24
|
+
matchingVerbsCompile(): void;
|
|
25
|
+
/** The value type still comes from the key: a trusted string key rejects a number. */
|
|
26
|
+
valueTypeStillEnforced(): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RequestContextTrustCompileAssertions = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
const RequestContext_1 = require("./RequestContext");
|
|
6
|
+
/**
|
|
7
|
+
* COMPILE-TIME assertions that the four trust verbs reject the wrong kind of key.
|
|
8
|
+
*
|
|
9
|
+
* This is the half of the guarantee that a runtime check could not give you: a reader cannot
|
|
10
|
+
* accidentally treat a caller-asserted value as proven, because the call does not compile. Each
|
|
11
|
+
* `@ts-expect-error` fails the build (TS2578) if its line ever starts compiling.
|
|
12
|
+
*
|
|
13
|
+
* In COMPILED source deliberately — a `@ts-expect-error` inside a `.spec.ts` is inert here
|
|
14
|
+
* (tsconfig.lib.json excludes specs; vitest strips types with esbuild), so the tripwire would be a
|
|
15
|
+
* no-op. See `ContextKeyTrustCompileAssertions` for the key-construction half.
|
|
16
|
+
*/
|
|
17
|
+
class RequestContextTrustCompileAssertions {
|
|
18
|
+
trusted = core_util_1.ContextKey.trusted('assertUserId', 'jwt claim `sub`');
|
|
19
|
+
untrusted = core_util_1.ContextKey.untrusted('assertActionId');
|
|
20
|
+
/** Reading an untrusted value as if it were proven is the bug this whole change exists to stop. */
|
|
21
|
+
cannotReadUntrustedAsTrusted() {
|
|
22
|
+
// @ts-expect-error - getTrusted does not accept an untrusted key
|
|
23
|
+
RequestContext_1.RequestContext.getTrusted(this.untrusted);
|
|
24
|
+
}
|
|
25
|
+
/** And the reverse, so a call site always states which kind of value it believes it has. */
|
|
26
|
+
cannotReadTrustedAsUntrusted() {
|
|
27
|
+
// @ts-expect-error - getUntrusted does not accept a trusted key
|
|
28
|
+
RequestContext_1.RequestContext.getUntrusted(this.trusted);
|
|
29
|
+
}
|
|
30
|
+
/** FORGERY: writing a trusted key through the untrusted verb must not compile. */
|
|
31
|
+
cannotForgeTrustedViaUntrustedWrite() {
|
|
32
|
+
// @ts-expect-error - putUntrusted does not accept a trusted key
|
|
33
|
+
RequestContext_1.RequestContext.putUntrusted(this.trusted, 'attacker-supplied');
|
|
34
|
+
}
|
|
35
|
+
/** Claiming proof for an untrusted key must not compile either — trust is not a caller's choice. */
|
|
36
|
+
cannotClaimProofForUntrustedKey() {
|
|
37
|
+
// @ts-expect-error - putTrusted does not accept an untrusted key
|
|
38
|
+
RequestContext_1.RequestContext.putTrusted(this.untrusted, 'whatever');
|
|
39
|
+
}
|
|
40
|
+
/** POSITIVE: the matching pairs must keep compiling, with the value type inferred from the key. */
|
|
41
|
+
matchingVerbsCompile() {
|
|
42
|
+
RequestContext_1.RequestContext.putTrusted(this.trusted, 'user-42');
|
|
43
|
+
RequestContext_1.RequestContext.putUntrusted(this.untrusted, 'click-7');
|
|
44
|
+
const a = RequestContext_1.RequestContext.getTrusted(this.trusted);
|
|
45
|
+
const b = RequestContext_1.RequestContext.getUntrusted(this.untrusted);
|
|
46
|
+
void a;
|
|
47
|
+
void b;
|
|
48
|
+
}
|
|
49
|
+
/** The value type still comes from the key: a trusted string key rejects a number. */
|
|
50
|
+
valueTypeStillEnforced() {
|
|
51
|
+
// @ts-expect-error - the key declares string, so a number is not a valid value
|
|
52
|
+
RequestContext_1.RequestContext.putTrusted(this.trusted, 42);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.RequestContextTrustCompileAssertions = RequestContextTrustCompileAssertions;
|
|
56
|
+
//# sourceMappingURL=RequestContextTrustCompileAssertions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RequestContextTrustCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextTrustCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,oDAAkD;AAClD,qDAAkD;AAElD;;;;;;;;;;GAUG;AACH,MAAa,oCAAoC;IAC5B,OAAO,GAAG,sBAAU,CAAC,OAAO,CAAS,cAAc,EAAE,iBAAiB,CAAC,CAAC;IACxE,SAAS,GAAG,sBAAU,CAAC,SAAS,CAAS,gBAAgB,CAAC,CAAC;IAE5E,mGAAmG;IACnG,4BAA4B;QACxB,iEAAiE;QACjE,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9C,CAAC;IAED,4FAA4F;IAC5F,4BAA4B;QACxB,gEAAgE;QAChE,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED,kFAAkF;IAClF,mCAAmC;QAC/B,gEAAgE;QAChE,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IACnE,CAAC;IAED,oGAAoG;IACpG,+BAA+B;QAC3B,iEAAiE;QACjE,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAC1D,CAAC;IAED,mGAAmG;IACnG,oBAAoB;QAChB,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QACnD,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACvD,MAAM,CAAC,GAAuB,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtE,MAAM,CAAC,GAAuB,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC1E,KAAK,CAAC,CAAC;QACP,KAAK,CAAC,CAAC;IACX,CAAC;IAED,sFAAsF;IACtF,sBAAsB;QAClB,+EAA+E;QAC/E,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAChD,CAAC;CACJ;AA3CD,oFA2CC","sourcesContent":["import { ContextKey } from '@webpieces/core-util';\nimport { RequestContext } from './RequestContext';\n\n/**\n * COMPILE-TIME assertions that the four trust verbs reject the wrong kind of key.\n *\n * This is the half of the guarantee that a runtime check could not give you: a reader cannot\n * accidentally treat a caller-asserted value as proven, because the call does not compile. Each\n * `@ts-expect-error` fails the build (TS2578) if its line ever starts compiling.\n *\n * In COMPILED source deliberately — a `@ts-expect-error` inside a `.spec.ts` is inert here\n * (tsconfig.lib.json excludes specs; vitest strips types with esbuild), so the tripwire would be a\n * no-op. See `ContextKeyTrustCompileAssertions` for the key-construction half.\n */\nexport class RequestContextTrustCompileAssertions {\n private readonly trusted = ContextKey.trusted<string>('assertUserId', 'jwt claim `sub`');\n private readonly untrusted = ContextKey.untrusted<string>('assertActionId');\n\n /** Reading an untrusted value as if it were proven is the bug this whole change exists to stop. */\n cannotReadUntrustedAsTrusted(): void {\n // @ts-expect-error - getTrusted does not accept an untrusted key\n RequestContext.getTrusted(this.untrusted);\n }\n\n /** And the reverse, so a call site always states which kind of value it believes it has. */\n cannotReadTrustedAsUntrusted(): void {\n // @ts-expect-error - getUntrusted does not accept a trusted key\n RequestContext.getUntrusted(this.trusted);\n }\n\n /** FORGERY: writing a trusted key through the untrusted verb must not compile. */\n cannotForgeTrustedViaUntrustedWrite(): void {\n // @ts-expect-error - putUntrusted does not accept a trusted key\n RequestContext.putUntrusted(this.trusted, 'attacker-supplied');\n }\n\n /** Claiming proof for an untrusted key must not compile either — trust is not a caller's choice. */\n cannotClaimProofForUntrustedKey(): void {\n // @ts-expect-error - putTrusted does not accept an untrusted key\n RequestContext.putTrusted(this.untrusted, 'whatever');\n }\n\n /** POSITIVE: the matching pairs must keep compiling, with the value type inferred from the key. */\n matchingVerbsCompile(): void {\n RequestContext.putTrusted(this.trusted, 'user-42');\n RequestContext.putUntrusted(this.untrusted, 'click-7');\n const a: string | undefined = RequestContext.getTrusted(this.trusted);\n const b: string | undefined = RequestContext.getUntrusted(this.untrusted);\n void a;\n void b;\n }\n\n /** The value type still comes from the key: a trusted string key rejects a number. */\n valueTypeStillEnforced(): void {\n // @ts-expect-error - the key declares string, so a number is not a valid value\n RequestContext.putTrusted(this.trusted, 42);\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { provideFrameworkSingleton, provideFrameworkSingletonDefaultForApi, prov
|
|
|
7
7
|
export type { FrameworkScope } from './frameworkProvide';
|
|
8
8
|
export { RequestContextHeaders } from './RequestContextHeaders';
|
|
9
9
|
export { RequestContextReader } from './RequestContextReader';
|
|
10
|
+
export { PendingWireTrust, PendingTrustedValue } from './PendingWireTrust';
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RequestContextReader = exports.RequestContextHeaders = exports.buildFrameworkModule = exports.bindFrameworkProvider = exports.provideFrameworkTransient = exports.provideFrameworkSingletonDefaultForApi = exports.provideFrameworkSingleton = exports.Provider = exports.provideSingletonDefaultForApi = exports.HttpRequest = exports.RequestContextApiCallContext = exports.RequestContext = void 0;
|
|
3
|
+
exports.PendingTrustedValue = exports.PendingWireTrust = exports.RequestContextReader = exports.RequestContextHeaders = exports.buildFrameworkModule = exports.bindFrameworkProvider = exports.provideFrameworkTransient = exports.provideFrameworkSingletonDefaultForApi = exports.provideFrameworkSingleton = exports.Provider = exports.provideSingletonDefaultForApi = exports.HttpRequest = exports.RequestContextApiCallContext = exports.RequestContext = void 0;
|
|
4
4
|
// Context management with AsyncLocalStorage
|
|
5
5
|
var RequestContext_1 = require("./RequestContext");
|
|
6
6
|
Object.defineProperty(exports, "RequestContext", { enumerable: true, get: function () { return RequestContext_1.RequestContext; } });
|
|
@@ -38,4 +38,7 @@ Object.defineProperty(exports, "RequestContextHeaders", { enumerable: true, get:
|
|
|
38
38
|
// The browser store's server counterpart, still used by the logging packages + http-server filters.
|
|
39
39
|
var RequestContextReader_1 = require("./RequestContextReader");
|
|
40
40
|
Object.defineProperty(exports, "RequestContextReader", { enumerable: true, get: function () { return RequestContextReader_1.RequestContextReader; } });
|
|
41
|
+
var PendingWireTrust_1 = require("./PendingWireTrust");
|
|
42
|
+
Object.defineProperty(exports, "PendingWireTrust", { enumerable: true, get: function () { return PendingWireTrust_1.PendingWireTrust; } });
|
|
43
|
+
Object.defineProperty(exports, "PendingTrustedValue", { enumerable: true, get: function () { return PendingWireTrust_1.PendingTrustedValue; } });
|
|
41
44
|
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/index.ts"],"names":[],"mappings":";;;AAAA,4CAA4C;AAC5C,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AACvB,oGAAoG;AACpG,uGAAuG;AACvG,4FAA4F;AAC5F,+EAA8E;AAArE,4IAAA,4BAA4B,OAAA;AACrC,mGAAmG;AACnG,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,mFAAmF;AACnF,qCAA0D;AAAjD,wHAAA,6BAA6B,OAAA;AACtC,2FAA2F;AAC3F,qCAAqC;AAA5B,mGAAA,QAAQ,OAAA;AACjB,sFAAsF;AACtF,wEAAwE;AACxE,uDAM4B;AALxB,6HAAA,yBAAyB,OAAA;AACzB,0IAAA,sCAAsC,OAAA;AACtC,6HAAA,yBAAyB,OAAA;AACzB,yHAAA,qBAAqB,OAAA;AACrB,wHAAA,oBAAoB,OAAA;AAIxB,mFAAmF;AACnF,yFAAyF;AACzF,yBAAyB;AACzB,EAAE;AACF,8FAA8F;AAC9F,2FAA2F;AAC3F,uDAAuD;AACvD,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,oGAAoG;AACpG,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA","sourcesContent":["// Context management with AsyncLocalStorage\nexport { RequestContext } from './RequestContext';\n// SERVER impl of the core-util ApiCallContext seam, bound to RequestContext. Importing it here runs\n// its install() side effect, so LogApiCall (core-util, browser-safe) stamps the real RequestContext on\n// a Node server without importing it. A browser never loads core-context → keeps the no-op.\nexport { RequestContextApiCallContext } from './RequestContextApiCallContext';\n// Transport-neutral request stored in the context (http-routing's request type; re-exported there)\nexport { HttpRequest } from './HttpRequest';\n\n// DI provider decorators (shared DI seam; http-routing re-exports for back-compat)\nexport { provideSingletonDefaultForApi } from './provide';\n// Guice-style Provider<T> — lazy singleton OR fresh-per-get, decided by T's binding scope.\nexport { Provider } from './provide';\n// Framework-only DI registry (packages/** use these; keeps framework classes out of a\n// client's buildProviderModule() global scan). See frameworkProvide.ts.\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n provideFrameworkTransient,\n bindFrameworkProvider,\n buildFrameworkModule,\n} from './frameworkProvide';\nexport type { FrameworkScope } from './frameworkProvide';\n\n// Outbound headers for a SERVER: reads RequestContext directly, fails fast outside\n// RequestContext.run(...). Server-side clients (http-client-node, cloudtasks-client) and\n// http-routing use THIS.\n//\n// ContextMgr is deliberately NOT re-exported. It is the browser's answer (an app-held store),\n// and only @webpieces/http-client-browser may name it — importing it here would let a node\n// package reach for a ContextReader it has no use for.\nexport { RequestContextHeaders } from './RequestContextHeaders';\n// The browser store's server counterpart, still used by the logging packages + http-server filters.\nexport { RequestContextReader } from './RequestContextReader';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/index.ts"],"names":[],"mappings":";;;AAAA,4CAA4C;AAC5C,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AACvB,oGAAoG;AACpG,uGAAuG;AACvG,4FAA4F;AAC5F,+EAA8E;AAArE,4IAAA,4BAA4B,OAAA;AACrC,mGAAmG;AACnG,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,mFAAmF;AACnF,qCAA0D;AAAjD,wHAAA,6BAA6B,OAAA;AACtC,2FAA2F;AAC3F,qCAAqC;AAA5B,mGAAA,QAAQ,OAAA;AACjB,sFAAsF;AACtF,wEAAwE;AACxE,uDAM4B;AALxB,6HAAA,yBAAyB,OAAA;AACzB,0IAAA,sCAAsC,OAAA;AACtC,6HAAA,yBAAyB,OAAA;AACzB,yHAAA,qBAAqB,OAAA;AACrB,wHAAA,oBAAoB,OAAA;AAIxB,mFAAmF;AACnF,yFAAyF;AACzF,yBAAyB;AACzB,EAAE;AACF,8FAA8F;AAC9F,2FAA2F;AAC3F,uDAAuD;AACvD,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,oGAAoG;AACpG,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,uDAA2E;AAAlE,oHAAA,gBAAgB,OAAA;AAAE,uHAAA,mBAAmB,OAAA","sourcesContent":["// Context management with AsyncLocalStorage\nexport { RequestContext } from './RequestContext';\n// SERVER impl of the core-util ApiCallContext seam, bound to RequestContext. Importing it here runs\n// its install() side effect, so LogApiCall (core-util, browser-safe) stamps the real RequestContext on\n// a Node server without importing it. A browser never loads core-context → keeps the no-op.\nexport { RequestContextApiCallContext } from './RequestContextApiCallContext';\n// Transport-neutral request stored in the context (http-routing's request type; re-exported there)\nexport { HttpRequest } from './HttpRequest';\n\n// DI provider decorators (shared DI seam; http-routing re-exports for back-compat)\nexport { provideSingletonDefaultForApi } from './provide';\n// Guice-style Provider<T> — lazy singleton OR fresh-per-get, decided by T's binding scope.\nexport { Provider } from './provide';\n// Framework-only DI registry (packages/** use these; keeps framework classes out of a\n// client's buildProviderModule() global scan). See frameworkProvide.ts.\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n provideFrameworkTransient,\n bindFrameworkProvider,\n buildFrameworkModule,\n} from './frameworkProvide';\nexport type { FrameworkScope } from './frameworkProvide';\n\n// Outbound headers for a SERVER: reads RequestContext directly, fails fast outside\n// RequestContext.run(...). Server-side clients (http-client-node, cloudtasks-client) and\n// http-routing use THIS.\n//\n// ContextMgr is deliberately NOT re-exported. It is the browser's answer (an app-held store),\n// and only @webpieces/http-client-browser may name it — importing it here would let a node\n// package reach for a ContextReader it has no use for.\nexport { RequestContextHeaders } from './RequestContextHeaders';\n// The browser store's server counterpart, still used by the logging packages + http-server filters.\nexport { RequestContextReader } from './RequestContextReader';\nexport { PendingWireTrust, PendingTrustedValue } from './PendingWireTrust';\n"]}
|