@webpieces/core-context 0.4.643 → 0.4.645

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.643",
3
+ "version": "0.4.645",
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.643",
25
+ "@webpieces/core-util": "0.4.645",
26
26
  "@inversifyjs/binding-decorators": "1.1.5",
27
27
  "inversify": "7.10.4",
28
28
  "reflect-metadata": "0.2.2"
@@ -206,7 +206,7 @@ class CapturedContext {
206
206
  const registry = core_util_1.HeaderRegistry.get();
207
207
  for (const name of this.#entries.keys()) {
208
208
  const key = registry.findByName(name);
209
- if (key && key.isUntrusted()) {
209
+ if (key && !key.isTrusted()) {
210
210
  kept.set(name, this.#entries.get(name));
211
211
  }
212
212
  }
@@ -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 CLAUDE.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(authority: ContextCaptureAuthority, live: Map<string, unknown>): 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.md shim shape #5, \"a widening\n * that is an ABSENCE rather than a token\", the same reason `@AuthJwt({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.isUntrusted()) {\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(authority: ContextCaptureAuthority, entries: Map<string, unknown>): 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":"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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 CLAUDE.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(authority: ContextCaptureAuthority, live: Map<string, unknown>): 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.md shim shape #5, \"a widening\n * that is an ABSENCE rather than a token\", the same reason `@AuthJwt({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(authority: ContextCaptureAuthority, entries: Map<string, unknown>): 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"]}
@@ -54,7 +54,8 @@ export declare class DetachedScopeCompileAssertions {
54
54
  * rebuilt from an EXTERNAL payload, driven off the registry's logged keys.
55
55
  *
56
56
  * The `isTrusted()` skip is what a reader sees; the compiler is what enforces it. Delete the skip
57
- * and `isUntrusted()` still refuses the trusted key, so the trusted branch has no write at all.
57
+ * and the write below stops compiling, because the key is mixed again and `putUntrusted` refuses
58
+ * it — see {@link cannotWriteAMixedKeyWithoutTestingItsTrust}.
58
59
  */
59
60
  theBrowserLogLoopCompiles(loggedKeys: AnyContextKey[], payload: Record<string, unknown>): void;
60
61
  /** The return value flows through, and an async closure is a promise the caller can await. */
@@ -80,7 +80,8 @@ class DetachedScopeCompileAssertions {
80
80
  * rebuilt from an EXTERNAL payload, driven off the registry's logged keys.
81
81
  *
82
82
  * The `isTrusted()` skip is what a reader sees; the compiler is what enforces it. Delete the skip
83
- * and `isUntrusted()` still refuses the trusted key, so the trusted branch has no write at all.
83
+ * and the write below stops compiling, because the key is mixed again and `putUntrusted` refuses
84
+ * it — see {@link cannotWriteAMixedKeyWithoutTestingItsTrust}.
84
85
  */
85
86
  // webpieces-disable no-any-unknown -- the EXTERNAL payload is by definition untyped JSON off the wire; that is precisely why every value below is trust-branched and typeof-checked before it is written
86
87
  theBrowserLogLoopCompiles(loggedKeys, payload) {
@@ -91,8 +92,10 @@ class DetachedScopeCompileAssertions {
91
92
  // no cast that could produce one.
92
93
  continue;
93
94
  }
95
+ // Past the `continue`, the key is narrowed to the untrusted branch by the SAME
96
+ // predicate — no second check, and no cast, to reach `putUntrusted`.
94
97
  const value = payload[key.name];
95
- if (key.isUntrusted() && typeof value === 'string') {
98
+ if (typeof value === 'string') {
96
99
  RequestContext_1.RequestContext.putUntrusted(key, value);
97
100
  }
98
101
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DetachedScopeCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/DetachedScopeCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,oDAAiE;AACjE,qDAAkD;AAElD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,8BAA8B;IACtB,OAAO,GAAG,sBAAU,CAAC,OAAO,CAAS,sBAAsB,EAAE,iBAAiB,CAAC,CAAC;IAChF,SAAS,GAAG,sBAAU,CAAC,SAAS,CAAS,wBAAwB,CAAC,CAAC;IAEpF;;;OAGG;IACH,+BAA+B;QAC3B,wFAAwF;QACxF,+BAAc,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACtF,CAAC;IAED,iFAAiF;IACjF,6BAA6B;QACzB,8EAA8E;QAC9E,+BAAc,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,oFAAoF;IACpF,0CAA0C,CAAC,GAAkB;QACzD,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,4FAA4F;YAC5F,+BAAc,CAAC,YAAY,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;OAIG;IACH,+CAA+C;QAC3C,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,gEAAgE;YAChE,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;;;;OAOG;IACH,0CAA0C;QACtC,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC;YAC9D,MAAM,MAAM,GAAuB,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3E,KAAK,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,yMAAyM;IACzM,yBAAyB,CAAC,UAA2B,EAAE,OAAgC;QACnF,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC3B,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;oBAClB,kFAAkF;oBAClF,kCAAkC;oBAClC,SAAS;gBACb,CAAC;gBACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBAChC,IAAI,GAAG,CAAC,WAAW,EAAE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;oBACjD,+BAAc,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBAC5C,CAAC;YACL,CAAC;YACD,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;IACP,CAAC;IAED,8FAA8F;IAC9F,KAAK,CAAC,uBAAuB;QACzB,MAAM,IAAI,GAAW,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;QACnE,MAAM,WAAW,GAAW,MAAM,+BAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;QACtF,KAAK,IAAI,CAAC;QACV,KAAK,WAAW,CAAC;IACrB,CAAC;CACJ;AAvFD,wEAuFC","sourcesContent":["import { AnyContextKey, ContextKey } from '@webpieces/core-util';\nimport { RequestContext } from './RequestContext';\n\n/**\n * COMPILE-TIME assertions for {@link RequestContext.runDetachedScope}.\n *\n * The runtime half — that the detached scope starts empty, that the enclosing scope survives, that a\n * throw unwinds it — is in `DetachedScope.spec.ts`. What a spec CANNOT express is the half that makes\n * the browser-log path safe BY CONSTRUCTION rather than by remembering to filter:\n *\n * - `runDetachedScope` has NO container-taking form, so the deleted `runWithContext(map, fn)` forgery\n * path cannot come back through this door;\n * - a loop over a mixed `AnyContextKey[]` cannot write ANY key until it has tested that key's trust;\n * - and a TRUSTED key can never reach `putUntrusted`, so a browser — which proves nothing — cannot\n * FABRICATE a proven value by naming `userId` in its payload. It is a compile error at the write,\n * not a filter someone has to remember to write.\n *\n * That is about the SOURCE, not about the key. Writing a trusted key is ordinary and legitimate — see\n * {@link putTrustedIsLegitimateInsideADetachedScope} — whenever the caller has actually proven the\n * value. What cannot be written down is a claim of proof by code that has none.\n *\n * In COMPILED source deliberately — `tsconfig.lib.json` excludes specs and vitest strips types with\n * esbuild, so a `@ts-expect-error` in a `.spec.ts` is inert and the suite would pass either way. Each\n * one below fails the build with TS2578 the day its line starts compiling. See\n * `CapturedContextCompileAssertions` and `RequestContextTrustCompileAssertions` for the sibling halves.\n */\nexport class DetachedScopeCompileAssertions {\n private readonly trusted = ContextKey.trusted<string>('assertDetachedUserId', 'jwt claim `sub`');\n private readonly untrusted = ContextKey.untrusted<string>('assertDetachedActionId');\n\n /**\n * THE hole this whole shape exists to keep shut: no Map/object/array of entries may cross the\n * boundary. Values are written INSIDE the closure, through the trust verbs.\n */\n cannotHandItAContainerOfEntries(): void {\n // @ts-expect-error - runDetachedScope takes ONLY a closure; there is no map-taking form\n RequestContext.runDetachedScope(new Map([['userId', 'victim']]), () => undefined);\n }\n\n /** Nor a plain object of entries, which is the same hole spelled differently. */\n cannotHandItAnObjectOfEntries(): void {\n // @ts-expect-error - the single parameter is the closure, not a bag of values\n RequestContext.runDetachedScope({ userId: 'victim' });\n }\n\n /** A key of unknown trust cannot be written AT ALL — the branch is not optional. */\n cannotWriteAMixedKeyWithoutTestingItsTrust(key: AnyContextKey): void {\n RequestContext.runDetachedScope(() => {\n // @ts-expect-error - putUntrusted needs a key KNOWN to be untrusted; AnyContextKey is mixed\n RequestContext.putUntrusted(key, 'from-the-browser');\n });\n }\n\n /**\n * And a TRUSTED key cannot be LAUNDERED through the untrusted verb, detached or not — which is\n * what a caller with no proof would have to do, since `putTrusted` is the only other way in and\n * saying it is a deliberate, greppable claim.\n */\n cannotLaunderATrustedKeyThroughTheUntrustedVerb(): void {\n RequestContext.runDetachedScope(() => {\n // @ts-expect-error - putUntrusted does not accept a trusted key\n RequestContext.putUntrusted(this.trusted, 'browser-said-so');\n });\n }\n\n /**\n * POSITIVE, and the point the negative above must not be mistaken for: writing a TRUSTED value\n * inside a detached scope is ordinary and correct when the caller has actually proven it — the\n * signed-webhook case (Twilio/WhatsApp proves the phone number, the app looks up the userId), or a\n * verified JWT claim. `putTrusted` is the verb for exactly that, and a detached scope does not\n * change it. Trust is about tamper-resistance, not secrecy: a trusted `userId` is a plain,\n * fully-logged GUID; masking is the separate `maskInLogs` axis.\n */\n putTrustedIsLegitimateInsideADetachedScope(): void {\n RequestContext.runDetachedScope(() => {\n RequestContext.putTrusted(this.trusted, 'proven-out-of-band');\n const proven: string | undefined = RequestContext.getTrusted(this.trusted);\n void proven;\n });\n }\n\n /**\n * POSITIVE, and the shape the real consumer writes: emit one browser line under a fresh scope\n * rebuilt from an EXTERNAL payload, driven off the registry's logged keys.\n *\n * The `isTrusted()` skip is what a reader sees; the compiler is what enforces it. Delete the skip\n * and `isUntrusted()` still refuses the trusted key, so the trusted branch has no write at all.\n */\n // webpieces-disable no-any-unknown -- the EXTERNAL payload is by definition untyped JSON off the wire; that is precisely why every value below is trust-branched and typeof-checked before it is written\n theBrowserLogLoopCompiles(loggedKeys: AnyContextKey[], payload: Record<string, unknown>): void {\n RequestContext.runDetachedScope(() => {\n for (const key of loggedKeys) {\n if (key.isTrusted()) {\n // A browser cannot vouch for a proven fact. There is no write in this branch, and\n // no cast that could produce one.\n continue;\n }\n const value = payload[key.name];\n if (key.isUntrusted() && typeof value === 'string') {\n RequestContext.putUntrusted(key, value);\n }\n }\n RequestContext.putUntrusted(this.untrusted, 'click-7');\n });\n }\n\n /** The return value flows through, and an async closure is a promise the caller can await. */\n async returnValuesFlowThrough(): Promise<void> {\n const sync: string = RequestContext.runDetachedScope(() => 'done');\n const asyncResult: string = await RequestContext.runDetachedScope(async () => 'done');\n void sync;\n void asyncResult;\n }\n}\n"]}
1
+ {"version":3,"file":"DetachedScopeCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/DetachedScopeCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,oDAAiE;AACjE,qDAAkD;AAElD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,8BAA8B;IACtB,OAAO,GAAG,sBAAU,CAAC,OAAO,CAAS,sBAAsB,EAAE,iBAAiB,CAAC,CAAC;IAChF,SAAS,GAAG,sBAAU,CAAC,SAAS,CAAS,wBAAwB,CAAC,CAAC;IAEpF;;;OAGG;IACH,+BAA+B;QAC3B,wFAAwF;QACxF,+BAAc,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACtF,CAAC;IAED,iFAAiF;IACjF,6BAA6B;QACzB,8EAA8E;QAC9E,+BAAc,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,oFAAoF;IACpF,0CAA0C,CAAC,GAAkB;QACzD,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,4FAA4F;YAC5F,+BAAc,CAAC,YAAY,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;OAIG;IACH,+CAA+C;QAC3C,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,gEAAgE;YAChE,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;;;;OAOG;IACH,0CAA0C;QACtC,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC;YAC9D,MAAM,MAAM,GAAuB,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3E,KAAK,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;;;;OAOG;IACH,yMAAyM;IACzM,yBAAyB,CAAC,UAA2B,EAAE,OAAgC;QACnF,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC3B,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;oBAClB,kFAAkF;oBAClF,kCAAkC;oBAClC,SAAS;gBACb,CAAC;gBACD,+EAA+E;gBAC/E,qEAAqE;gBACrE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;oBAC5B,+BAAc,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBAC5C,CAAC;YACL,CAAC;YACD,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;IACP,CAAC;IAED,8FAA8F;IAC9F,KAAK,CAAC,uBAAuB;QACzB,MAAM,IAAI,GAAW,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;QACnE,MAAM,WAAW,GAAW,MAAM,+BAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;QACtF,KAAK,IAAI,CAAC;QACV,KAAK,WAAW,CAAC;IACrB,CAAC;CACJ;AA1FD,wEA0FC","sourcesContent":["import { AnyContextKey, ContextKey } from '@webpieces/core-util';\nimport { RequestContext } from './RequestContext';\n\n/**\n * COMPILE-TIME assertions for {@link RequestContext.runDetachedScope}.\n *\n * The runtime half — that the detached scope starts empty, that the enclosing scope survives, that a\n * throw unwinds it — is in `DetachedScope.spec.ts`. What a spec CANNOT express is the half that makes\n * the browser-log path safe BY CONSTRUCTION rather than by remembering to filter:\n *\n * - `runDetachedScope` has NO container-taking form, so the deleted `runWithContext(map, fn)` forgery\n * path cannot come back through this door;\n * - a loop over a mixed `AnyContextKey[]` cannot write ANY key until it has tested that key's trust;\n * - and a TRUSTED key can never reach `putUntrusted`, so a browser — which proves nothing — cannot\n * FABRICATE a proven value by naming `userId` in its payload. It is a compile error at the write,\n * not a filter someone has to remember to write.\n *\n * That is about the SOURCE, not about the key. Writing a trusted key is ordinary and legitimate — see\n * {@link putTrustedIsLegitimateInsideADetachedScope} — whenever the caller has actually proven the\n * value. What cannot be written down is a claim of proof by code that has none.\n *\n * In COMPILED source deliberately — `tsconfig.lib.json` excludes specs and vitest strips types with\n * esbuild, so a `@ts-expect-error` in a `.spec.ts` is inert and the suite would pass either way. Each\n * one below fails the build with TS2578 the day its line starts compiling. See\n * `CapturedContextCompileAssertions` and `RequestContextTrustCompileAssertions` for the sibling halves.\n */\nexport class DetachedScopeCompileAssertions {\n private readonly trusted = ContextKey.trusted<string>('assertDetachedUserId', 'jwt claim `sub`');\n private readonly untrusted = ContextKey.untrusted<string>('assertDetachedActionId');\n\n /**\n * THE hole this whole shape exists to keep shut: no Map/object/array of entries may cross the\n * boundary. Values are written INSIDE the closure, through the trust verbs.\n */\n cannotHandItAContainerOfEntries(): void {\n // @ts-expect-error - runDetachedScope takes ONLY a closure; there is no map-taking form\n RequestContext.runDetachedScope(new Map([['userId', 'victim']]), () => undefined);\n }\n\n /** Nor a plain object of entries, which is the same hole spelled differently. */\n cannotHandItAnObjectOfEntries(): void {\n // @ts-expect-error - the single parameter is the closure, not a bag of values\n RequestContext.runDetachedScope({ userId: 'victim' });\n }\n\n /** A key of unknown trust cannot be written AT ALL — the branch is not optional. */\n cannotWriteAMixedKeyWithoutTestingItsTrust(key: AnyContextKey): void {\n RequestContext.runDetachedScope(() => {\n // @ts-expect-error - putUntrusted needs a key KNOWN to be untrusted; AnyContextKey is mixed\n RequestContext.putUntrusted(key, 'from-the-browser');\n });\n }\n\n /**\n * And a TRUSTED key cannot be LAUNDERED through the untrusted verb, detached or not — which is\n * what a caller with no proof would have to do, since `putTrusted` is the only other way in and\n * saying it is a deliberate, greppable claim.\n */\n cannotLaunderATrustedKeyThroughTheUntrustedVerb(): void {\n RequestContext.runDetachedScope(() => {\n // @ts-expect-error - putUntrusted does not accept a trusted key\n RequestContext.putUntrusted(this.trusted, 'browser-said-so');\n });\n }\n\n /**\n * POSITIVE, and the point the negative above must not be mistaken for: writing a TRUSTED value\n * inside a detached scope is ordinary and correct when the caller has actually proven it — the\n * signed-webhook case (Twilio/WhatsApp proves the phone number, the app looks up the userId), or a\n * verified JWT claim. `putTrusted` is the verb for exactly that, and a detached scope does not\n * change it. Trust is about tamper-resistance, not secrecy: a trusted `userId` is a plain,\n * fully-logged GUID; masking is the separate `maskInLogs` axis.\n */\n putTrustedIsLegitimateInsideADetachedScope(): void {\n RequestContext.runDetachedScope(() => {\n RequestContext.putTrusted(this.trusted, 'proven-out-of-band');\n const proven: string | undefined = RequestContext.getTrusted(this.trusted);\n void proven;\n });\n }\n\n /**\n * POSITIVE, and the shape the real consumer writes: emit one browser line under a fresh scope\n * rebuilt from an EXTERNAL payload, driven off the registry's logged keys.\n *\n * The `isTrusted()` skip is what a reader sees; the compiler is what enforces it. Delete the skip\n * and the write below stops compiling, because the key is mixed again and `putUntrusted` refuses\n * it — see {@link cannotWriteAMixedKeyWithoutTestingItsTrust}.\n */\n // webpieces-disable no-any-unknown -- the EXTERNAL payload is by definition untyped JSON off the wire; that is precisely why every value below is trust-branched and typeof-checked before it is written\n theBrowserLogLoopCompiles(loggedKeys: AnyContextKey[], payload: Record<string, unknown>): void {\n RequestContext.runDetachedScope(() => {\n for (const key of loggedKeys) {\n if (key.isTrusted()) {\n // A browser cannot vouch for a proven fact. There is no write in this branch, and\n // no cast that could produce one.\n continue;\n }\n // Past the `continue`, the key is narrowed to the untrusted branch by the SAME\n // predicate — no second check, and no cast, to reach `putUntrusted`.\n const value = payload[key.name];\n if (typeof value === 'string') {\n RequestContext.putUntrusted(key, value);\n }\n }\n RequestContext.putUntrusted(this.untrusted, 'click-7');\n });\n }\n\n /** The return value flows through, and an async closure is a promise the caller can await. */\n async returnValuesFlowThrough(): Promise<void> {\n const sync: string = RequestContext.runDetachedScope(() => 'done');\n const asyncResult: string = await RequestContext.runDetachedScope(async () => 'done');\n void sync;\n void asyncResult;\n }\n}\n"]}
@@ -67,12 +67,10 @@ export declare class RequestContextHeaders {
67
67
  * the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`
68
68
  * and therefore the reason service-to-service identity propagation works at all.
69
69
  *
70
- * There is NO cast here. `isTrusted()` / `isUntrusted()` are type predicates, so each branch holds
71
- * the narrowed key already the runtime check produces the type it proves. Two positive `if`s
72
- * rather than an `if/else` because TypeScript cannot narrow the NEGATIVE of a predicate over
73
- * `AnyContextKey` (it is `ContextKey<unknown, Trust>`, one type rather than a union, so there is
74
- * nothing to `Exclude`); see {@link ContextKey.isUntrusted}. There is no third trust level, so the
75
- * fall-through is unreachable, not a silent drop.
70
+ * There is NO cast here, on EITHER branch. `isTrusted()` is a type predicate over the
71
+ * {@link AnyContextKey} union, so the `if` holds a trusted key and the `else` holds an untrusted
72
+ * one the runtime check produces the type it proves. Trust is binary and the union has exactly
73
+ * two constituents, so the `else` is the whole remaining case rather than a silent drop.
76
74
  */
77
75
  private acceptInbound;
78
76
  /**
@@ -121,19 +121,16 @@ let RequestContextHeaders = class RequestContextHeaders {
121
121
  * the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`
122
122
  * and therefore the reason service-to-service identity propagation works at all.
123
123
  *
124
- * There is NO cast here. `isTrusted()` / `isUntrusted()` are type predicates, so each branch holds
125
- * the narrowed key already the runtime check produces the type it proves. Two positive `if`s
126
- * rather than an `if/else` because TypeScript cannot narrow the NEGATIVE of a predicate over
127
- * `AnyContextKey` (it is `ContextKey<unknown, Trust>`, one type rather than a union, so there is
128
- * nothing to `Exclude`); see {@link ContextKey.isUntrusted}. There is no third trust level, so the
129
- * fall-through is unreachable, not a silent drop.
124
+ * There is NO cast here, on EITHER branch. `isTrusted()` is a type predicate over the
125
+ * {@link AnyContextKey} union, so the `if` holds a trusted key and the `else` holds an untrusted
126
+ * one the runtime check produces the type it proves. Trust is binary and the union has exactly
127
+ * two constituents, so the `else` is the whole remaining case rather than a silent drop.
130
128
  */
131
129
  acceptInbound(key, value) {
132
130
  if (key.isTrusted()) {
133
131
  PendingWireTrust_1.PendingWireTrust.stash(key, value);
134
- return;
135
132
  }
136
- if (key.isUntrusted()) {
133
+ else {
137
134
  RequestContext_1.RequestContext.putUntrusted(key, value);
138
135
  }
139
136
  }
@@ -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 headers\n *\n * Reads the AsyncLocalStorage-backed {@link RequestContext} straight through — no ContextReader,\n * no ContextMgr, no abstract base. A server has exactly one place its context lives, and the\n * indirection only hid the failure below. (The browser's answer is `ContextMgr` in\n * @webpieces/core-util, which reads an app-held store because a browser has no ambient scope.)\n *\n * FAILS FAST outside a RequestContext. Silently sending an outbound call with NO request id or\n * tenant is far worse than a loud error — the trace just disappears and you find out in production. Every server-side client (RPC and Cloud Tasks) therefore only works\n * inside `RequestContext.run(...)`, which a top-level server filter normally establishes for you.\n *\n * Stateless once built, so it binds as a framework singleton every server-side client shares.\n */\n@provideFrameworkSingleton()\nexport class RequestContextHeaders {\n /**\n * Every transferred key with a non-empty value 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 `@Public` / `@AuthJwt` 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 * 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. `isTrusted()` / `isUntrusted()` are type predicates, so each branch holds\n * the narrowed key already — the runtime check produces the type it proves. Two positive `if`s\n * rather than an `if/else` because TypeScript cannot narrow the NEGATIVE of a predicate over\n * `AnyContextKey` (it is `ContextKey<unknown, Trust>`, one type rather than a union, so there is\n * nothing to `Exclude`); see {@link ContextKey.isUntrusted}. There is no third trust level, so the\n * fall-through is unreachable, not a silent drop.\n */\n private acceptInbound(key: AnyContextKey, value: string): void {\n if (key.isTrusted()) {\n PendingWireTrust.stash(key, value);\n return;\n }\n if (key.isUntrusted()) {\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 headers\n *\n * Reads the AsyncLocalStorage-backed {@link RequestContext} straight through — no ContextReader,\n * no ContextMgr, no abstract base. A server has exactly one place its context lives, and the\n * indirection only hid the failure below. (The browser's answer is `ContextMgr` in\n * @webpieces/core-util, which reads an app-held store because a browser has no ambient scope.)\n *\n * FAILS FAST outside a RequestContext. Silently sending an outbound call with NO request id or\n * tenant is far worse than a loud error — the trace just disappears and you find out in production. Every server-side client (RPC and Cloud Tasks) therefore only works\n * inside `RequestContext.run(...)`, which a top-level server filter normally establishes for you.\n *\n * Stateless once built, so it binds as a framework singleton every server-side client shares.\n */\n@provideFrameworkSingleton()\nexport class RequestContextHeaders {\n /**\n * Every transferred key with a non-empty value 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 `@Public` / `@AuthJwt` 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 * 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"]}