@webpieces/core-context 0.4.863 → 0.4.865

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.863",
3
+ "version": "0.4.865",
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.863",
25
+ "@webpieces/core-util": "0.4.865",
26
26
  "@inversifyjs/binding-decorators": "1.1.5",
27
27
  "inversify": "7.10.4",
28
28
  "reflect-metadata": "0.2.2"
@@ -157,7 +157,7 @@ export declare class CapturedContext {
157
157
  * parameter makes the two intents equally easy to type and impossible to grep, and a defaulted one
158
158
  * makes the permissive branch the shortest thing to write — `.claude/rules/no-backwards-compat.md` shim
159
159
  * shape #5, "a widening
160
- * that is an ABSENCE rather than a token", the same reason `@WpAuthJwt({allRolesAllowed: true})` says
160
+ * that is an ABSENCE rather than a token", the same reason `AuthorizationType.ALL_USERS` says
161
161
  * the wide grant out loud. As a transform on the SNAPSHOT rather than a second capture mechanism it
162
162
  * composes with BOTH consumers — `runWithContext` and `restoreContext` — for free.
163
163
  *
@@ -180,7 +180,7 @@ class CapturedContext {
180
180
  * parameter makes the two intents equally easy to type and impossible to grep, and a defaulted one
181
181
  * makes the permissive branch the shortest thing to write — `.claude/rules/no-backwards-compat.md` shim
182
182
  * shape #5, "a widening
183
- * that is an ABSENCE rather than a token", the same reason `@WpAuthJwt({allRolesAllowed: true})` says
183
+ * that is an ABSENCE rather than a token", the same reason `AuthorizationType.ALL_USERS` says
184
184
  * the wide grant out loud. As a transform on the SNAPSHOT rather than a second capture mechanism it
185
185
  * composes with BOTH consumers — `runWithContext` and `restoreContext` — for free.
186
186
  *
@@ -1 +1 @@
1
- {"version":3,"file":"CapturedContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/CapturedContext.ts"],"names":[],"mappings":"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","sourcesContent":["import { HeaderRegistry } from '@webpieces/core-util';\n\n/**\n * A capability TOKEN, not data — the thing you must be holding to build a {@link CapturedContext} or\n * to unpack one.\n *\n * It exists because `CapturedContext` needs a producer that {@link RequestContext} (a different class,\n * in a different file) can call, and TypeScript's `private` is class-scoped: a plain public\n * `CapturedContext.capture(map)` would be exactly the hand-assembled-payload hole this class exists to\n * close. So the producer takes a token whose constructor is private and whose only instance is\n * {@link INTERNAL}, and this module is deliberately NOT re-exported from the package barrel — in any\n * form, type or value.\n *\n * On its own that stops a consumer NAMING the token but not SUPPLYING one, since `null as never`\n * type-checks. It is the barrel's `export type { CapturedContext }` that finishes the job: with no\n * class object on the package surface there is no `capture(...)` to call, cast or not. The two halves\n * are load-bearing together, which is why each names the other.\n *\n * Per CLAUDE.md this is a class rather than a `Symbol` or an object literal: it is nominal (the private\n * `brand` field stops a structurally-identical `{}` from satisfying it) and it has exactly one\n * instantiation point.\n */\nexport class ContextCaptureAuthority {\n /**\n * Nominal brand. Without a private member the class is structurally `{}`, and any object at all\n * would typecheck as an authority.\n */\n private readonly brand: string = 'webpieces.context-capture';\n\n /** The ONE token that exists. The constructor below is private, so no other can be made. */\n static readonly INTERNAL = new ContextCaptureAuthority();\n\n private constructor() {}\n\n /** Kept honest — the brand is read here so it is a real field, not a type-only fiction. */\n describe(): string {\n return this.brand;\n }\n}\n\n/**\n * CapturedContext — an OPAQUE snapshot of a {@link RequestContext} scope.\n *\n * ## THE THREE CASES — pick by where the values came from and what may keep the identity\n *\n * | you have | you want | write |\n * |---|---|---|\n * | a genuine prior scope | ALL of it, trusted values included | `runWithContext(snapshot.withTrusted(), fn)` |\n * | a genuine prior scope | the trace fields but NOT the identity | `runWithContext(snapshot.withoutTrusted(), fn)` |\n * | values from OUTSIDE this process | exactly what you re-state, nothing inherited | `RequestContext.runDetachedScope(fn)` |\n *\n * Row 1 is a faithful re-root of a broken async chain — the work continues AS that user. Row 2 is a\n * deliberate PRIVILEGE DROP: a background job keeps `requestId`/`actionId` so it stays greppable, and\n * loses `userId`/`orgId`/roles because it runs as the system (see {@link withoutTrusted}). Row 3 is a\n * different question entirely — the values were never in a scope here, so there is no snapshot to take;\n * see `RequestContext.runDetachedScope`, where each value is written inside the closure with the trust\n * verbs.\n *\n * There is NO bare form of rows 1 and 2. `copyContext()` hands back a `CapturedContext`, and\n * `runWithContext`/`restoreContext` do not accept one — they take the {@link RestorableContext} that\n * {@link withTrusted} and {@link withoutTrusted} produce. Every call site therefore STATES whether the\n * proven identity travels, and neither intent is shorter to type than the other. That is\n * `.claude/rules/no-backwards-compat.md` shim\n * shape #5 applied here: a bare snapshot silently carrying a user identity is a widening that is an\n * absence rather than a token, and it is ungreppable. Now `grep -rn withTrusted` enumerates every place\n * an identity crosses a scope boundary and `grep -rn withoutTrusted` every deliberate drop.\n *\n * ## What it is for\n *\n * `AsyncLocalStorage` follows `await`, `.then()` and ordinary callbacks on its own, so the vast\n * majority of code never needs this. What it does NOT follow is work whose async chain was BROKEN and\n * re-rooted somewhere else: an item pushed onto an in-memory queue during a request and drained later\n * by a background loop, a batch flushed on a scheduler tick, an `EventEmitter` listener fired from a\n * socket the request does not own, a retry re-armed from a top-level timer, a hand-off to a worker\n * pool. In each of those the work executes under a DIFFERENT (or no) context, so the request id, the\n * log fields and the proven identity would silently vanish from everything the work logs or calls.\n *\n * The answer is two halves: `RequestContext.copyContext()` where the work is ENQUEUED, and\n * `RequestContext.runWithContext(captured.withTrusted(), fn)` — or `.withoutTrusted()`, or\n * `restoreContext(...)` of either — where it RUNS. The narrowing is not optional; see the table above.\n *\n * ## Why it is opaque instead of a `Map<string, unknown>`\n *\n * A restored context legitimately contains TRUSTED values — reinstating what the original scope had\n * proven is the entire point — so the restore side cannot type-check its payload the way\n * `putTrusted`/`getTrusted` do. That left the `Map`-taking signature that this class DELETED (a\n * now-removed `setContext(map)`, and `runWithContext(map, fn)`) as a complete bypass of the trust\n * system: handing it `new Map([['userId', 'victim']])` forged a proven identity in one line, without\n * ever typing a trust verb, and the only thing standing against it was a doc comment saying \"the Map\n * must come from copyContext()\". An agent picks whatever compiles, so a doc comment is not\n * enforcement.\n *\n * Making the PAYLOAD opaque solves it without type-checking the contents: the only way to obtain one is\n * a real capture of a real scope, so whatever it holds was, by construction, already in a context that\n * something legitimately wrote. Concretely:\n *\n * - the constructor is `private`, and there is no public factory — {@link capture} demands a\n * {@link ContextCaptureAuthority} that cannot be named outside this package;\n * - the package barrel exports this class as a TYPE ONLY, so a consumer never receives the class\n * object at all and cannot reach `capture` even with a cast. That second half matters: a token whose\n * TYPE is unexported still stops nothing on its own, because `capture(null as never, forgedMap)`\n * type-checks. Withholding the class object is what actually closes it;\n * - the entries live in `#entries`, a genuine ECMAScript private field, so they are unreachable at\n * RUNTIME as well as at compile time — no `Object.keys`, no cast, no index signature;\n * - the map is defensively copied ON CAPTURE, again on each NARROWING, and again ON RESTORE, so a\n * caller who still holds the live context (or who keeps writing to it after capturing) cannot reach\n * through the snapshot, narrowing never mutates the capture it came from, and a snapshot can be\n * restored repeatedly without the first restore's mutations bleeding into the second.\n *\n * There is deliberately no reader: nothing hands the entries back out. That is why `getAll()` is gone\n * rather than re-typed to return one of these — a `CapturedContext` you cannot read is useless as a\n * `getAll`, and a readable one would be the raw enumeration of every trusted value all over again.\n *\n * The one residual: a consumer holding a NARROWED snapshot can still cast a token into\n * {@link RestorableContext.toFreshStore} and read the entries back out as a plain Map. That is knowingly accepted, and it is the same asymmetry\n * `RequestContext.getAny` states — FORGING a trusted value is the dangerous direction and is closed\n * here; reading one you were already legitimately handed, without saying `getTrusted`, costs you\n * nothing but the type. Closing it too would mean no method could take the token at all, which is to\n * say no restore could exist.\n */\nexport class CapturedContext {\n /**\n * A real ECMAScript private field, not a TypeScript `private`. The distinction matters here: `#`\n * is enforced by the runtime, so the snapshot's contents cannot be reached by a cast, by\n * `Object.entries`, or by `as unknown as { entries: Map<string, unknown> }`.\n */\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; each ContextKey carries its own value type and the typed verbs re-apply it on read\n readonly #entries: Map<string, unknown>;\n\n /**\n * PRIVATE — a CapturedContext can only come from {@link capture}, which in turn can only be called\n * by code holding a {@link ContextCaptureAuthority}. Copies the map so the snapshot is never a\n * window onto a live store.\n */\n // webpieces-disable no-any-unknown -- see #entries\n private constructor(entries: Map<string, unknown>) {\n this.#entries = new Map(entries);\n }\n\n /**\n * The ONLY producer. `authority` is a compile-time capability, not a runtime check — so it is\n * referenced below only to keep it from being an unused parameter. Note the token alone is not the\n * guarantee (a cast can supply one); the guarantee is that the barrel exports this class as a TYPE\n * ONLY, so no consumer ever holds the class object this static hangs off. See the class doc.\n */\n // webpieces-disable no-any-unknown -- see #entries\n // webpieces-disable no-function-outside-class -- static factory standing in for the (private) constructor; making it an instance method would mean an instance already existed, which is the thing being created\n static capture(\n authority: ContextCaptureAuthority,\n live: Map<string, unknown>,\n ): CapturedContext {\n void authority;\n return new CapturedContext(live);\n }\n\n /**\n * Carry EVERY value onward, the proven identity included — the faithful re-root. The work runs AS\n * that user: `getTrusted(USER_ID)` inside it answers exactly what it answered in the original scope,\n * which is the entire point when a request's own continuation was re-rooted onto a queue or a timer.\n *\n * Said OUT LOUD, because it is the wide branch. A bare snapshot is deliberately not accepted by\n * `runWithContext`/`restoreContext` (see the class doc), so the identity never crosses a scope\n * boundary by default or by omission, and `grep -rn withTrusted` enumerates every place it does.\n *\n * NON-MUTATING, like its sibling — the receiver is unchanged, so ONE snapshot can be narrowed both\n * ways at two different call sites.\n */\n withTrusted(): RestorableContext {\n return RestorableContext.of(ContextCaptureAuthority.INTERNAL, this.#entries);\n }\n\n /**\n * Carry only the UNTRUSTED values — a deliberate PRIVILEGE DROP.\n *\n * ```typescript\n * const snapshot = RequestContext.copyContext();\n * RequestContext.runWithContext(snapshot.withTrusted(), fn); // runs AS that user\n * RequestContext.runWithContext(snapshot.withoutTrusted(), fn); // runs as the SYSTEM\n * ```\n *\n * The case: a background job or fire-and-forget task spawned during a request should keep the\n * untrusted trace fields — `requestId`, `actionId` — so its log lines are still greppable back to\n * the click that caused them, but it must NOT keep `userId` / `orgId` / roles, because it executes\n * as the system rather than as that user. Carrying the proven identity onward would make every\n * downstream authorization decision think the user is still on the other end of the wire.\n *\n * A METHOD PAIR, never a `keepTrusted: boolean` on {@link RequestContext.runWithContext}: a\n * parameter makes the two intents equally easy to type and impossible to grep, and a defaulted one\n * makes the permissive branch the shortest thing to write — `.claude/rules/no-backwards-compat.md` shim\n * shape #5, \"a widening\n * that is an ABSENCE rather than a token\", the same reason `@WpAuthJwt({allRolesAllowed: true})` says\n * the wide grant out loud. As a transform on the SNAPSHOT rather than a second capture mechanism it\n * composes with BOTH consumers — `runWithContext` and `restoreContext` — for free.\n *\n * NON-MUTATING: the receiver is untouched, so one snapshot can be used both ways.\n *\n * NO AUTHORITY TOKEN, deliberately, and it must not grow one. The token on {@link capture} exists\n * because CONSTRUCTING a snapshot from arbitrary entries forges trust. This direction only ever\n * REMOVES entries: whatever survives was already in a real capture of a real scope, so the result\n * is strictly less privileged than the object the caller is already holding. Dropping cannot forge.\n *\n * WHAT SURVIVES is exactly \"registered as an UNTRUSTED {@link ContextKey}\". Trusted keys go, and so\n * do names the {@link HeaderRegistry} does not know — the framework's reserved slots (the\n * `HttpRequest`, the AuthFilter principal, the Cloud Tasks schedule frame), which carry no declared\n * trust and are the caller's identity and connection rather than trace fields. A privilege drop\n * that guessed in the permissive direction would not be one. For the same reason, with no registry\n * configured NOTHING is knowably untrusted and the result is empty — always the safe answer, since\n * this method's only job is to remove.\n */\n withoutTrusted(): RestorableContext {\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; see #entries\n const kept = new Map<string, unknown>();\n if (!HeaderRegistry.isConfigured()) {\n return RestorableContext.of(ContextCaptureAuthority.INTERNAL, kept);\n }\n const registry = HeaderRegistry.get();\n for (const name of this.#entries.keys()) {\n const key = registry.findByName(name);\n if (key && !key.isTrusted()) {\n kept.set(name, this.#entries.get(name));\n }\n }\n return RestorableContext.of(ContextCaptureAuthority.INTERNAL, kept);\n }\n\n /**\n * How many entries the snapshot holds. The one thing it will tell you about itself — a count is\n * not a value, so it leaks nothing, and it lets a caller (and a test) see that a capture taken\n * outside an active scope is simply empty rather than an error.\n */\n size(): number {\n return this.#entries.size;\n }\n}\n\n/**\n * A snapshot whose TRUST INTENT has been stated — the only thing `RequestContext.restoreContext` and\n * `RequestContext.runWithContext` accept.\n *\n * It exists to make the wide choice unskippable. A single type would have meant\n * `runWithContext(snapshot, fn)` compiling next to `runWithContext(snapshot.withTrusted(), fn)`: two\n * spellings of one thing (shim shape #1), with the shorter one silently carrying a user identity into\n * work that may have no business running as that user. Splitting the type deletes the default — a\n * capture is inert until it says which it means — so there is exactly one spelling per intent, and\n * `grep -rn withTrusted` / `grep -rn withoutTrusted` enumerate the two populations of call sites.\n *\n * Two CLASSES rather than a phantom type parameter on {@link CapturedContext}, even though this repo\n * uses that trick on `ContextKey<V, T extends Trust>`. There the parameter rides along with a key that\n * consumers name constantly and read values through, so it earns its complexity. Here the two states\n * have DIFFERENT MEMBERS — a capture can only be narrowed, a narrowed one can only be restored — and a\n * type that changes its members between states is a second class, not a second type argument. Naming\n * it also gives the field/queue-entry type a consumer holds a name that says what it is.\n *\n * The #622 opacity guarantees are unchanged and are why this class is also barrel-exported as a TYPE\n * ONLY: private constructor, a capability token on the producer, a real `#entries` private field, and\n * defensive copies on the way in and on the way out.\n */\nexport class RestorableContext {\n /** Same real ECMAScript private field, for the same reason — see {@link CapturedContext}. */\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; each ContextKey carries its own value type and the typed verbs re-apply it on read\n readonly #entries: Map<string, unknown>;\n\n /** PRIVATE — {@link of} is the only producer, and only this module can call it. */\n // webpieces-disable no-any-unknown -- see #entries\n private constructor(entries: Map<string, unknown>) {\n this.#entries = new Map(entries);\n }\n\n /**\n * The ONLY producer, called by `CapturedContext.withTrusted()` / `withoutTrusted()`. Guarded the\n * same way `capture` is: a token no consumer can name, and a barrel that exports this class as a\n * TYPE ONLY, so the class object — and with it this static — never reaches a consumer at all.\n */\n // webpieces-disable no-any-unknown -- see #entries\n // webpieces-disable no-function-outside-class -- static factory standing in for the (private) constructor; an instance method would presuppose the instance being created\n static of(\n authority: ContextCaptureAuthority,\n entries: Map<string, unknown>,\n ): RestorableContext {\n void authority;\n return new RestorableContext(entries);\n }\n\n /**\n * Overwrite a LIVE store with this snapshot — the engine behind `RequestContext.restoreContext`.\n * Write-only by design: it pushes entries in and hands nothing back, so it is not a side door onto\n * the snapshot's contents.\n */\n // webpieces-disable no-any-unknown -- see #entries\n restoreInto(authority: ContextCaptureAuthority, live: Map<string, unknown>): void {\n void authority;\n live.clear();\n for (const name of this.#entries.keys()) {\n live.set(name, this.#entries.get(name));\n }\n }\n\n /**\n * A FRESH store holding this snapshot — the engine behind `RequestContext.runWithContext`, which\n * opens a new AsyncLocalStorage scope around it. Fresh (a copy) rather than the internal map, so\n * everything the restored scope writes stays in that scope and the snapshot remains reusable.\n */\n // webpieces-disable no-any-unknown -- see #entries\n toFreshStore(authority: ContextCaptureAuthority): Map<string, unknown> {\n void authority;\n return new Map(this.#entries);\n }\n\n /** How many entries survived the narrowing. A count is not a value, so it leaks nothing. */\n size(): number {\n return this.#entries.size;\n }\n}\n"]}
1
+ {"version":3,"file":"CapturedContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/CapturedContext.ts"],"names":[],"mappings":";;;AAAA,oDAAsD;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAa,uBAAuB;IAChC;;;OAGG;IACc,KAAK,GAAW,2BAA2B,CAAC;IAE7D,4FAA4F;IAC5F,MAAM,CAAU,QAAQ,GAAG,IAAI,uBAAuB,EAAE,CAAC;IAEzD,gBAAuB,CAAC;IAExB,2FAA2F;IAC3F,QAAQ;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC;IACtB,CAAC;;AAfL,0DAgBC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+EG;AACH,MAAa,eAAe;IACxB;;;;OAIG;IACH,wKAAwK;IAC/J,QAAQ,CAAuB;IAExC;;;;OAIG;IACH,mDAAmD;IACnD,YAAoB,OAA6B;QAC7C,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IAED;;;;;OAKG;IACH,mDAAmD;IACnD,iNAAiN;IACjN,MAAM,CAAC,OAAO,CACV,SAAkC,EAClC,IAA0B;QAE1B,KAAK,SAAS,CAAC;QACf,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAED;;;;;;;;;;;OAWG;IACH,WAAW;QACP,OAAO,iBAAiB,CAAC,EAAE,CAAC,uBAAuB,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjF,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;IACH,cAAc;QACV,kGAAkG;QAClG,MAAM,IAAI,GAAG,IAAI,GAAG,EAAmB,CAAC;QACxC,IAAI,CAAC,0BAAc,CAAC,YAAY,EAAE,EAAE,CAAC;YACjC,OAAO,iBAAiB,CAAC,EAAE,CAAC,uBAAuB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACxE,CAAC;QACD,MAAM,QAAQ,GAAG,0BAAc,CAAC,GAAG,EAAE,CAAC;QACtC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YACtC,MAAM,GAAG,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;gBAC1B,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5C,CAAC;QACL,CAAC;QACD,OAAO,iBAAiB,CAAC,EAAE,CAAC,uBAAuB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACxE,CAAC;IAED;;;;OAIG;IACH,IAAI;QACA,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC9B,CAAC;CACJ;AAjHD,0CAiHC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,iBAAiB;IAC1B,6FAA6F;IAC7F,wKAAwK;IAC/J,QAAQ,CAAuB;IAExC,mFAAmF;IACnF,mDAAmD;IACnD,YAAoB,OAA6B;QAC7C,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IAED;;;;OAIG;IACH,mDAAmD;IACnD,0KAA0K;IAC1K,MAAM,CAAC,EAAE,CACL,SAAkC,EAClC,OAA6B;QAE7B,KAAK,SAAS,CAAC;QACf,OAAO,IAAI,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED;;;;OAIG;IACH,mDAAmD;IACnD,WAAW,CAAC,SAAkC,EAAE,IAA0B;QACtE,KAAK,SAAS,CAAC;QACf,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YACtC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5C,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,mDAAmD;IACnD,YAAY,CAAC,SAAkC;QAC3C,KAAK,SAAS,CAAC;QACf,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC;IAED,4FAA4F;IAC5F,IAAI;QACA,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC9B,CAAC;CACJ;AAvDD,8CAuDC","sourcesContent":["import { HeaderRegistry } from '@webpieces/core-util';\n\n/**\n * A capability TOKEN, not data — the thing you must be holding to build a {@link CapturedContext} or\n * to unpack one.\n *\n * It exists because `CapturedContext` needs a producer that {@link RequestContext} (a different class,\n * in a different file) can call, and TypeScript's `private` is class-scoped: a plain public\n * `CapturedContext.capture(map)` would be exactly the hand-assembled-payload hole this class exists to\n * close. So the producer takes a token whose constructor is private and whose only instance is\n * {@link INTERNAL}, and this module is deliberately NOT re-exported from the package barrel — in any\n * form, type or value.\n *\n * On its own that stops a consumer NAMING the token but not SUPPLYING one, since `null as never`\n * type-checks. It is the barrel's `export type { CapturedContext }` that finishes the job: with no\n * class object on the package surface there is no `capture(...)` to call, cast or not. The two halves\n * are load-bearing together, which is why each names the other.\n *\n * Per CLAUDE.md this is a class rather than a `Symbol` or an object literal: it is nominal (the private\n * `brand` field stops a structurally-identical `{}` from satisfying it) and it has exactly one\n * instantiation point.\n */\nexport class ContextCaptureAuthority {\n /**\n * Nominal brand. Without a private member the class is structurally `{}`, and any object at all\n * would typecheck as an authority.\n */\n private readonly brand: string = 'webpieces.context-capture';\n\n /** The ONE token that exists. The constructor below is private, so no other can be made. */\n static readonly INTERNAL = new ContextCaptureAuthority();\n\n private constructor() {}\n\n /** Kept honest — the brand is read here so it is a real field, not a type-only fiction. */\n describe(): string {\n return this.brand;\n }\n}\n\n/**\n * CapturedContext — an OPAQUE snapshot of a {@link RequestContext} scope.\n *\n * ## THE THREE CASES — pick by where the values came from and what may keep the identity\n *\n * | you have | you want | write |\n * |---|---|---|\n * | a genuine prior scope | ALL of it, trusted values included | `runWithContext(snapshot.withTrusted(), fn)` |\n * | a genuine prior scope | the trace fields but NOT the identity | `runWithContext(snapshot.withoutTrusted(), fn)` |\n * | values from OUTSIDE this process | exactly what you re-state, nothing inherited | `RequestContext.runDetachedScope(fn)` |\n *\n * Row 1 is a faithful re-root of a broken async chain — the work continues AS that user. Row 2 is a\n * deliberate PRIVILEGE DROP: a background job keeps `requestId`/`actionId` so it stays greppable, and\n * loses `userId`/`orgId`/roles because it runs as the system (see {@link withoutTrusted}). Row 3 is a\n * different question entirely — the values were never in a scope here, so there is no snapshot to take;\n * see `RequestContext.runDetachedScope`, where each value is written inside the closure with the trust\n * verbs.\n *\n * There is NO bare form of rows 1 and 2. `copyContext()` hands back a `CapturedContext`, and\n * `runWithContext`/`restoreContext` do not accept one — they take the {@link RestorableContext} that\n * {@link withTrusted} and {@link withoutTrusted} produce. Every call site therefore STATES whether the\n * proven identity travels, and neither intent is shorter to type than the other. That is\n * `.claude/rules/no-backwards-compat.md` shim\n * shape #5 applied here: a bare snapshot silently carrying a user identity is a widening that is an\n * absence rather than a token, and it is ungreppable. Now `grep -rn withTrusted` enumerates every place\n * an identity crosses a scope boundary and `grep -rn withoutTrusted` every deliberate drop.\n *\n * ## What it is for\n *\n * `AsyncLocalStorage` follows `await`, `.then()` and ordinary callbacks on its own, so the vast\n * majority of code never needs this. What it does NOT follow is work whose async chain was BROKEN and\n * re-rooted somewhere else: an item pushed onto an in-memory queue during a request and drained later\n * by a background loop, a batch flushed on a scheduler tick, an `EventEmitter` listener fired from a\n * socket the request does not own, a retry re-armed from a top-level timer, a hand-off to a worker\n * pool. In each of those the work executes under a DIFFERENT (or no) context, so the request id, the\n * log fields and the proven identity would silently vanish from everything the work logs or calls.\n *\n * The answer is two halves: `RequestContext.copyContext()` where the work is ENQUEUED, and\n * `RequestContext.runWithContext(captured.withTrusted(), fn)` — or `.withoutTrusted()`, or\n * `restoreContext(...)` of either — where it RUNS. The narrowing is not optional; see the table above.\n *\n * ## Why it is opaque instead of a `Map<string, unknown>`\n *\n * A restored context legitimately contains TRUSTED values — reinstating what the original scope had\n * proven is the entire point — so the restore side cannot type-check its payload the way\n * `putTrusted`/`getTrusted` do. That left the `Map`-taking signature that this class DELETED (a\n * now-removed `setContext(map)`, and `runWithContext(map, fn)`) as a complete bypass of the trust\n * system: handing it `new Map([['userId', 'victim']])` forged a proven identity in one line, without\n * ever typing a trust verb, and the only thing standing against it was a doc comment saying \"the Map\n * must come from copyContext()\". An agent picks whatever compiles, so a doc comment is not\n * enforcement.\n *\n * Making the PAYLOAD opaque solves it without type-checking the contents: the only way to obtain one is\n * a real capture of a real scope, so whatever it holds was, by construction, already in a context that\n * something legitimately wrote. Concretely:\n *\n * - the constructor is `private`, and there is no public factory — {@link capture} demands a\n * {@link ContextCaptureAuthority} that cannot be named outside this package;\n * - the package barrel exports this class as a TYPE ONLY, so a consumer never receives the class\n * object at all and cannot reach `capture` even with a cast. That second half matters: a token whose\n * TYPE is unexported still stops nothing on its own, because `capture(null as never, forgedMap)`\n * type-checks. Withholding the class object is what actually closes it;\n * - the entries live in `#entries`, a genuine ECMAScript private field, so they are unreachable at\n * RUNTIME as well as at compile time — no `Object.keys`, no cast, no index signature;\n * - the map is defensively copied ON CAPTURE, again on each NARROWING, and again ON RESTORE, so a\n * caller who still holds the live context (or who keeps writing to it after capturing) cannot reach\n * through the snapshot, narrowing never mutates the capture it came from, and a snapshot can be\n * restored repeatedly without the first restore's mutations bleeding into the second.\n *\n * There is deliberately no reader: nothing hands the entries back out. That is why `getAll()` is gone\n * rather than re-typed to return one of these — a `CapturedContext` you cannot read is useless as a\n * `getAll`, and a readable one would be the raw enumeration of every trusted value all over again.\n *\n * The one residual: a consumer holding a NARROWED snapshot can still cast a token into\n * {@link RestorableContext.toFreshStore} and read the entries back out as a plain Map. That is knowingly accepted, and it is the same asymmetry\n * `RequestContext.getAny` states — FORGING a trusted value is the dangerous direction and is closed\n * here; reading one you were already legitimately handed, without saying `getTrusted`, costs you\n * nothing but the type. Closing it too would mean no method could take the token at all, which is to\n * say no restore could exist.\n */\nexport class CapturedContext {\n /**\n * A real ECMAScript private field, not a TypeScript `private`. The distinction matters here: `#`\n * is enforced by the runtime, so the snapshot's contents cannot be reached by a cast, by\n * `Object.entries`, or by `as unknown as { entries: Map<string, unknown> }`.\n */\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; each ContextKey carries its own value type and the typed verbs re-apply it on read\n readonly #entries: Map<string, unknown>;\n\n /**\n * PRIVATE — a CapturedContext can only come from {@link capture}, which in turn can only be called\n * by code holding a {@link ContextCaptureAuthority}. Copies the map so the snapshot is never a\n * window onto a live store.\n */\n // webpieces-disable no-any-unknown -- see #entries\n private constructor(entries: Map<string, unknown>) {\n this.#entries = new Map(entries);\n }\n\n /**\n * The ONLY producer. `authority` is a compile-time capability, not a runtime check — so it is\n * referenced below only to keep it from being an unused parameter. Note the token alone is not the\n * guarantee (a cast can supply one); the guarantee is that the barrel exports this class as a TYPE\n * ONLY, so no consumer ever holds the class object this static hangs off. See the class doc.\n */\n // webpieces-disable no-any-unknown -- see #entries\n // webpieces-disable no-function-outside-class -- static factory standing in for the (private) constructor; making it an instance method would mean an instance already existed, which is the thing being created\n static capture(\n authority: ContextCaptureAuthority,\n live: Map<string, unknown>,\n ): CapturedContext {\n void authority;\n return new CapturedContext(live);\n }\n\n /**\n * Carry EVERY value onward, the proven identity included — the faithful re-root. The work runs AS\n * that user: `getTrusted(USER_ID)` inside it answers exactly what it answered in the original scope,\n * which is the entire point when a request's own continuation was re-rooted onto a queue or a timer.\n *\n * Said OUT LOUD, because it is the wide branch. A bare snapshot is deliberately not accepted by\n * `runWithContext`/`restoreContext` (see the class doc), so the identity never crosses a scope\n * boundary by default or by omission, and `grep -rn withTrusted` enumerates every place it does.\n *\n * NON-MUTATING, like its sibling — the receiver is unchanged, so ONE snapshot can be narrowed both\n * ways at two different call sites.\n */\n withTrusted(): RestorableContext {\n return RestorableContext.of(ContextCaptureAuthority.INTERNAL, this.#entries);\n }\n\n /**\n * Carry only the UNTRUSTED values — a deliberate PRIVILEGE DROP.\n *\n * ```typescript\n * const snapshot = RequestContext.copyContext();\n * RequestContext.runWithContext(snapshot.withTrusted(), fn); // runs AS that user\n * RequestContext.runWithContext(snapshot.withoutTrusted(), fn); // runs as the SYSTEM\n * ```\n *\n * The case: a background job or fire-and-forget task spawned during a request should keep the\n * untrusted trace fields — `requestId`, `actionId` — so its log lines are still greppable back to\n * the click that caused them, but it must NOT keep `userId` / `orgId` / roles, because it executes\n * as the system rather than as that user. Carrying the proven identity onward would make every\n * downstream authorization decision think the user is still on the other end of the wire.\n *\n * A METHOD PAIR, never a `keepTrusted: boolean` on {@link RequestContext.runWithContext}: a\n * parameter makes the two intents equally easy to type and impossible to grep, and a defaulted one\n * makes the permissive branch the shortest thing to write — `.claude/rules/no-backwards-compat.md` shim\n * shape #5, \"a widening\n * that is an ABSENCE rather than a token\", the same reason `AuthorizationType.ALL_USERS` says\n * the wide grant out loud. As a transform on the SNAPSHOT rather than a second capture mechanism it\n * composes with BOTH consumers — `runWithContext` and `restoreContext` — for free.\n *\n * NON-MUTATING: the receiver is untouched, so one snapshot can be used both ways.\n *\n * NO AUTHORITY TOKEN, deliberately, and it must not grow one. The token on {@link capture} exists\n * because CONSTRUCTING a snapshot from arbitrary entries forges trust. This direction only ever\n * REMOVES entries: whatever survives was already in a real capture of a real scope, so the result\n * is strictly less privileged than the object the caller is already holding. Dropping cannot forge.\n *\n * WHAT SURVIVES is exactly \"registered as an UNTRUSTED {@link ContextKey}\". Trusted keys go, and so\n * do names the {@link HeaderRegistry} does not know — the framework's reserved slots (the\n * `HttpRequest`, the AuthFilter principal, the Cloud Tasks schedule frame), which carry no declared\n * trust and are the caller's identity and connection rather than trace fields. A privilege drop\n * that guessed in the permissive direction would not be one. For the same reason, with no registry\n * configured NOTHING is knowably untrusted and the result is empty — always the safe answer, since\n * this method's only job is to remove.\n */\n withoutTrusted(): RestorableContext {\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; see #entries\n const kept = new Map<string, unknown>();\n if (!HeaderRegistry.isConfigured()) {\n return RestorableContext.of(ContextCaptureAuthority.INTERNAL, kept);\n }\n const registry = HeaderRegistry.get();\n for (const name of this.#entries.keys()) {\n const key = registry.findByName(name);\n if (key && !key.isTrusted()) {\n kept.set(name, this.#entries.get(name));\n }\n }\n return RestorableContext.of(ContextCaptureAuthority.INTERNAL, kept);\n }\n\n /**\n * How many entries the snapshot holds. The one thing it will tell you about itself — a count is\n * not a value, so it leaks nothing, and it lets a caller (and a test) see that a capture taken\n * outside an active scope is simply empty rather than an error.\n */\n size(): number {\n return this.#entries.size;\n }\n}\n\n/**\n * A snapshot whose TRUST INTENT has been stated — the only thing `RequestContext.restoreContext` and\n * `RequestContext.runWithContext` accept.\n *\n * It exists to make the wide choice unskippable. A single type would have meant\n * `runWithContext(snapshot, fn)` compiling next to `runWithContext(snapshot.withTrusted(), fn)`: two\n * spellings of one thing (shim shape #1), with the shorter one silently carrying a user identity into\n * work that may have no business running as that user. Splitting the type deletes the default — a\n * capture is inert until it says which it means — so there is exactly one spelling per intent, and\n * `grep -rn withTrusted` / `grep -rn withoutTrusted` enumerate the two populations of call sites.\n *\n * Two CLASSES rather than a phantom type parameter on {@link CapturedContext}, even though this repo\n * uses that trick on `ContextKey<V, T extends Trust>`. There the parameter rides along with a key that\n * consumers name constantly and read values through, so it earns its complexity. Here the two states\n * have DIFFERENT MEMBERS — a capture can only be narrowed, a narrowed one can only be restored — and a\n * type that changes its members between states is a second class, not a second type argument. Naming\n * it also gives the field/queue-entry type a consumer holds a name that says what it is.\n *\n * The #622 opacity guarantees are unchanged and are why this class is also barrel-exported as a TYPE\n * ONLY: private constructor, a capability token on the producer, a real `#entries` private field, and\n * defensive copies on the way in and on the way out.\n */\nexport class RestorableContext {\n /** Same real ECMAScript private field, for the same reason — see {@link CapturedContext}. */\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; each ContextKey carries its own value type and the typed verbs re-apply it on read\n readonly #entries: Map<string, unknown>;\n\n /** PRIVATE — {@link of} is the only producer, and only this module can call it. */\n // webpieces-disable no-any-unknown -- see #entries\n private constructor(entries: Map<string, unknown>) {\n this.#entries = new Map(entries);\n }\n\n /**\n * The ONLY producer, called by `CapturedContext.withTrusted()` / `withoutTrusted()`. Guarded the\n * same way `capture` is: a token no consumer can name, and a barrel that exports this class as a\n * TYPE ONLY, so the class object — and with it this static — never reaches a consumer at all.\n */\n // webpieces-disable no-any-unknown -- see #entries\n // webpieces-disable no-function-outside-class -- static factory standing in for the (private) constructor; an instance method would presuppose the instance being created\n static of(\n authority: ContextCaptureAuthority,\n entries: Map<string, unknown>,\n ): RestorableContext {\n void authority;\n return new RestorableContext(entries);\n }\n\n /**\n * Overwrite a LIVE store with this snapshot — the engine behind `RequestContext.restoreContext`.\n * Write-only by design: it pushes entries in and hands nothing back, so it is not a side door onto\n * the snapshot's contents.\n */\n // webpieces-disable no-any-unknown -- see #entries\n restoreInto(authority: ContextCaptureAuthority, live: Map<string, unknown>): void {\n void authority;\n live.clear();\n for (const name of this.#entries.keys()) {\n live.set(name, this.#entries.get(name));\n }\n }\n\n /**\n * A FRESH store holding this snapshot — the engine behind `RequestContext.runWithContext`, which\n * opens a new AsyncLocalStorage scope around it. Fresh (a copy) rather than the internal map, so\n * everything the restored scope writes stays in that scope and the snapshot remains reusable.\n */\n // webpieces-disable no-any-unknown -- see #entries\n toFreshStore(authority: ContextCaptureAuthority): Map<string, unknown> {\n void authority;\n return new Map(this.#entries);\n }\n\n /** How many entries survived the narrowing. A count is not a value, so it leaks nothing. */\n size(): number {\n return this.#entries.size;\n }\n}\n"]}
@@ -21,7 +21,7 @@ export declare class HttpRequest {
21
21
  /**
22
22
  * The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`
23
23
  * route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:
24
- * `@WpAuthWebhook` has nothing to verify without it and 401s rather than waving the call
24
+ * `webhook(...)` has nothing to verify without it and 401s rather than waving the call
25
25
  * through. A spec driving a webhook route in-process supplies one here, the same way a spec
26
26
  * today supplies an `authorization` header.
27
27
  */
@@ -32,7 +32,7 @@ export declare class HttpRequest {
32
32
  /**
33
33
  * The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`
34
34
  * route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:
35
- * `@WpAuthWebhook` has nothing to verify without it and 401s rather than waving the call
35
+ * `webhook(...)` has nothing to verify without it and 401s rather than waving the call
36
36
  * through. A spec driving a webhook route in-process supplies one here, the same way a spec
37
37
  * today supplies an `authorization` header.
38
38
  */
@@ -25,7 +25,7 @@ class HttpRequest {
25
25
  /**
26
26
  * The verbatim bytes + absolute url, present ONLY on an `@Endpoint(..., { rawBody: true })`
27
27
  * route (see {@link RawRequest}). Absent everywhere else, and absent is the SAFE state:
28
- * `@WpAuthWebhook` has nothing to verify without it and 401s rather than waving the call
28
+ * `webhook(...)` has nothing to verify without it and 401s rather than waving the call
29
29
  * through. A spec driving a webhook route in-process supplies one here, the same way a spec
30
30
  * today supplies an `authorization` header.
31
31
  */
@@ -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,CAAC,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAC1F,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 * `@WpAuthWebhook` 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"]}
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,CAAC,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAC1F,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 * `webhook(...)` 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 `AuthenticatedCaller.entries`, so a fully verified `@WpAuthJwt` request stamps no context entries at
27
+ * EMPTY `AuthenticatedCaller.entries`, so a fully verified `jwt()` 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
@@ -32,11 +32,11 @@ export declare class PendingTrustedValue {
32
32
  * Inbound trusted values never enter the context. They are stashed HERE, and `AuthFilter` decides
33
33
  * what to do with them once it knows who the caller is:
34
34
  *
35
- * - `@WpAuthOidc` / `@WpAuthSharedSecret` — the CALLER's own identity was verified, so this is an
35
+ * - `oidc(...)` / `sharedSecret(...)` — the CALLER's own identity was verified, so this is an
36
36
  * internal service passing along context it already holds. Admit the pending values as trusted.
37
37
  * This is what makes service-to-service propagation of a verified userId work, and it is the whole
38
38
  * reason trusted keys are allowed to have an `httpHeader` at all.
39
- * - `@WpAuthJwt` / public — the caller may be a browser or anyone with curl. A pending value is
39
+ * - `jwt()` / public — the caller may be a browser or anyone with curl. A pending value is
40
40
  * admitted ONLY if the authenticator independently derived the SAME value. Anything else — a
41
41
  * different value, or a value nothing vouched for — rejects the request.
42
42
  *
@@ -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 `AuthenticatedCaller.entries`, so a fully verified `@WpAuthJwt` request stamps no context entries at
40
+ * EMPTY `AuthenticatedCaller.entries`, so a fully verified `jwt()` 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
@@ -45,11 +45,11 @@ exports.PendingTrustedValue = PendingTrustedValue;
45
45
  * Inbound trusted values never enter the context. They are stashed HERE, and `AuthFilter` decides
46
46
  * what to do with them once it knows who the caller is:
47
47
  *
48
- * - `@WpAuthOidc` / `@WpAuthSharedSecret` — the CALLER's own identity was verified, so this is an
48
+ * - `oidc(...)` / `sharedSecret(...)` — the CALLER's own identity was verified, so this is an
49
49
  * internal service passing along context it already holds. Admit the pending values as trusted.
50
50
  * This is what makes service-to-service propagation of a verified userId work, and it is the whole
51
51
  * reason trusted keys are allowed to have an `httpHeader` at all.
52
- * - `@WpAuthJwt` / public — the caller may be a browser or anyone with curl. A pending value is
52
+ * - `jwt()` / public — the caller may be a browser or anyone with curl. A pending value is
53
53
  * admitted ONLY if the authenticator independently derived the SAME value. Anything else — a
54
54
  * different value, or a value nothing vouched for — rejects the request.
55
55
  *
@@ -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 `AuthenticatedCaller.entries`, so a fully verified `@WpAuthJwt` 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 * - `@WpAuthOidc` / `@WpAuthSharedSecret` — 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 * - `@WpAuthJwt` / 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 `jwt()` 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 * - `oidc(...)` / `sharedSecret(...)` — 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 * - `jwt()` / 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"]}
@@ -16,6 +16,9 @@ import { CapturedContext, RestorableContext } from './CapturedContext';
16
16
  * ```
17
17
  */
18
18
  declare class RequestContextImpl {
19
+ private readonly scopeIdentities;
20
+ /** Opaque scope identity; restoring snapshots never restores local authentication. */
21
+ activeScopeIdentity(): object;
19
22
  private storage;
20
23
  constructor();
21
24
  /**
@@ -21,6 +21,19 @@ const HTTP_REQUEST_KEY = '__webpieces_http_request__';
21
21
  * ```
22
22
  */
23
23
  class RequestContextImpl {
24
+ scopeIdentities = new WeakMap();
25
+ /** Opaque scope identity; restoring snapshots never restores local authentication. */
26
+ activeScopeIdentity() {
27
+ const store = this.storage.getStore();
28
+ if (!store)
29
+ throw new Error('Invocation authentication requires an active RequestContext.');
30
+ let identity = this.scopeIdentities.get(store);
31
+ if (!identity) {
32
+ identity = Object.freeze({});
33
+ this.scopeIdentities.set(store, identity);
34
+ }
35
+ return identity;
36
+ }
24
37
  storage;
25
38
  constructor() {
26
39
  this.storage = new async_hooks_1.AsyncLocalStorage();
@@ -456,6 +469,7 @@ class RequestContextImpl {
456
469
  '.withoutTrusted() — which opens its own.');
457
470
  }
458
471
  captured.restoreInto(CapturedContext_1.ContextCaptureAuthority.INTERNAL, store);
472
+ this.scopeIdentities.delete(store);
459
473
  }
460
474
  /**
461
475
  * Check if a key exists in the context.
@@ -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 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"]}
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 readonly scopeIdentities = new WeakMap<object, object>();\n\n /** Opaque scope identity; restoring snapshots never restores local authentication. */\n activeScopeIdentity(): object {\n const store = this.storage.getStore();\n if (!store) throw new Error('Invocation authentication requires an active RequestContext.');\n let identity = this.scopeIdentities.get(store);\n if (!identity) {\n identity = Object.freeze({});\n this.scopeIdentities.set(store, identity);\n }\n return identity;\n }\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 this.scopeIdentities.delete(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"]}
@@ -30,7 +30,7 @@ export declare class RequestContextHeaders {
30
30
  *
31
31
  * TRUSTED keys are the exception, and `destination` is why this method takes an argument at all.
32
32
  * The callee's `AuthFilter` admits an inbound `x-user-id` only on a route that authenticated its
33
- * CALLER, so shipping one to a `@WpAuthPublic` / `@WpAuthJwt` endpoint builds a request the callee is
33
+ * CALLER, so shipping one to a `@WpAuthPublic` / `jwt()` endpoint builds a request the callee is
34
34
  * obliged to 401. {@link DestinationTrust} answers that from the destination endpoint's own
35
35
  * AuthMode — there is no "send everything" default to fall into. Untrusted keys always travel.
36
36
  *
@@ -36,7 +36,7 @@ let RequestContextHeaders = class RequestContextHeaders {
36
36
  *
37
37
  * TRUSTED keys are the exception, and `destination` is why this method takes an argument at all.
38
38
  * The callee's `AuthFilter` admits an inbound `x-user-id` only on a route that authenticated its
39
- * CALLER, so shipping one to a `@WpAuthPublic` / `@WpAuthJwt` endpoint builds a request the callee is
39
+ * CALLER, so shipping one to a `@WpAuthPublic` / `jwt()` endpoint builds a request the callee is
40
40
  * obliged to 401. {@link DestinationTrust} answers that from the destination endpoint's own
41
41
  * AuthMode — there is no "send everything" default to fall into. Untrusted keys always travel.
42
42
  *
@@ -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 AnyContextKey,\n DestinationTrust,\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 request headers\n * response-out {@link buildResponseHeaders} the context -> THIS hop's response headers\n * response-in {@link acceptResponseHeaders} a callee's response headers -> the context\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 THAT THIS DESTINATION MAY RECEIVE, under its wire\n * 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 * TRUSTED keys are the exception, and `destination` is why this method takes an argument at all.\n * The callee's `AuthFilter` admits an inbound `x-user-id` only on a route that authenticated its\n * CALLER, so shipping one to a `@WpAuthPublic` / `@WpAuthJwt` endpoint builds a request the callee is\n * obliged to 401. {@link DestinationTrust} answers that from the destination endpoint's own\n * AuthMode — there is no \"send everything\" default to fall into. Untrusted keys always travel.\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(destination: DestinationTrust): 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 if (!destination.allows(key)) {\n continue;\n }\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 * OUTBOUND, RESPONSE direction — the mirror of {@link buildOutboundHeaders}: every key that\n * declares a `responseHeader` and has a value in this context, under its response wire name.\n *\n * This is what the server's ONE response choke point writes onto EVERY response (success and\n * error, webpieces' default body and an app's own). Before it there was a single hard-coded\n * `res.setHeader('x-request-id', ...)` in `ExpressWrapper.stampTransactionId` that no other key\n * could join without editing that method; `x-request-id` is now an ordinary registry entry and\n * the loop is the whole feature.\n *\n * Returns an EMPTY map outside a `run(...)` block rather than throwing — deliberately, and unlike\n * {@link buildOutboundHeaders}. A response is written on paths where the context was never\n * established: a malformed or oversize body is refused BEFORE `fillFromRequest` mints an id. That\n * is the accepted known issue the deleted `stampTransactionId` recorded, and throwing here would\n * turn a 400 into a 500.\n *\n * A `collect` key holds a LIST, so it is emitted as the comma-separated list HTTP already has a\n * grammar for. A value that is neither a string nor a list of strings is skipped: a response\n * header is text, and an object-valued key (the api tag, the recorder) has no wire form.\n */\n buildResponseHeaders(): Map<string, string> {\n const headers = new Map<string, string>();\n if (!RequestContext.isActive()) {\n return headers;\n }\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const wire = this.toResponseWireValue(RequestContext.getAny(key));\n if (wire !== undefined && wire !== '') {\n headers.set(key.responseHeader!, wire);\n }\n }\n return headers;\n }\n\n /** One context value -> its response-header text, or undefined when it has no wire form. */\n // webpieces-disable no-any-unknown -- the context store is honestly type-erased; the key's own V types the public accessors\n private toResponseWireValue(value: unknown): string | undefined {\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value)) {\n // webpieces-disable no-any-unknown -- a collect key's list is type-erased here, exactly as every other serialization read is\n const parts = (value as unknown[]).filter((v: unknown) => typeof v === 'string');\n return parts.length > 0 ? parts.join(', ') : undefined;\n }\n return undefined;\n }\n\n /**\n * INBOUND, RESPONSE direction — the exact inverse of {@link buildResponseHeaders}, and the reason\n * a value can travel UP the call tree hop by hop: a webpieces client calls this with the response\n * it just received, and every declared response key lands in the CALLER's context.\n *\n * TRUST IS THE MIRROR OF THE INBOUND PROBLEM, and is not hand-waved. A response is another\n * process's ASSERTION, caller-asserted from our seat. An UNTRUSTED response key is admitted —\n * nobody was ever going to make a security decision on it. A TRUSTED one is admitted only from a\n * destination this client AUTHENTICATED TO (the same {@link DestinationTrust} table that decides\n * what may ride OUT) and is otherwise DROPPED. Without that, a partner's server sets `x-user-id`\n * on its response and this process believes it.\n *\n * MERGE is the key's own declaration, because fan-out is the normal case: one request calling\n * five services yields five values for one key, and last-write-wins silently keeps one. See\n * {@link ContextKey.mergeResponseValue}.\n */\n acceptResponseHeaders(headers: Headers, destination: DestinationTrust): void {\n this.requireActiveContext();\n\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const value = headers.get(key.responseHeader!);\n if (value === null || value === '') {\n continue;\n }\n if (!destination.allows(key)) {\n // DROPPED: a trusted key asserted by a destination we did not authenticate to.\n continue;\n }\n const merged = key.mergeResponseValue(RequestContext.getAny(key), value);\n if (key.isTrusted()) {\n RequestContext.putTrusted(key, merged);\n } else {\n RequestContext.putUntrusted(key, merged);\n }\n }\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 * There is NO cast here, on EITHER branch. `isTrusted()` is a type predicate over the\n * {@link AnyContextKey} union, so the `if` holds a trusted key and the `else` holds an untrusted\n * one — the runtime check produces the type it proves. Trust is binary and the union has exactly\n * two constituents, so the `else` is the whole remaining case rather than a silent drop.\n */\n private acceptInbound(key: AnyContextKey, value: string): void {\n if (key.isTrusted()) {\n PendingWireTrust.stash(key, value);\n } else {\n RequestContext.putUntrusted(key, value);\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.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"]}
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 DestinationTrust,\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 request headers\n * response-out {@link buildResponseHeaders} the context -> THIS hop's response headers\n * response-in {@link acceptResponseHeaders} a callee's response headers -> the context\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 THAT THIS DESTINATION MAY RECEIVE, under its wire\n * 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 * TRUSTED keys are the exception, and `destination` is why this method takes an argument at all.\n * The callee's `AuthFilter` admits an inbound `x-user-id` only on a route that authenticated its\n * CALLER, so shipping one to a `@WpAuthPublic` / `jwt()` endpoint builds a request the callee is\n * obliged to 401. {@link DestinationTrust} answers that from the destination endpoint's own\n * AuthMode — there is no \"send everything\" default to fall into. Untrusted keys always travel.\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(destination: DestinationTrust): 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 if (!destination.allows(key)) {\n continue;\n }\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 * OUTBOUND, RESPONSE direction — the mirror of {@link buildOutboundHeaders}: every key that\n * declares a `responseHeader` and has a value in this context, under its response wire name.\n *\n * This is what the server's ONE response choke point writes onto EVERY response (success and\n * error, webpieces' default body and an app's own). Before it there was a single hard-coded\n * `res.setHeader('x-request-id', ...)` in `ExpressWrapper.stampTransactionId` that no other key\n * could join without editing that method; `x-request-id` is now an ordinary registry entry and\n * the loop is the whole feature.\n *\n * Returns an EMPTY map outside a `run(...)` block rather than throwing — deliberately, and unlike\n * {@link buildOutboundHeaders}. A response is written on paths where the context was never\n * established: a malformed or oversize body is refused BEFORE `fillFromRequest` mints an id. That\n * is the accepted known issue the deleted `stampTransactionId` recorded, and throwing here would\n * turn a 400 into a 500.\n *\n * A `collect` key holds a LIST, so it is emitted as the comma-separated list HTTP already has a\n * grammar for. A value that is neither a string nor a list of strings is skipped: a response\n * header is text, and an object-valued key (the api tag, the recorder) has no wire form.\n */\n buildResponseHeaders(): Map<string, string> {\n const headers = new Map<string, string>();\n if (!RequestContext.isActive()) {\n return headers;\n }\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const wire = this.toResponseWireValue(RequestContext.getAny(key));\n if (wire !== undefined && wire !== '') {\n headers.set(key.responseHeader!, wire);\n }\n }\n return headers;\n }\n\n /** One context value -> its response-header text, or undefined when it has no wire form. */\n // webpieces-disable no-any-unknown -- the context store is honestly type-erased; the key's own V types the public accessors\n private toResponseWireValue(value: unknown): string | undefined {\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value)) {\n // webpieces-disable no-any-unknown -- a collect key's list is type-erased here, exactly as every other serialization read is\n const parts = (value as unknown[]).filter((v: unknown) => typeof v === 'string');\n return parts.length > 0 ? parts.join(', ') : undefined;\n }\n return undefined;\n }\n\n /**\n * INBOUND, RESPONSE direction — the exact inverse of {@link buildResponseHeaders}, and the reason\n * a value can travel UP the call tree hop by hop: a webpieces client calls this with the response\n * it just received, and every declared response key lands in the CALLER's context.\n *\n * TRUST IS THE MIRROR OF THE INBOUND PROBLEM, and is not hand-waved. A response is another\n * process's ASSERTION, caller-asserted from our seat. An UNTRUSTED response key is admitted —\n * nobody was ever going to make a security decision on it. A TRUSTED one is admitted only from a\n * destination this client AUTHENTICATED TO (the same {@link DestinationTrust} table that decides\n * what may ride OUT) and is otherwise DROPPED. Without that, a partner's server sets `x-user-id`\n * on its response and this process believes it.\n *\n * MERGE is the key's own declaration, because fan-out is the normal case: one request calling\n * five services yields five values for one key, and last-write-wins silently keeps one. See\n * {@link ContextKey.mergeResponseValue}.\n */\n acceptResponseHeaders(headers: Headers, destination: DestinationTrust): void {\n this.requireActiveContext();\n\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const value = headers.get(key.responseHeader!);\n if (value === null || value === '') {\n continue;\n }\n if (!destination.allows(key)) {\n // DROPPED: a trusted key asserted by a destination we did not authenticate to.\n continue;\n }\n const merged = key.mergeResponseValue(RequestContext.getAny(key), value);\n if (key.isTrusted()) {\n RequestContext.putTrusted(key, merged);\n } else {\n RequestContext.putUntrusted(key, merged);\n }\n }\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 * There is NO cast here, on EITHER branch. `isTrusted()` is a type predicate over the\n * {@link AnyContextKey} union, so the `if` holds a trusted key and the `else` holds an untrusted\n * one — the runtime check produces the type it proves. Trust is binary and the union has exactly\n * two constituents, so the `else` is the whole remaining case rather than a silent drop.\n */\n private acceptInbound(key: AnyContextKey, value: string): void {\n if (key.isTrusted()) {\n PendingWireTrust.stash(key, value);\n } else {\n RequestContext.putUntrusted(key, value);\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.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"]}