@webpieces/core-context 0.4.665 → 0.4.667

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.665",
3
+ "version": "0.4.667",
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.665",
25
+ "@webpieces/core-util": "0.4.667",
26
26
  "@inversifyjs/binding-decorators": "1.1.5",
27
27
  "inversify": "7.10.4",
28
28
  "reflect-metadata": "0.2.2"
@@ -42,3 +42,18 @@ export declare class HttpRequest {
42
42
  /** All values of a header. */
43
43
  getHeaderValues(key: AnyContextKey | string): string[] | undefined;
44
44
  }
45
+ /**
46
+ * RawHttpRequest - an {@link HttpRequest} on a `@Endpoint(..., { rawBody: true })` route, where
47
+ * {@link HttpRequest.raw} is PRESENT rather than optional.
48
+ *
49
+ * It exists so a vendor's webhook hook never writes `request.raw!` or an `if (!raw) throw`. The
50
+ * absence of the bytes is checked in exactly ONE place — `AuthFilter` 401s before it calls the hook —
51
+ * and this type is what carries the result of that check into the signature, so the bad state is
52
+ * unrepresentable downstream instead of re-guarded at every implementor.
53
+ *
54
+ * A TYPE, not a class: it narrows a field of an existing class rather than describing new data, so
55
+ * the transports keep building the one `HttpRequest` they always built.
56
+ */
57
+ export type RawHttpRequest = HttpRequest & {
58
+ readonly raw: RawRequest;
59
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"HttpRequest.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/HttpRequest.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;GAYG;AACH,MAAa,WAAW;IAEA;IACA;IAEA;IAQA;IAZpB,YACoB,MAAc,EACd,IAAY;IAC5B,iFAAiF;IACjE,OAA8B;IAC9C;;;;;;OAMG;IACa,GAAgB;QAXhB,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAEZ,YAAO,GAAP,OAAO,CAAuB;QAQ9B,QAAG,GAAH,GAAG,CAAa;IACjC,CAAC;IAEJ,8FAA8F;IAC9F,SAAS,CAAC,GAA2B;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACzC,OAAO,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/D,CAAC;IAED,8BAA8B;IAC9B,eAAe,CAAC,GAA2B;QACvC,MAAM,IAAI,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QACxF,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;CACJ;AA3BD,kCA2BC","sourcesContent":["import { ContextKey, AnyContextKey } from '@webpieces/core-util';\nimport { RawRequest } from './RawRequest';\n\n/**\n * HttpRequest - webpieces' TRANSPORT-NEUTRAL inbound request.\n *\n * This is @webpieces/http-routing's own request type (http-routing re-exports it), NOT\n * express's `req`. Each transport adapter (the express adapter in @webpieces/http-server, or\n * any other TypeScript web framework) builds an HttpRequest from its native request and hands\n * it to the router. Filters and the auth layer read it via `RequestContext.getRequest()`\n * instead of touching express — which is what lets the SAME filter chain run over HTTP and\n * in-process (tests build an HttpRequest carrying their credential).\n *\n * It lives in core-context (alongside RequestContext, which stores it in AsyncLocalStorage)\n * to avoid a core-context → http-routing cycle; it is a pure data holder (no express, no DI).\n */\nexport class HttpRequest {\n constructor(\n public readonly method: string,\n public readonly path: string,\n /** Header name (lowercased) -> values (HTTP allows multiple values per name). */\n public readonly headers: Map<string, string[]>,\n /**\n * The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`\n * route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:\n * `@AuthWebhook` has nothing to verify without it and 401s rather than waving the call\n * through. A spec driving a webhook route in-process supplies one here, the same way a spec\n * today supplies an `authorization` header.\n */\n public readonly raw?: RawRequest,\n ) {}\n\n /** First value of a header, looked up by ContextKey.httpHeader (or a raw lowercased name). */\n getHeader(key: AnyContextKey | string): string | undefined {\n const values = this.getHeaderValues(key);\n return values && values.length > 0 ? values[0] : undefined;\n }\n\n /** All values of a header. */\n getHeaderValues(key: AnyContextKey | string): string[] | undefined {\n const name = (typeof key === 'string' ? key : key.httpHeader ?? key.name).toLowerCase();\n return this.headers.get(name);\n }\n}\n"]}
1
+ {"version":3,"file":"HttpRequest.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/HttpRequest.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;GAYG;AACH,MAAa,WAAW;IAEA;IACA;IAEA;IAQA;IAZpB,YACoB,MAAc,EACd,IAAY;IAC5B,iFAAiF;IACjE,OAA8B;IAC9C;;;;;;OAMG;IACa,GAAgB;QAXhB,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAEZ,YAAO,GAAP,OAAO,CAAuB;QAQ9B,QAAG,GAAH,GAAG,CAAa;IACjC,CAAC;IAEJ,8FAA8F;IAC9F,SAAS,CAAC,GAA2B;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACzC,OAAO,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/D,CAAC;IAED,8BAA8B;IAC9B,eAAe,CAAC,GAA2B;QACvC,MAAM,IAAI,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QACxF,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;CACJ;AA3BD,kCA2BC","sourcesContent":["import { ContextKey, AnyContextKey } from '@webpieces/core-util';\nimport { RawRequest } from './RawRequest';\n\n/**\n * HttpRequest - webpieces' TRANSPORT-NEUTRAL inbound request.\n *\n * This is @webpieces/http-routing's own request type (http-routing re-exports it), NOT\n * express's `req`. Each transport adapter (the express adapter in @webpieces/http-server, or\n * any other TypeScript web framework) builds an HttpRequest from its native request and hands\n * it to the router. Filters and the auth layer read it via `RequestContext.getRequest()`\n * instead of touching express — which is what lets the SAME filter chain run over HTTP and\n * in-process (tests build an HttpRequest carrying their credential).\n *\n * It lives in core-context (alongside RequestContext, which stores it in AsyncLocalStorage)\n * to avoid a core-context → http-routing cycle; it is a pure data holder (no express, no DI).\n */\nexport class HttpRequest {\n constructor(\n public readonly method: string,\n public readonly path: string,\n /** Header name (lowercased) -> values (HTTP allows multiple values per name). */\n public readonly headers: Map<string, string[]>,\n /**\n * The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`\n * route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:\n * `@AuthWebhook` has nothing to verify without it and 401s rather than waving the call\n * through. A spec driving a webhook route in-process supplies one here, the same way a spec\n * today supplies an `authorization` header.\n */\n public readonly raw?: RawRequest,\n ) {}\n\n /** First value of a header, looked up by ContextKey.httpHeader (or a raw lowercased name). */\n getHeader(key: AnyContextKey | string): string | undefined {\n const values = this.getHeaderValues(key);\n return values && values.length > 0 ? values[0] : undefined;\n }\n\n /** All values of a header. */\n getHeaderValues(key: AnyContextKey | string): string[] | undefined {\n const name = (typeof key === 'string' ? key : key.httpHeader ?? key.name).toLowerCase();\n return this.headers.get(name);\n }\n}\n\n/**\n * RawHttpRequest - an {@link HttpRequest} on a `@Endpoint(..., { rawBody: true })` route, where\n * {@link HttpRequest.raw} is PRESENT rather than optional.\n *\n * It exists so a vendor's webhook hook never writes `request.raw!` or an `if (!raw) throw`. The\n * absence of the bytes is checked in exactly ONE place — `AuthFilter` 401s before it calls the hook —\n * and this type is what carries the result of that check into the signature, so the bad state is\n * unrepresentable downstream instead of re-guarded at every implementor.\n *\n * A TYPE, not a class: it narrows a field of an existing class rather than describing new data, so\n * the transports keep building the one `HttpRequest` they always built.\n */\nexport type RawHttpRequest = HttpRequest & { readonly raw: RawRequest };\n"]}
@@ -24,7 +24,7 @@ export declare class PendingTrustedValue {
24
24
  * identity.
25
25
  *
26
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
27
+ * EMPTY `AuthenticatedCaller.entries`, so a fully verified `@AuthJwt` request stamps no context entries at
28
28
  * all and would leave a forged `x-user-id` completely unopposed.
29
29
  *
30
30
  * ## The fix
@@ -37,7 +37,7 @@ exports.PendingTrustedValue = PendingTrustedValue;
37
37
  * identity.
38
38
  *
39
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
40
+ * EMPTY `AuthenticatedCaller.entries`, so a fully verified `@AuthJwt` request stamps no context entries at
41
41
  * all and would leave a forged `x-user-id` completely unopposed.
42
42
  *
43
43
  * ## The fix
@@ -1 +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"]}
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 `AuthenticatedCaller.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"]}
@@ -230,9 +230,12 @@ declare class RequestContextImpl {
230
230
  getRequest(): HttpRequest | undefined;
231
231
  /**
232
232
  * Store a value under a RAW STRING key — the escape hatch for the framework's own reserved,
233
- * UNREGISTERED slots ('__webpieces_http_request__', the AuthFilter principal, the Cloud Tasks
234
- * schedule frame). Those are internal plumbing, not context keys, so they have no ContextKey and
235
- * no trust level.
233
+ * UNREGISTERED slots ('__webpieces_http_request__', the Cloud Tasks schedule frame). Those are
234
+ * internal plumbing, not context keys, so they have no ContextKey and no trust level.
235
+ *
236
+ * The AuthFilter principal used to be one of these and is NOT any more: it is a proven fact, so
237
+ * it goes through `putTrusted` under `AUTHENTICATED_CALLER_KEY` like every other proven value.
238
+ * Reach for this only when the value genuinely has no trust level to state.
236
239
  *
237
240
  * REJECTS any name that belongs to a registered {@link ContextKey}. Without that check this
238
241
  * method is a complete bypass of the trust system — `put('userId', req.body.userId)` would forge
@@ -333,9 +333,12 @@ class RequestContextImpl {
333
333
  }
334
334
  /**
335
335
  * Store a value under a RAW STRING key — the escape hatch for the framework's own reserved,
336
- * UNREGISTERED slots ('__webpieces_http_request__', the AuthFilter principal, the Cloud Tasks
337
- * schedule frame). Those are internal plumbing, not context keys, so they have no ContextKey and
338
- * no trust level.
336
+ * UNREGISTERED slots ('__webpieces_http_request__', the Cloud Tasks schedule frame). Those are
337
+ * internal plumbing, not context keys, so they have no ContextKey and no trust level.
338
+ *
339
+ * The AuthFilter principal used to be one of these and is NOT any more: it is a proven fact, so
340
+ * it goes through `putTrusted` under `AUTHENTICATED_CALLER_KEY` like every other proven value.
341
+ * Reach for this only when the value genuinely has no trust level to state.
339
342
  *
340
343
  * REJECTS any name that belongs to a registered {@link ContextKey}. Without that check this
341
344
  * method is a complete bypass of the trust system — `put('userId', req.body.userId)` would forge
@@ -1 +1 @@
1
- {"version":3,"file":"RequestContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContext.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'async_hooks';\nimport { ContextKey, AnyContextKey, HeaderRegistry, ServiceInfo } from '@webpieces/core-util';\nimport { HttpRequest } from './HttpRequest';\nimport { CapturedContext, ContextCaptureAuthority, RestorableContext } from './CapturedContext';\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 * If you genuinely WANT a fresh empty scope inside an active one — work that must not inherit the\n * surrounding request's actionId/requestId — that is {@link runDetachedScope}, which says so by\n * name. This guard exists to stop the ACCIDENTAL empty scope, not the deliberate one.\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. If you ' +\n 'MEANT a fresh empty scope that does not inherit this one (a browser-log line, work ' +\n 'reconstructed from an external payload), use RequestContext.runDetachedScope(fn) and ' +\n 'write its values inside the closure with putTrusted/putUntrusted.',\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 * Open a NEW scope pre-loaded with a snapshot — the restore half of {@link copyContext}, for work\n * whose async chain was broken and re-rooted elsewhere (a queued job drained by a background loop,\n * a batch flushed on a timer, an event listener fired from a socket the request does not own). See\n * {@link CapturedContext} for the full list and for why the payload is opaque.\n *\n * A restored context legitimately contains TRUSTED values — reinstating what the original scope\n * proved is the entire point — so this cannot type-check its contents the way the trust verbs do.\n * The guarantee instead comes from the PAYLOAD: a {@link RestorableContext} can only be narrowed\n * out of a {@link CapturedContext}, which only {@link copyContext} produces, so there is no\n * hand-assembled Map to hand it and no way to forge one.\n *\n * The caller must SAY whether the proven identity travels — `snapshot.withTrusted()` (runs as that\n * user) or `snapshot.withoutTrusted()` (runs as the system, keeping only the trace fields). A bare\n * `CapturedContext` is deliberately not accepted; see {@link CapturedContext} for the three-case\n * table and why the wide branch is spelled out rather than defaulted.\n *\n * The snapshot is copied into a fresh store, so writes inside `fn` stay inside `fn` and the\n * snapshot stays reusable.\n *\n * Deliberately NOT guarded against nesting the way {@link run} is. `run`'s guard exists because a\n * second EMPTY scope shadowing the first is always a bug; here the inner scope is a faithful copy\n * of a real one, which is the whole point — a worker that restores a snapshot inside a scope it\n * opened per job is correct, not a mistake. Prefer this over {@link restoreContext} unless you\n * specifically need the CURRENT scope overwritten in place.\n */\n runWithContext<T>(captured: RestorableContext, fn: () => T): T {\n return this.storage.run(captured.toFreshStore(ContextCaptureAuthority.INTERNAL), fn);\n }\n\n /**\n * Open a FRESH, EMPTY, NESTED scope. Nothing is inherited from the enclosing scope, and nothing\n * crosses the boundary as data — every value the work runs under is WRITTEN INSIDE `fn`, through\n * the ordinary trust-typed verbs:\n *\n * ```typescript\n * RequestContext.runDetachedScope(() => {\n * RequestContext.putUntrusted(WebpiecesCoreHeaders.ACTION_ID, line.actionId);\n * emit(); // runs under exactly what this closure wrote, and nothing else\n * });\n * ```\n *\n * ## When you want THIS and not {@link runWithContext}\n *\n * The two look similar and are opposites. `runWithContext` faithfully RE-ROOTS a real snapshot of a\n * real scope, for work whose async chain was broken (a queued job, a timer flush) — it exists to\n * PRESERVE a context. This one exists to DISCARD one: the values do not come from any scope this\n * process ever had, they were reconstructed from somewhere else, and inheriting the ambient scope\n * would be actively wrong.\n *\n * The live case is a browser-log shipper. A batch of browser lines arrives on one HTTP request; each\n * line carries the context the BROWSER captured when it was written, and a single batch routinely\n * spans several user actions. Emitting a line under the shipping request's own scope would stamp\n * every line with that request's actionId and requestId, silently destroying the ability to grep an\n * action while the feature still appeared to work. So each line is emitted detached, under exactly\n * the keys the closure re-stated from the browser's payload.\n *\n * ## Why it MAY nest when {@link run} may not\n *\n * `run`'s nesting guard is right and is not softened here. It refuses a second EMPTY scope because\n * there an empty scope is always an ACCIDENT — a transport opening the request scope twice, whose\n * only effect is to hide the outer scope's values and mint a second request id. Here an empty scope\n * is the thing that was ASKED for, in a distinctly-named verb, and the caller is normally already\n * inside a request scope (the shipper's own). A guard would refuse the only situation the method has.\n *\n * ## No Map-taking form, ever\n *\n * There is deliberately no overload accepting a `Map`, an object, or an array of entries. That was\n * the DELETED `runWithContext(map, fn)`, and it was a forgery path: a hand-built map is\n * indistinguishable from a genuine snapshot, so `new Map([['userId','victim']])` minted a proven\n * identity in one line without ever typing a trust verb. Writing the values INSIDE the closure is\n * what closes it — a loop over a mixed `AnyContextKey[]` must branch on `key.isTrusted()` before it\n * can write anything, and `putUntrusted` does not compile for a trusted key, so code fed by a\n * BROWSER (which proves nothing) cannot fabricate a proven value. (See\n * `DetachedScopeCompileAssertions`.)\n *\n * That is a limit on the SOURCE, not on the key. `putTrusted` inside a detached scope is ordinary\n * and correct whenever the caller has actually proven the value — a verified JWT claim, or the\n * signed-webhook case where Twilio/WhatsApp proves the phone number and the app looks up the\n * userId. Trust is tamper-resistance, not secrecy (a trusted `userId` is a plain, fully-logged\n * GUID; redaction is the separate `maskInLogs` axis on the key).\n *\n * SYNC AND ASYNC BOTH: `fn` may return a promise, and the detached scope follows every `await`\n * inside it exactly as `run`/`runWithContext` do — same `AsyncLocalStorage.run` underneath. The\n * enclosing scope is reinstated for everything after the synchronous return, INCLUDING when `fn`\n * throws (AsyncLocalStorage unwinds the store as the frame unwinds); an async `fn` that is not\n * awaited will therefore keep the detached scope for its own continuation while the caller has\n * already resumed under the enclosing one, which is the intended and only sane reading of \"detached\".\n */\n runDetachedScope<T>(fn: () => T): T {\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 * 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 * Snapshot this scope so the work you are about to hand off keeps its request id, log fields and\n * proven identity. The ONLY producer of a {@link CapturedContext} — which is what makes the\n * restore side unforgeable, since there is no other way to obtain the payload it accepts.\n *\n * Outside a `run(...)` block this returns an EMPTY snapshot rather than throwing: capturing \"no\n * context\" is a legitimate thing for a background caller to do, and restoring it simply installs\n * nothing.\n *\n * The snapshot is a defensive COPY — writes to this context after capturing do not reach it.\n */\n copyContext(): CapturedContext {\n const store = this.storage.getStore();\n return CapturedContext.capture(ContextCaptureAuthority.INTERNAL, store ?? new Map());\n }\n\n /**\n * Overwrite the ACTIVE scope with a snapshot. The in-place twin of {@link runWithContext}, and the\n * one you almost never want: prefer `runWithContext`, which gives the restored work its OWN scope\n * and cannot disturb the caller's. Reach for this only when something else owns the scope and it\n * must be re-pointed in place.\n *\n * OVERWRITE, not merge — `clear()` runs first, so every entry the active scope holds and the\n * snapshot does not is DROPPED. That includes the empty case:\n * `restoreContext(copyContext().withTrusted())` taken outside a scope wipes the request id and\n * every proven identity from a live request, and says nothing. That is faithful (a snapshot\n * restores exactly what it captured) but it is the sharp edge of this method and the reason\n * `runWithContext` is the default.\n *\n * Takes only a {@link RestorableContext} for the reason spelled out there — the DELETED Map-taking\n * form let `new Map([['userId','victim']])` forge a proven identity in one line — and that type\n * exists only via `withTrusted()` / `withoutTrusted()`, so this call site states whether the proven\n * identity survives the re-point.\n *\n * @throws Error when no RequestContext is active.\n */\n restoreContext(captured: RestorableContext): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error(\n 'No context available to restore into. Either open one with RequestContext.run(...) ' +\n 'first, or use RequestContext.runWithContext(captured.withTrusted(), fn) — or ' +\n '.withoutTrusted() — which opens its own.',\n );\n }\n captured.restoreInto(ContextCaptureAuthority.INTERNAL, store);\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
+ {"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';\nimport { CapturedContext, ContextCaptureAuthority, RestorableContext } from './CapturedContext';\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 * If you genuinely WANT a fresh empty scope inside an active one — work that must not inherit the\n * surrounding request's actionId/requestId — that is {@link runDetachedScope}, which says so by\n * name. This guard exists to stop the ACCIDENTAL empty scope, not the deliberate one.\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. If you ' +\n 'MEANT a fresh empty scope that does not inherit this one (a browser-log line, work ' +\n 'reconstructed from an external payload), use RequestContext.runDetachedScope(fn) and ' +\n 'write its values inside the closure with putTrusted/putUntrusted.',\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 * Open a NEW scope pre-loaded with a snapshot — the restore half of {@link copyContext}, for work\n * whose async chain was broken and re-rooted elsewhere (a queued job drained by a background loop,\n * a batch flushed on a timer, an event listener fired from a socket the request does not own). See\n * {@link CapturedContext} for the full list and for why the payload is opaque.\n *\n * A restored context legitimately contains TRUSTED values — reinstating what the original scope\n * proved is the entire point — so this cannot type-check its contents the way the trust verbs do.\n * The guarantee instead comes from the PAYLOAD: a {@link RestorableContext} can only be narrowed\n * out of a {@link CapturedContext}, which only {@link copyContext} produces, so there is no\n * hand-assembled Map to hand it and no way to forge one.\n *\n * The caller must SAY whether the proven identity travels — `snapshot.withTrusted()` (runs as that\n * user) or `snapshot.withoutTrusted()` (runs as the system, keeping only the trace fields). A bare\n * `CapturedContext` is deliberately not accepted; see {@link CapturedContext} for the three-case\n * table and why the wide branch is spelled out rather than defaulted.\n *\n * The snapshot is copied into a fresh store, so writes inside `fn` stay inside `fn` and the\n * snapshot stays reusable.\n *\n * Deliberately NOT guarded against nesting the way {@link run} is. `run`'s guard exists because a\n * second EMPTY scope shadowing the first is always a bug; here the inner scope is a faithful copy\n * of a real one, which is the whole point — a worker that restores a snapshot inside a scope it\n * opened per job is correct, not a mistake. Prefer this over {@link restoreContext} unless you\n * specifically need the CURRENT scope overwritten in place.\n */\n runWithContext<T>(captured: RestorableContext, fn: () => T): T {\n return this.storage.run(captured.toFreshStore(ContextCaptureAuthority.INTERNAL), fn);\n }\n\n /**\n * Open a FRESH, EMPTY, NESTED scope. Nothing is inherited from the enclosing scope, and nothing\n * crosses the boundary as data — every value the work runs under is WRITTEN INSIDE `fn`, through\n * the ordinary trust-typed verbs:\n *\n * ```typescript\n * RequestContext.runDetachedScope(() => {\n * RequestContext.putUntrusted(WebpiecesCoreHeaders.ACTION_ID, line.actionId);\n * emit(); // runs under exactly what this closure wrote, and nothing else\n * });\n * ```\n *\n * ## When you want THIS and not {@link runWithContext}\n *\n * The two look similar and are opposites. `runWithContext` faithfully RE-ROOTS a real snapshot of a\n * real scope, for work whose async chain was broken (a queued job, a timer flush) — it exists to\n * PRESERVE a context. This one exists to DISCARD one: the values do not come from any scope this\n * process ever had, they were reconstructed from somewhere else, and inheriting the ambient scope\n * would be actively wrong.\n *\n * The live case is a browser-log shipper. A batch of browser lines arrives on one HTTP request; each\n * line carries the context the BROWSER captured when it was written, and a single batch routinely\n * spans several user actions. Emitting a line under the shipping request's own scope would stamp\n * every line with that request's actionId and requestId, silently destroying the ability to grep an\n * action while the feature still appeared to work. So each line is emitted detached, under exactly\n * the keys the closure re-stated from the browser's payload.\n *\n * ## Why it MAY nest when {@link run} may not\n *\n * `run`'s nesting guard is right and is not softened here. It refuses a second EMPTY scope because\n * there an empty scope is always an ACCIDENT — a transport opening the request scope twice, whose\n * only effect is to hide the outer scope's values and mint a second request id. Here an empty scope\n * is the thing that was ASKED for, in a distinctly-named verb, and the caller is normally already\n * inside a request scope (the shipper's own). A guard would refuse the only situation the method has.\n *\n * ## No Map-taking form, ever\n *\n * There is deliberately no overload accepting a `Map`, an object, or an array of entries. That was\n * the DELETED `runWithContext(map, fn)`, and it was a forgery path: a hand-built map is\n * indistinguishable from a genuine snapshot, so `new Map([['userId','victim']])` minted a proven\n * identity in one line without ever typing a trust verb. Writing the values INSIDE the closure is\n * what closes it — a loop over a mixed `AnyContextKey[]` must branch on `key.isTrusted()` before it\n * can write anything, and `putUntrusted` does not compile for a trusted key, so code fed by a\n * BROWSER (which proves nothing) cannot fabricate a proven value. (See\n * `DetachedScopeCompileAssertions`.)\n *\n * That is a limit on the SOURCE, not on the key. `putTrusted` inside a detached scope is ordinary\n * and correct whenever the caller has actually proven the value — a verified JWT claim, or the\n * signed-webhook case where Twilio/WhatsApp proves the phone number and the app looks up the\n * userId. Trust is tamper-resistance, not secrecy (a trusted `userId` is a plain, fully-logged\n * GUID; redaction is the separate `maskInLogs` axis on the key).\n *\n * SYNC AND ASYNC BOTH: `fn` may return a promise, and the detached scope follows every `await`\n * inside it exactly as `run`/`runWithContext` do — same `AsyncLocalStorage.run` underneath. The\n * enclosing scope is reinstated for everything after the synchronous return, INCLUDING when `fn`\n * throws (AsyncLocalStorage unwinds the store as the frame unwinds); an async `fn` that is not\n * awaited will therefore keep the detached scope for its own continuation while the caller has\n * already resumed under the enclosing one, which is the intended and only sane reading of \"detached\".\n */\n runDetachedScope<T>(fn: () => T): T {\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 * 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 Cloud Tasks schedule frame). Those are\n * internal plumbing, not context keys, so they have no ContextKey and no trust level.\n *\n * The AuthFilter principal used to be one of these and is NOT any more: it is a proven fact, so\n * it goes through `putTrusted` under `AUTHENTICATED_CALLER_KEY` like every other proven value.\n * Reach for this only when the value genuinely has no trust level to state.\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 * Snapshot this scope so the work you are about to hand off keeps its request id, log fields and\n * proven identity. The ONLY producer of a {@link CapturedContext} — which is what makes the\n * restore side unforgeable, since there is no other way to obtain the payload it accepts.\n *\n * Outside a `run(...)` block this returns an EMPTY snapshot rather than throwing: capturing \"no\n * context\" is a legitimate thing for a background caller to do, and restoring it simply installs\n * nothing.\n *\n * The snapshot is a defensive COPY — writes to this context after capturing do not reach it.\n */\n copyContext(): CapturedContext {\n const store = this.storage.getStore();\n return CapturedContext.capture(ContextCaptureAuthority.INTERNAL, store ?? new Map());\n }\n\n /**\n * Overwrite the ACTIVE scope with a snapshot. The in-place twin of {@link runWithContext}, and the\n * one you almost never want: prefer `runWithContext`, which gives the restored work its OWN scope\n * and cannot disturb the caller's. Reach for this only when something else owns the scope and it\n * must be re-pointed in place.\n *\n * OVERWRITE, not merge — `clear()` runs first, so every entry the active scope holds and the\n * snapshot does not is DROPPED. That includes the empty case:\n * `restoreContext(copyContext().withTrusted())` taken outside a scope wipes the request id and\n * every proven identity from a live request, and says nothing. That is faithful (a snapshot\n * restores exactly what it captured) but it is the sharp edge of this method and the reason\n * `runWithContext` is the default.\n *\n * Takes only a {@link RestorableContext} for the reason spelled out there — the DELETED Map-taking\n * form let `new Map([['userId','victim']])` forge a proven identity in one line — and that type\n * exists only via `withTrusted()` / `withoutTrusted()`, so this call site states whether the proven\n * identity survives the re-point.\n *\n * @throws Error when no RequestContext is active.\n */\n restoreContext(captured: RestorableContext): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error(\n 'No context available to restore into. Either open one with RequestContext.run(...) ' +\n 'first, or use RequestContext.runWithContext(captured.withTrusted(), fn) — or ' +\n '.withoutTrusted() — which opens its own.',\n );\n }\n captured.restoreInto(ContextCaptureAuthority.INTERNAL, store);\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"]}
package/src/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { RequestContext } from './RequestContext';
2
2
  export type { CapturedContext } from './CapturedContext';
3
3
  export type { RestorableContext } from './CapturedContext';
4
4
  export { RequestContextApiCallContext } from './RequestContextApiCallContext';
5
- export { HttpRequest } from './HttpRequest';
5
+ export { HttpRequest, RawHttpRequest } from './HttpRequest';
6
6
  export { RawRequest } from './RawRequest';
7
7
  export { provideSingletonDefaultForApi } from './provide';
8
8
  export { Provider } from './provide';
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;AAiBvB,oGAAoG;AACpG,uGAAuG;AACvG,4FAA4F;AAC5F,+EAA8E;AAArE,4IAAA,4BAA4B,OAAA;AACrC,mGAAmG;AACnG,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,qGAAqG;AACrG,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AAEnB,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// The OPAQUE snapshot type that copyContext() produces and restoreContext()/runWithContext() accept.\n//\n// `export type`, NOT `export` — deliberately. Consumers need to NAME it (a field, a queue entry, a\n// parameter) and nothing more. A VALUE export would hand them the class object, and with it the static\n// `capture(...)`, whose capability token a cast can supply even though this barrel never exports the\n// token's type: `CapturedContext.capture(null as never, new Map([['userId','victim']]))` would compile\n// and forge a proven identity — the exact hole this whole change closes. A type-only export removes the\n// class object from the package surface, so there is no factory to reach and copyContext() really is\n// the only producer. (ContextCaptureAuthority is not exported here in any form.)\nexport type { CapturedContext } from './CapturedContext';\n// The narrowed snapshot — what withTrusted() / withoutTrusted() produce and what\n// runWithContext()/restoreContext() accept. A bare CapturedContext is NOT accepted by either, so every\n// call site states whether the proven identity travels with the work (`grep -rn withTrusted`) or is\n// deliberately dropped (`grep -rn withoutTrusted`). Type-only for the same reason as above: with no\n// class object on the surface there is no `of(...)` factory to reach, cast or not.\nexport type { RestorableContext } from './CapturedContext';\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// The verbatim bytes + absolute url a webhook SIGNATURE is computed over ({ rawBody: true } routes).\nexport { RawRequest } from './RawRequest';\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"]}
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;AAiBvB,oGAAoG;AACpG,uGAAuG;AACvG,4FAA4F;AAC5F,+EAA8E;AAArE,4IAAA,4BAA4B,OAAA;AACrC,mGAAmG;AACnG,6CAA4D;AAAnD,0GAAA,WAAW,OAAA;AACpB,qGAAqG;AACrG,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AAEnB,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// The OPAQUE snapshot type that copyContext() produces and restoreContext()/runWithContext() accept.\n//\n// `export type`, NOT `export` — deliberately. Consumers need to NAME it (a field, a queue entry, a\n// parameter) and nothing more. A VALUE export would hand them the class object, and with it the static\n// `capture(...)`, whose capability token a cast can supply even though this barrel never exports the\n// token's type: `CapturedContext.capture(null as never, new Map([['userId','victim']]))` would compile\n// and forge a proven identity — the exact hole this whole change closes. A type-only export removes the\n// class object from the package surface, so there is no factory to reach and copyContext() really is\n// the only producer. (ContextCaptureAuthority is not exported here in any form.)\nexport type { CapturedContext } from './CapturedContext';\n// The narrowed snapshot — what withTrusted() / withoutTrusted() produce and what\n// runWithContext()/restoreContext() accept. A bare CapturedContext is NOT accepted by either, so every\n// call site states whether the proven identity travels with the work (`grep -rn withTrusted`) or is\n// deliberately dropped (`grep -rn withoutTrusted`). Type-only for the same reason as above: with no\n// class object on the surface there is no `of(...)` factory to reach, cast or not.\nexport type { RestorableContext } from './CapturedContext';\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, RawHttpRequest } from './HttpRequest';\n// The verbatim bytes + absolute url a webhook SIGNATURE is computed over ({ rawBody: true } routes).\nexport { RawRequest } from './RawRequest';\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"]}