@webpieces/core-context 0.4.598 → 0.4.600

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/core-context",
3
- "version": "0.4.598",
3
+ "version": "0.4.600",
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.598",
25
+ "@webpieces/core-util": "0.4.600",
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"]}
@@ -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 the value stored under a {@link ContextKey}. The return type is the key's OWN value type
40
- * `V` `string` for wire/log keys, `ApiCallInfo` for the api tag, `TestCaseRecorder` for the
41
- * recorder INFERRED from the key, never asserted by the caller. This is the typed public
42
- * surface over the deliberately type-erased backing Map.
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
- getHeader<V>(key: ContextKey<V>): V | undefined;
58
+ getTrusted<V>(key: ContextKey<V, 'trusted'>): V | undefined;
45
59
  /**
46
- * Store a value under a {@link ContextKey}. `value` is type-checked against the key's value type
47
- * `V`, so you cannot put a number under a `ContextKey<string>` or a raw object under a typed key.
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
- putHeader<V>(key: ContextKey<V>, value: V): void;
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.maskIfSecured}), keyed by each key's `name`.
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 in the current context.
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 from the current context.
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 from the current context.
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
- * Used by XPromise to restore context.
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.
@@ -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 the value stored under a {@link ContextKey}. The return type is the key's OWN value type
58
- * `V` `string` for wire/log keys, `ApiCallInfo` for the api tag, `TestCaseRecorder` for the
59
- * recorder INFERRED from the key, never asserted by the caller. This is the typed public
60
- * surface over the deliberately type-erased backing Map.
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
- getHeader(key) {
63
- return this.get(key.name);
76
+ getTrusted(key) {
77
+ return this.readByName(key.name);
64
78
  }
65
79
  /**
66
- * Store a value under a {@link ContextKey}. `value` is type-checked against the key's value type
67
- * `V`, so you cannot put a number under a `ContextKey<string>` or a raw object under a typed key.
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
- putHeader(key, value) {
70
- this.put(key.name, value);
87
+ getUntrusted(key) {
88
+ return this.readByName(key.name);
71
89
  }
72
- /** Clear one context key. Used by the api-tag seam's set → log → remove span (see LogApiCall). */
73
- removeHeader(key) {
74
- this.remove(key.name);
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
- hasHeader(key) {
77
- return this.has(key.name);
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.maskIfSecured}), keyed by each key's `name`.
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[] (mixed value types), so read by name and
105
- // narrow with the typeof-string guard rather than asserting a value type per key.
106
- const value = this.get(key.name);
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.maskIfSecured(value));
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.getHeader(key);
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.maskIfSecured(value));
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 in the current context.
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
- const store = this.storage.getStore();
196
- if (!store) {
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 from the current context.
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
- const store = this.storage.getStore();
206
- return store?.get(key);
274
+ this.rejectRegisteredName(key, 'getTrusted / getUntrusted / getAny');
275
+ return this.readByName(key);
207
276
  }
208
277
  /**
209
- * Remove a value from the current context.
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?.delete(key);
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
- * Used by XPromise to restore context.
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
- const store = this.storage.getStore();
259
- return store?.has(key) ?? false;
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":";;;AAAA,6CAAgD;AAChD,oDAA8F;AAG9F,0EAA0E;AAC1E,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAEtD;;;;;;;;;;;;;GAaG;AACH,MAAM,kBAAkB;IACZ,OAAO,CAAsC;IAErD;QACI,IAAI,CAAC,OAAO,GAAG,IAAI,+BAAiB,EAAoB,CAAC;IAC7D,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,GAAG,CAAI,EAAW;QACd,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CACX,qFAAqF;gBACrF,uFAAuF;gBACvF,+EAA+E,CAClF,CAAC;QACN,CAAC;QACD,yGAAyG;QACzG,MAAM,KAAK,GAAG,IAAI,GAAG,EAAe,CAAC;QACrC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACvC,CAAC;IAED;;OAEG;IACH,cAAc,CAAI,OAAyB,EAAE,EAAW;QACpD,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACzC,CAAC;IAED;;;;;OAKG;IACH,SAAS,CAAI,GAAkB;QAC3B,OAAO,IAAI,CAAC,GAAG,CAAI,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED;;;OAGG;IACH,SAAS,CAAI,GAAkB,EAAE,KAAQ;QACrC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,kGAAkG;IAClG,YAAY,CAAC,GAAkB;QAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,SAAS,CAAC,GAAkB;QACxB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,cAAc;QACV,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;QACzC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;YACnB,OAAO,MAAM,CAAC;QAClB,CAAC;QACD,4FAA4F;QAC5F,2FAA2F;QAC3F,oFAAoF;QACpF,4FAA4F;QAC5F,+FAA+F;QAC/F,KAAK,MAAM,GAAG,IAAI,0BAAc,CAAC,GAAG,EAAE,CAAC,aAAa,EAAE,EAAE,CAAC;YACrD,8EAA8E;YAC9E,kFAAkF;YAClF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAS,GAAG,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,EAAE,CAAC;gBACrC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;YACnD,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,wBAAwB;QACpB,MAAM,MAAM,GAAG,IAAI,GAAG,EAA2B,CAAC;QAClD,iGAAiG;QACjG,kGAAkG;QAClG,mGAAmG;QACnG,yFAAyF;QACzF,gGAAgG;QAChG,6FAA6F;QAC7F,+FAA+F;QAC/F,MAAM,OAAO,GAAG,uBAAW,CAAC,OAAO,EAAE,CAAC;QACtC,IAAI,OAAO,EAAE,CAAC;YACV,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACnC,CAAC;QACD,MAAM,OAAO,GAAG,uBAAW,CAAC,UAAU,EAAE,CAAC;QACzC,IAAI,OAAO,EAAE,CAAC;YACV,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;YACnB,OAAO,MAAM,CAAC;QAClB,CAAC;QACD,6FAA6F;QAC7F,+FAA+F;QAC/F,6FAA6F;QAC7F,0FAA0F;QAC1F,KAAK,MAAM,GAAG,IAAI,0BAAc,CAAC,GAAG,EAAE,CAAC,aAAa,EAAE,EAAE,CAAC;YACrD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YAClC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACxC,SAAS;YACb,CAAC;YACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC5B,IAAI,KAAK,EAAE,CAAC;oBACR,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;gBACnD,CAAC;YACL,CAAC;iBAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBACnC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAChC,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IAGD;;;;;OAKG;IACH,UAAU,CAAC,OAAoB;QAC3B,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;IACxC,CAAC;IAED,sFAAsF;IACtF,UAAU;QACN,OAAO,IAAI,CAAC,GAAG,CAAc,gBAAgB,CAAC,CAAC;IACnD,CAAC;IAED,yGAAyG;IACzG,UAAU,CAAC,IAAqB;QAC5B,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC;IAED;;OAEG;IACH,GAAG,CAAC,GAAW,EAAE,KAAU;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtC,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC/E,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,GAAG,CAAU,GAAW;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtC,OAAO,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,GAAW;QACd,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;IAED;;OAEG;IACH,KAAK;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtC,KAAK,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC;IAED;;;OAGG;IACH,WAAW;QACP,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtC,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,OAAO,IAAI,GAAG,EAAE,CAAC;QACrB,CAAC;QACD,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED;;;OAGG;IACH,UAAU,CAAC,OAAyB;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtC,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC/E,CAAC;QACD,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC3B,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;OAEG;IACH,MAAM;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;IAC9C,CAAC;IAED;;OAEG;IACH,GAAG,CAAC,GAAW;QACX,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtC,OAAO,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACH,QAAQ;QACJ,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,SAAS,CAAC;IACjD,CAAC;CAEJ;AAID;;;GAGG;AACU,QAAA,cAAc,GAAG,IAAI,kBAAkB,EAAE,CAAC","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, AnyContextKey } from '@webpieces/core-util';
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: AnyContextKey, value: unknown): void;
20
- remove(contextKey: AnyContextKey): void;
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.putHeader(contextKey, value);
26
+ RequestContext_1.RequestContext.putUntrusted(contextKey, value);
27
27
  }
28
28
  remove(contextKey) {
29
- RequestContext_1.RequestContext.removeHeader(contextKey);
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,UAAyB,EAAE,KAAc;QACzC,+BAAc,CAAC,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,CAAC,UAAyB;QAC5B,+BAAc,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAC5C,CAAC;CACJ;AAdD,oEAcC","sourcesContent":["import { ApiCallContext, AnyContextKey } 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: AnyContextKey, value: unknown): void {\n RequestContext.putHeader(contextKey, value);\n }\n\n remove(contextKey: AnyContextKey): void {\n RequestContext.removeHeader(contextKey);\n }\n}\n"]}
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[]; every transferred value is a wire
43
- // string, so read by name rather than asserting a value type on a generically-typed key.
44
- const value = RequestContext_1.RequestContext.get(key.name);
45
- if (value !== undefined && value !== null && value !== '') {
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.putHeader(core_util_1.WebpiecesCoreHeaders.HTTP_METHOD, request.method);
87
- RequestContext_1.RequestContext.putHeader(core_util_1.WebpiecesCoreHeaders.REQUEST_PATH, request.path);
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
- RequestContext_1.RequestContext.putHeader(key, values[0]);
94
+ this.acceptInbound(key, values[0]);
93
95
  }
94
96
  }
95
- if (!RequestContext_1.RequestContext.hasHeader(core_util_1.WebpiecesCoreHeaders.REQUEST_ID)) {
96
- RequestContext_1.RequestContext.putHeader(core_util_1.WebpiecesCoreHeaders.REQUEST_ID, this.generateRequestId());
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.putHeader(core_util_1.WebpiecesCoreHeaders.REQUEST_ID_SOURCE, svcName);
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.getHeader(core_util_1.RecorderKeys.RECORDER);
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), and
20
- // this method's contract is string-only, so read by name rather than via the typed getHeader.
21
- return RequestContext_1.RequestContext.get(key.name);
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.getHeader(key);
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,oFAAoF;QACpF,8FAA8F;QAC9F,OAAO,+BAAc,CAAC,GAAG,CAAS,GAAG,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED;;;;OAIG;IACH,gGAAgG;IAChG,SAAS,CAAC,GAAkB;QACxB,OAAO,+BAAc,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC;CACJ;AAjBD,oDAiBC","sourcesContent":["import { ContextKey, 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), and\n // this method's contract is string-only, so read by name rather than via the typed getHeader.\n return RequestContext.get<string>(key.name);\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.getHeader(key);\n }\n}\n"]}
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"]}