@webpieces/core-util 0.4.606 → 0.4.607

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-util",
3
- "version": "0.4.606",
3
+ "version": "0.4.607",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -175,8 +175,32 @@ export declare class ContextKey<V, T extends Trust = Trust> {
175
175
  * True for a key built by {@link trusted}. The RUNTIME check used by the inbound fill and the
176
176
  * AuthFilter reconciliation; ordinary application code should never need it, because the typed
177
177
  * verbs already made the decision at compile time.
178
+ *
179
+ * It is a TYPE PREDICATE, so the branch that takes it holds a `ContextKey<V, 'trusted'>` and can
180
+ * hand the key straight to `putTrusted` / `PendingWireTrust.stash` with NO cast. Before this, every
181
+ * such site wrote `key as AnyTrustedContextKey` — a cast is exactly the thing an agent copies to
182
+ * the one place it is not warranted, so the runtime check now produces the type it proves.
183
+ */
184
+ isTrusted(): this is ContextKey<V, 'trusted'>;
185
+ /**
186
+ * The other branch — true for a key built by {@link untrusted}, narrowing to
187
+ * `ContextKey<V, 'untrusted'>` so the key can go straight to `putUntrusted`.
188
+ *
189
+ * NOT a second spelling of `!isTrusted()`: that expression cannot narrow anything. TypeScript
190
+ * narrows the NEGATIVE of a `this is X` predicate by `Exclude`, and an {@link AnyContextKey} is
191
+ * `ContextKey<unknown, Trust>` — a single type, not a union of the two branches — so there is
192
+ * nothing to exclude and the key stays mixed. A positive predicate per branch is the only form that
193
+ * types the branch it guards, which is why both exist and why neither takes an argument saying
194
+ * which one you meant.
195
+ *
196
+ * This is what makes a loop over a mixed `AnyContextKey[]` — the {@link HeaderRegistry} arrays, a
197
+ * browser-log payload re-stated into a detached scope — safe BY CONSTRUCTION: the loop can only
198
+ * write a key whose trust it has just tested, and a trusted key cannot reach `putUntrusted` at all,
199
+ * so a loop fed by a source that proves nothing cannot fabricate a proven value, and nobody has to
200
+ * remember to filter. That is a limit on the SOURCE, not on the key: a trusted key is written all
201
+ * the time via `putTrusted`, by an authenticator or by app code that proved the value out of band.
178
202
  */
179
- isTrusted(): boolean;
203
+ isUntrusted(): this is ContextKey<V, 'untrusted'>;
180
204
  /**
181
205
  * The value as it should appear in a log line: returned as-is for a normal
182
206
  * key, partially masked when this key sets `maskInLogs`. Masking is length-based:
package/src/ContextKey.js CHANGED
@@ -155,10 +155,36 @@ class ContextKey {
155
155
  * True for a key built by {@link trusted}. The RUNTIME check used by the inbound fill and the
156
156
  * AuthFilter reconciliation; ordinary application code should never need it, because the typed
157
157
  * verbs already made the decision at compile time.
158
+ *
159
+ * It is a TYPE PREDICATE, so the branch that takes it holds a `ContextKey<V, 'trusted'>` and can
160
+ * hand the key straight to `putTrusted` / `PendingWireTrust.stash` with NO cast. Before this, every
161
+ * such site wrote `key as AnyTrustedContextKey` — a cast is exactly the thing an agent copies to
162
+ * the one place it is not warranted, so the runtime check now produces the type it proves.
158
163
  */
159
164
  isTrusted() {
160
165
  return this.trust === 'trusted';
161
166
  }
167
+ /**
168
+ * The other branch — true for a key built by {@link untrusted}, narrowing to
169
+ * `ContextKey<V, 'untrusted'>` so the key can go straight to `putUntrusted`.
170
+ *
171
+ * NOT a second spelling of `!isTrusted()`: that expression cannot narrow anything. TypeScript
172
+ * narrows the NEGATIVE of a `this is X` predicate by `Exclude`, and an {@link AnyContextKey} is
173
+ * `ContextKey<unknown, Trust>` — a single type, not a union of the two branches — so there is
174
+ * nothing to exclude and the key stays mixed. A positive predicate per branch is the only form that
175
+ * types the branch it guards, which is why both exist and why neither takes an argument saying
176
+ * which one you meant.
177
+ *
178
+ * This is what makes a loop over a mixed `AnyContextKey[]` — the {@link HeaderRegistry} arrays, a
179
+ * browser-log payload re-stated into a detached scope — safe BY CONSTRUCTION: the loop can only
180
+ * write a key whose trust it has just tested, and a trusted key cannot reach `putUntrusted` at all,
181
+ * so a loop fed by a source that proves nothing cannot fabricate a proven value, and nobody has to
182
+ * remember to filter. That is a limit on the SOURCE, not on the key: a trusted key is written all
183
+ * the time via `putTrusted`, by an authenticator or by app code that proved the value out of band.
184
+ */
185
+ isUntrusted() {
186
+ return this.trust === 'untrusted';
187
+ }
162
188
  /**
163
189
  * The value as it should appear in a log line: returned as-is for a normal
164
190
  * key, partially masked when this key sets `maskInLogs`. Masking is length-based:
@@ -1 +1 @@
1
- {"version":3,"file":"ContextKey.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKey.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;;;AAkCH,MAAa,UAAU;IAgBnB,qEAAqE;IAC5D,IAAI,CAAS;IAEtB;;;OAGG;IACM,UAAU,CAAU;IAE7B,0EAA0E;IACjE,KAAK,CAAQ;IAEtB;;;;;OAKG;IACM,UAAU,CAAU;IAE7B,4FAA4F;IACnF,UAAU,CAAU;IAE7B,6EAA6E;IACpE,QAAQ,CAAU;IAE3B;;;;OAIG;IACH,YACI,IAAY,EACZ,KAAY,EACZ,UAA8B,EAC9B,UAA8B,EAC9B,UAAmB,EACnB,QAAiB;QAEjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;IAED;;;;;;;;OAQG;IACH,6KAA6K;IAC7K,MAAM,CAAC,OAAO,CACV,IAAY,EACZ,UAAkB,EAClB,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI;QAEf,OAAO,IAAI,UAAU,CAAe,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IACvG,CAAC;IAED;;;;;;;OAOG;IACH,uHAAuH;IACvH,MAAM,CAAC,SAAS,CACZ,IAAY,EACZ,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI;QAEf,OAAO,IAAI,UAAU,CAAiB,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC1G,CAAC;IAED,uEAAuE;IACvE,aAAa;QACT,OAAO,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC;IACzC,CAAC;IAED;;;;OAIG;IACH,SAAS;QACL,OAAO,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC;IACpC,CAAC;IAED;;;;;;OAMG;IACH,WAAW,CAAC,KAAa;QACrB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACnB,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;QACzB,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YACV,OAAO,+BAA+B,CAAC;QAC3C,CAAC;aAAM,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC;YACnB,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC;QACzC,CAAC;aAAM,CAAC;YACJ,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;QACpE,CAAC;IACL,CAAC;CACJ;AAvID,gCAuIC","sourcesContent":["/**\n * ContextKey - a single key that travels in the request's \"magic context\"\n * (RequestContext on the server, MutableContextStore in the browser).\n *\n * This ONE class replaces the old split of `Header` (interface) + `PlatformHeader`\n * (class) + `ContextKey` (class). Every context value — whether it rides over HTTP\n * (request-id, tenant, authorization) or stays in-process (method-meta, the\n * TestCaseRecorder) — is a `ContextKey`.\n *\n * The fields are named for what they DO (flipped from the old model):\n * - `name` ALWAYS set. The context storage key, the log/MDC key, and the\n * recorder name. e.g. 'requestId', 'tenantId', 'authorization'.\n * - `httpHeader` OPTIONAL. When set, this key is transferred over the wire under\n * this HTTP header name (inbound request -> context, and context ->\n * outbound request). e.g. 'x-request-id'. When UNSET, the key is\n * context-only and never leaves the process (method-meta, recorder).\n * - `trust` REQUIRED, and stated by WHICH FACTORY you call. See below.\n * - `maskInLogs` When true, the value is masked (partially) in logs. This is about\n * LOG REDACTION and has NOTHING to do with `trust` — a userId is\n * trusted AND fully logged; a bearer token is untrusted AND masked.\n * (Formerly `isSecured`, renamed because sitting next to `trust` the\n * old name read as \"this value is secure\", which it never meant.)\n * - `isLogged` Defaults to true. When false, the value is NEVER logged (used for\n * object-valued/internal keys like the recorder or method-meta that\n * must not be serialized into log lines).\n *\n * ## TRUST — the whole point of this class\n *\n * A context value is either something the framework PROVED (`trusted`) or something a\n * caller merely ASSERTED (`untrusted`). That distinction is invisible in a `Map<string,\n * string>`, so code — human- or AI-written — routinely reads a spoofable header as if it\n * were an authenticated fact. `userId` is the canonical example: it is a verified JWT\n * claim on one route and an attacker-supplied `x-user-id` on the next, and nothing in the\n * old API told them apart.\n *\n * So trust is declared ON THE KEY, at the single place the key is defined, and it is\n * enforced at BOTH ends:\n *\n * ContextKey.trusted<string>('userId', 'jwt claim `sub`, stamped by AuthFilter', 'x-user-id')\n * ContextKey.untrusted<string>('actionId', 'x-webpieces-actionid')\n *\n * `grep -rn \"ContextKey.trusted\"` therefore enumerates every high-assurance field in the\n * codebase, each with its `provenance` string on the same line saying WHY it is trusted.\n * That is the AI-facing payoff: the answer is one grep, not an audit.\n *\n * The constructor is PRIVATE — you cannot make a key without picking a branch, and there\n * is no default. A default would make the permissive branch the shortest thing to type,\n * which is exactly the \"widening that is an ABSENCE rather than a token\" that CLAUDE.md\n * rejects. `provenance` is a REQUIRED positional argument on the trusted factory only, so\n * \"trusted with no stated reason\" cannot be written down.\n *\n * ## What makes the `trusted` label HONEST at runtime\n *\n * The label would be a lie if anything could write a trusted key from an unverified\n * source. Three enforced facts prevent that:\n *\n * 1. WRITES are typed: only `RequestContext.putTrusted(key, value)` accepts a trusted key,\n * and it is a distinct, greppable verb an app has to type on purpose.\n * 2. INBOUND wire values for trusted keys never enter the context directly.\n * `RequestContextHeaders.fillFromRequest` stashes them as PENDING, and `AuthFilter`\n * admits them only after it knows who the caller is (see {@link PendingWireTrust}).\n * 3. READS are typed: `getTrusted(key)` does not compile for an untrusted key, and\n * `getUntrusted(key)` does not compile for a trusted one. Picking a verb is\n * unavoidable, so a reader always knows which kind of value it is holding.\n *\n * Trusted keys DO keep their `httpHeader` — service-to-service propagation of a verified\n * userId is a first-class requirement, not a hole. It is safe because rule 2 gates it on\n * the endpoint's own auth mode: a route that verified WHO called it (`@AuthOidc`,\n * `@AuthSharedSecret`) accepts the caller's trusted headers; a route reachable by a\n * browser (`@AuthJwt`, public) does not.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n *\n * The type parameter `V` is the TYPE OF THE VALUE stored under this key — `string` for the wire/log\n * keys (requestId, tenantId, ...), `ApiCallInfo` for the structured api tag, `TestCaseRecorder` for\n * the recorder. It is REQUIRED (no default): every key must state what it holds. A heterogeneous\n * store CANNOT be a `Record<string, string>` — the recorder and the api payload are not strings — so\n * instead each KEY carries its own value type, and the typed accessors INFER it from the key. That\n * keeps the backing Map honestly type-erased while the public surface stays fully typed: a caller\n * never asserts a value type, the key already declares it. A genuinely mixed collection of keys is\n * spelled explicitly as `AnyContextKey[]`, so \"I mean a mixed bag\" is a visible, deliberate\n * statement, never a default.\n *\n * The type parameter `T` is the TRUST LEVEL, carried as a phantom type so the accessor verbs can\n * reject the wrong kind of key at COMPILE time rather than throwing at runtime (CLAUDE.md treats a\n * runtime throw standing in for an expressible type as a defect).\n */\n\n/** The two kinds of context value. See the {@link ContextKey} class doc. */\nexport type Trust = 'trusted' | 'untrusted';\n\n/**\n * A ContextKey whose value type is intentionally UNCONSTRAINED — a \"key of any value type\". Use this\n * (never a bare `ContextKey`, which no longer compiles) for genuinely mixed-bag collections and\n * key-agnostic code: `ALL_HEADERS: AnyContextKey[]`, the {@link HeaderRegistry}'s key arrays, a\n * reader that takes whatever key it is handed. Naming the mixed case makes \"I mean any key\" a visible,\n * deliberate statement, and confines the one sanctioned `unknown` to this single alias instead of\n * scattering `ContextKey<unknown>` — and its disable comment — across the codebase.\n *\n * NOTE this is mixed in TRUST as well as in value type, so it is READ-ONLY territory: `getAny(key)`\n * takes one, but no WRITE verb does. A write must name the trust level, which is what keeps the\n * `trusted` label honest.\n */\n// webpieces-disable no-any-unknown -- the ONE sanctioned `unknown`: a key whose value type is deliberately unconstrained (mixed-bag collections / key-agnostic code). Every other site names AnyContextKey instead of repeating this.\nexport type AnyContextKey = ContextKey<unknown>;\n\n/**\n * A trusted key of any value type — what {@link ContextTuple} carries, and what the trusted write\n * verb accepts when the value type is not statically known.\n */\n// webpieces-disable no-any-unknown -- same sanctioned mixed-bag alias as AnyContextKey, narrowed to the trusted branch\nexport type AnyTrustedContextKey = ContextKey<unknown, 'trusted'>;\n\n/**\n * An untrusted key of any value type — what the {@link ApiCallContext} seam stamps, so that seam\n * cannot be used as a side door to forge a trusted value.\n */\n// webpieces-disable no-any-unknown -- same sanctioned mixed-bag alias as AnyContextKey, narrowed to the untrusted branch\nexport type AnyUntrustedContextKey = ContextKey<unknown, 'untrusted'>;\n\nexport class ContextKey<V, T extends Trust = Trust> {\n /**\n * Phantom marker carrying the value type {@link V}. It has no runtime existence (`declare`, never\n * assigned) — it exists ONLY so the read verbs return `V` and the write verbs check `value`\n * against `V`, both inferred straight from the key. Optional, so `ContextKey<A>` stays assignable\n * to `AnyContextKey` (i.e. `ContextKey<unknown>`) — arrays of mixed keys keep working.\n */\n declare readonly __valueType?: V;\n\n /**\n * Phantom marker carrying the trust level {@link T} — the reason `getTrusted(SOME_UNTRUSTED_KEY)`\n * is a COMPILE error and not a runtime throw. Like `__valueType` it never exists at runtime; the\n * runtime answer is the {@link trust} field below, which the fill/reconcile path reads.\n */\n declare readonly __trust?: T;\n\n /** Context storage key + log/MDC key + recorder name. Always set. */\n readonly name: string;\n\n /**\n * HTTP header name when this key is transferred over the wire (e.g.\n * 'x-request-id'). Undefined = context-only, never transferred.\n */\n readonly httpHeader?: string;\n\n /** The runtime twin of the phantom {@link __trust}. See the class doc. */\n readonly trust: Trust;\n\n /**\n * WHY this key is trusted, in prose — 'jwt claim `sub`, stamped by AuthFilter', or\n * 'whatsapp webhook phone number -> user lookup'. Required on a trusted key, absent on an\n * untrusted one. It exists so that grepping the trusted keys also tells you what proves each\n * one, without opening another file.\n */\n readonly provenance?: string;\n\n /** Mask this value (partially) in logs. Log redaction only — unrelated to {@link trust}. */\n readonly maskInLogs: boolean;\n\n /** Whether this key is logged at all. Default true; false = never logged. */\n readonly isLogged: boolean;\n\n /**\n * PRIVATE — use {@link trusted} or {@link untrusted}. There is deliberately no way to build a key\n * without stating its trust level: a defaulted trust argument would make the permissive branch\n * the shortest thing to type and impossible to grep.\n */\n private constructor(\n name: string,\n trust: Trust,\n provenance: string | undefined,\n httpHeader: string | undefined,\n maskInLogs: boolean,\n isLogged: boolean,\n ) {\n this.name = name;\n this.trust = trust;\n this.provenance = provenance;\n this.httpHeader = httpHeader;\n this.maskInLogs = maskInLogs;\n this.isLogged = isLogged;\n }\n\n /**\n * A key whose value the framework PROVED — a verified JWT claim, or a fact an app derived from a\n * verified credential (a Twilio/WhatsApp webhook's signed phone number looked up to a userId).\n *\n * Only `RequestContext.putTrusted` can write one, only `RequestContext.getTrusted` can read one,\n * and an inbound wire value for one is held PENDING until `AuthFilter` knows who the caller is.\n *\n * @param provenance WHY it is trusted, in prose. Required — see {@link ContextKey.provenance}.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor; the trust branch must be part of the call, not a defaulted argument\n static trusted<V>(\n name: string,\n provenance: string,\n httpHeader?: string,\n maskInLogs = false,\n isLogged = true,\n ): ContextKey<V, 'trusted'> {\n return new ContextKey<V, 'trusted'>(name, 'trusted', provenance, httpHeader, maskInLogs, isLogged);\n }\n\n /**\n * A key whose value is merely ASSERTED by whoever sent it — a browser-minted actionId, a\n * client-supplied recording flag, an in-process log tag. Perfectly fine to use; just never\n * an input to an authorization decision.\n *\n * This is the DEFAULT choice in the sense that most keys are this — but it is never the default\n * VALUE: you still type the word, so reading a key definition always tells you which it is.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor; see trusted()\n static untrusted<V>(\n name: string,\n httpHeader?: string,\n maskInLogs = false,\n isLogged = true,\n ): ContextKey<V, 'untrusted'> {\n return new ContextKey<V, 'untrusted'>(name, 'untrusted', undefined, httpHeader, maskInLogs, isLogged);\n }\n\n /** True when this key is transferred over HTTP (has an httpHeader). */\n isTransferred(): boolean {\n return this.httpHeader !== undefined;\n }\n\n /**\n * True for a key built by {@link trusted}. The RUNTIME check used by the inbound fill and the\n * AuthFilter reconciliation; ordinary application code should never need it, because the typed\n * verbs already made the decision at compile time.\n */\n isTrusted(): boolean {\n return this.trust === 'trusted';\n }\n\n /**\n * The value as it should appear in a log line: returned as-is for a normal\n * key, partially masked when this key sets `maskInLogs`. Masking is length-based:\n * - Length > 15: first 3 + \"...\" + last 3\n * - Length 8-15: first 2 + \"...\"\n * - Length < 8: \"<secure key too short to log>\"\n */\n maskForLogs(value: string): string {\n if (!this.maskInLogs) {\n return value;\n }\n const len = value.length;\n if (len < 8) {\n return '<secure key too short to log>';\n } else if (len <= 15) {\n return `${value.substring(0, 2)}...`;\n } else {\n return `${value.substring(0, 3)}...${value.substring(len - 3)}`;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"ContextKey.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKey.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;;;AAkCH,MAAa,UAAU;IAgBnB,qEAAqE;IAC5D,IAAI,CAAS;IAEtB;;;OAGG;IACM,UAAU,CAAU;IAE7B,0EAA0E;IACjE,KAAK,CAAQ;IAEtB;;;;;OAKG;IACM,UAAU,CAAU;IAE7B,4FAA4F;IACnF,UAAU,CAAU;IAE7B,6EAA6E;IACpE,QAAQ,CAAU;IAE3B;;;;OAIG;IACH,YACI,IAAY,EACZ,KAAY,EACZ,UAA8B,EAC9B,UAA8B,EAC9B,UAAmB,EACnB,QAAiB;QAEjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;IAED;;;;;;;;OAQG;IACH,6KAA6K;IAC7K,MAAM,CAAC,OAAO,CACV,IAAY,EACZ,UAAkB,EAClB,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI;QAEf,OAAO,IAAI,UAAU,CAAe,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IACvG,CAAC;IAED;;;;;;;OAOG;IACH,uHAAuH;IACvH,MAAM,CAAC,SAAS,CACZ,IAAY,EACZ,UAAmB,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,IAAI;QAEf,OAAO,IAAI,UAAU,CAAiB,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC1G,CAAC;IAED,uEAAuE;IACvE,aAAa;QACT,OAAO,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC;IACzC,CAAC;IAED;;;;;;;;;OASG;IACH,SAAS;QACL,OAAO,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC;IACpC,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,WAAW;QACP,OAAO,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC;IACtC,CAAC;IAED;;;;;;OAMG;IACH,WAAW,CAAC,KAAa;QACrB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACnB,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;QACzB,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YACV,OAAO,+BAA+B,CAAC;QAC3C,CAAC;aAAM,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC;YACnB,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC;QACzC,CAAC;aAAM,CAAC;YACJ,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;QACpE,CAAC;IACL,CAAC;CACJ;AAlKD,gCAkKC","sourcesContent":["/**\n * ContextKey - a single key that travels in the request's \"magic context\"\n * (RequestContext on the server, MutableContextStore in the browser).\n *\n * This ONE class replaces the old split of `Header` (interface) + `PlatformHeader`\n * (class) + `ContextKey` (class). Every context value — whether it rides over HTTP\n * (request-id, tenant, authorization) or stays in-process (method-meta, the\n * TestCaseRecorder) — is a `ContextKey`.\n *\n * The fields are named for what they DO (flipped from the old model):\n * - `name` ALWAYS set. The context storage key, the log/MDC key, and the\n * recorder name. e.g. 'requestId', 'tenantId', 'authorization'.\n * - `httpHeader` OPTIONAL. When set, this key is transferred over the wire under\n * this HTTP header name (inbound request -> context, and context ->\n * outbound request). e.g. 'x-request-id'. When UNSET, the key is\n * context-only and never leaves the process (method-meta, recorder).\n * - `trust` REQUIRED, and stated by WHICH FACTORY you call. See below.\n * - `maskInLogs` When true, the value is masked (partially) in logs. This is about\n * LOG REDACTION and has NOTHING to do with `trust` — a userId is\n * trusted AND fully logged; a bearer token is untrusted AND masked.\n * (Formerly `isSecured`, renamed because sitting next to `trust` the\n * old name read as \"this value is secure\", which it never meant.)\n * - `isLogged` Defaults to true. When false, the value is NEVER logged (used for\n * object-valued/internal keys like the recorder or method-meta that\n * must not be serialized into log lines).\n *\n * ## TRUST — the whole point of this class\n *\n * A context value is either something the framework PROVED (`trusted`) or something a\n * caller merely ASSERTED (`untrusted`). That distinction is invisible in a `Map<string,\n * string>`, so code — human- or AI-written — routinely reads a spoofable header as if it\n * were an authenticated fact. `userId` is the canonical example: it is a verified JWT\n * claim on one route and an attacker-supplied `x-user-id` on the next, and nothing in the\n * old API told them apart.\n *\n * So trust is declared ON THE KEY, at the single place the key is defined, and it is\n * enforced at BOTH ends:\n *\n * ContextKey.trusted<string>('userId', 'jwt claim `sub`, stamped by AuthFilter', 'x-user-id')\n * ContextKey.untrusted<string>('actionId', 'x-webpieces-actionid')\n *\n * `grep -rn \"ContextKey.trusted\"` therefore enumerates every high-assurance field in the\n * codebase, each with its `provenance` string on the same line saying WHY it is trusted.\n * That is the AI-facing payoff: the answer is one grep, not an audit.\n *\n * The constructor is PRIVATE — you cannot make a key without picking a branch, and there\n * is no default. A default would make the permissive branch the shortest thing to type,\n * which is exactly the \"widening that is an ABSENCE rather than a token\" that CLAUDE.md\n * rejects. `provenance` is a REQUIRED positional argument on the trusted factory only, so\n * \"trusted with no stated reason\" cannot be written down.\n *\n * ## What makes the `trusted` label HONEST at runtime\n *\n * The label would be a lie if anything could write a trusted key from an unverified\n * source. Three enforced facts prevent that:\n *\n * 1. WRITES are typed: only `RequestContext.putTrusted(key, value)` accepts a trusted key,\n * and it is a distinct, greppable verb an app has to type on purpose.\n * 2. INBOUND wire values for trusted keys never enter the context directly.\n * `RequestContextHeaders.fillFromRequest` stashes them as PENDING, and `AuthFilter`\n * admits them only after it knows who the caller is (see {@link PendingWireTrust}).\n * 3. READS are typed: `getTrusted(key)` does not compile for an untrusted key, and\n * `getUntrusted(key)` does not compile for a trusted one. Picking a verb is\n * unavoidable, so a reader always knows which kind of value it is holding.\n *\n * Trusted keys DO keep their `httpHeader` — service-to-service propagation of a verified\n * userId is a first-class requirement, not a hole. It is safe because rule 2 gates it on\n * the endpoint's own auth mode: a route that verified WHO called it (`@AuthOidc`,\n * `@AuthSharedSecret`) accepts the caller's trusted headers; a route reachable by a\n * browser (`@AuthJwt`, public) does not.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n *\n * The type parameter `V` is the TYPE OF THE VALUE stored under this key — `string` for the wire/log\n * keys (requestId, tenantId, ...), `ApiCallInfo` for the structured api tag, `TestCaseRecorder` for\n * the recorder. It is REQUIRED (no default): every key must state what it holds. A heterogeneous\n * store CANNOT be a `Record<string, string>` — the recorder and the api payload are not strings — so\n * instead each KEY carries its own value type, and the typed accessors INFER it from the key. That\n * keeps the backing Map honestly type-erased while the public surface stays fully typed: a caller\n * never asserts a value type, the key already declares it. A genuinely mixed collection of keys is\n * spelled explicitly as `AnyContextKey[]`, so \"I mean a mixed bag\" is a visible, deliberate\n * statement, never a default.\n *\n * The type parameter `T` is the TRUST LEVEL, carried as a phantom type so the accessor verbs can\n * reject the wrong kind of key at COMPILE time rather than throwing at runtime (CLAUDE.md treats a\n * runtime throw standing in for an expressible type as a defect).\n */\n\n/** The two kinds of context value. See the {@link ContextKey} class doc. */\nexport type Trust = 'trusted' | 'untrusted';\n\n/**\n * A ContextKey whose value type is intentionally UNCONSTRAINED — a \"key of any value type\". Use this\n * (never a bare `ContextKey`, which no longer compiles) for genuinely mixed-bag collections and\n * key-agnostic code: `ALL_HEADERS: AnyContextKey[]`, the {@link HeaderRegistry}'s key arrays, a\n * reader that takes whatever key it is handed. Naming the mixed case makes \"I mean any key\" a visible,\n * deliberate statement, and confines the one sanctioned `unknown` to this single alias instead of\n * scattering `ContextKey<unknown>` — and its disable comment — across the codebase.\n *\n * NOTE this is mixed in TRUST as well as in value type, so it is READ-ONLY territory: `getAny(key)`\n * takes one, but no WRITE verb does. A write must name the trust level, which is what keeps the\n * `trusted` label honest.\n */\n// webpieces-disable no-any-unknown -- the ONE sanctioned `unknown`: a key whose value type is deliberately unconstrained (mixed-bag collections / key-agnostic code). Every other site names AnyContextKey instead of repeating this.\nexport type AnyContextKey = ContextKey<unknown>;\n\n/**\n * A trusted key of any value type — what {@link ContextTuple} carries, and what the trusted write\n * verb accepts when the value type is not statically known.\n */\n// webpieces-disable no-any-unknown -- same sanctioned mixed-bag alias as AnyContextKey, narrowed to the trusted branch\nexport type AnyTrustedContextKey = ContextKey<unknown, 'trusted'>;\n\n/**\n * An untrusted key of any value type — what the {@link ApiCallContext} seam stamps, so that seam\n * cannot be used as a side door to forge a trusted value.\n */\n// webpieces-disable no-any-unknown -- same sanctioned mixed-bag alias as AnyContextKey, narrowed to the untrusted branch\nexport type AnyUntrustedContextKey = ContextKey<unknown, 'untrusted'>;\n\nexport class ContextKey<V, T extends Trust = Trust> {\n /**\n * Phantom marker carrying the value type {@link V}. It has no runtime existence (`declare`, never\n * assigned) — it exists ONLY so the read verbs return `V` and the write verbs check `value`\n * against `V`, both inferred straight from the key. Optional, so `ContextKey<A>` stays assignable\n * to `AnyContextKey` (i.e. `ContextKey<unknown>`) — arrays of mixed keys keep working.\n */\n declare readonly __valueType?: V;\n\n /**\n * Phantom marker carrying the trust level {@link T} — the reason `getTrusted(SOME_UNTRUSTED_KEY)`\n * is a COMPILE error and not a runtime throw. Like `__valueType` it never exists at runtime; the\n * runtime answer is the {@link trust} field below, which the fill/reconcile path reads.\n */\n declare readonly __trust?: T;\n\n /** Context storage key + log/MDC key + recorder name. Always set. */\n readonly name: string;\n\n /**\n * HTTP header name when this key is transferred over the wire (e.g.\n * 'x-request-id'). Undefined = context-only, never transferred.\n */\n readonly httpHeader?: string;\n\n /** The runtime twin of the phantom {@link __trust}. See the class doc. */\n readonly trust: Trust;\n\n /**\n * WHY this key is trusted, in prose — 'jwt claim `sub`, stamped by AuthFilter', or\n * 'whatsapp webhook phone number -> user lookup'. Required on a trusted key, absent on an\n * untrusted one. It exists so that grepping the trusted keys also tells you what proves each\n * one, without opening another file.\n */\n readonly provenance?: string;\n\n /** Mask this value (partially) in logs. Log redaction only — unrelated to {@link trust}. */\n readonly maskInLogs: boolean;\n\n /** Whether this key is logged at all. Default true; false = never logged. */\n readonly isLogged: boolean;\n\n /**\n * PRIVATE — use {@link trusted} or {@link untrusted}. There is deliberately no way to build a key\n * without stating its trust level: a defaulted trust argument would make the permissive branch\n * the shortest thing to type and impossible to grep.\n */\n private constructor(\n name: string,\n trust: Trust,\n provenance: string | undefined,\n httpHeader: string | undefined,\n maskInLogs: boolean,\n isLogged: boolean,\n ) {\n this.name = name;\n this.trust = trust;\n this.provenance = provenance;\n this.httpHeader = httpHeader;\n this.maskInLogs = maskInLogs;\n this.isLogged = isLogged;\n }\n\n /**\n * A key whose value the framework PROVED — a verified JWT claim, or a fact an app derived from a\n * verified credential (a Twilio/WhatsApp webhook's signed phone number looked up to a userId).\n *\n * Only `RequestContext.putTrusted` can write one, only `RequestContext.getTrusted` can read one,\n * and an inbound wire value for one is held PENDING until `AuthFilter` knows who the caller is.\n *\n * @param provenance WHY it is trusted, in prose. Required — see {@link ContextKey.provenance}.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor; the trust branch must be part of the call, not a defaulted argument\n static trusted<V>(\n name: string,\n provenance: string,\n httpHeader?: string,\n maskInLogs = false,\n isLogged = true,\n ): ContextKey<V, 'trusted'> {\n return new ContextKey<V, 'trusted'>(name, 'trusted', provenance, httpHeader, maskInLogs, isLogged);\n }\n\n /**\n * A key whose value is merely ASSERTED by whoever sent it — a browser-minted actionId, a\n * client-supplied recording flag, an in-process log tag. Perfectly fine to use; just never\n * an input to an authorization decision.\n *\n * This is the DEFAULT choice in the sense that most keys are this — but it is never the default\n * VALUE: you still type the word, so reading a key definition always tells you which it is.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor; see trusted()\n static untrusted<V>(\n name: string,\n httpHeader?: string,\n maskInLogs = false,\n isLogged = true,\n ): ContextKey<V, 'untrusted'> {\n return new ContextKey<V, 'untrusted'>(name, 'untrusted', undefined, httpHeader, maskInLogs, isLogged);\n }\n\n /** True when this key is transferred over HTTP (has an httpHeader). */\n isTransferred(): boolean {\n return this.httpHeader !== undefined;\n }\n\n /**\n * True for a key built by {@link trusted}. The RUNTIME check used by the inbound fill and the\n * AuthFilter reconciliation; ordinary application code should never need it, because the typed\n * verbs already made the decision at compile time.\n *\n * It is a TYPE PREDICATE, so the branch that takes it holds a `ContextKey<V, 'trusted'>` and can\n * hand the key straight to `putTrusted` / `PendingWireTrust.stash` with NO cast. Before this, every\n * such site wrote `key as AnyTrustedContextKey` — a cast is exactly the thing an agent copies to\n * the one place it is not warranted, so the runtime check now produces the type it proves.\n */\n isTrusted(): this is ContextKey<V, 'trusted'> {\n return this.trust === 'trusted';\n }\n\n /**\n * The other branch — true for a key built by {@link untrusted}, narrowing to\n * `ContextKey<V, 'untrusted'>` so the key can go straight to `putUntrusted`.\n *\n * NOT a second spelling of `!isTrusted()`: that expression cannot narrow anything. TypeScript\n * narrows the NEGATIVE of a `this is X` predicate by `Exclude`, and an {@link AnyContextKey} is\n * `ContextKey<unknown, Trust>` — a single type, not a union of the two branches — so there is\n * nothing to exclude and the key stays mixed. A positive predicate per branch is the only form that\n * types the branch it guards, which is why both exist and why neither takes an argument saying\n * which one you meant.\n *\n * This is what makes a loop over a mixed `AnyContextKey[]` — the {@link HeaderRegistry} arrays, a\n * browser-log payload re-stated into a detached scope — safe BY CONSTRUCTION: the loop can only\n * write a key whose trust it has just tested, and a trusted key cannot reach `putUntrusted` at all,\n * so a loop fed by a source that proves nothing cannot fabricate a proven value, and nobody has to\n * remember to filter. That is a limit on the SOURCE, not on the key: a trusted key is written all\n * the time via `putTrusted`, by an authenticator or by app code that proved the value out of band.\n */\n isUntrusted(): this is ContextKey<V, 'untrusted'> {\n return this.trust === 'untrusted';\n }\n\n /**\n * The value as it should appear in a log line: returned as-is for a normal\n * key, partially masked when this key sets `maskInLogs`. Masking is length-based:\n * - Length > 15: first 3 + \"...\" + last 3\n * - Length 8-15: first 2 + \"...\"\n * - Length < 8: \"<secure key too short to log>\"\n */\n maskForLogs(value: string): string {\n if (!this.maskInLogs) {\n return value;\n }\n const len = value.length;\n if (len < 8) {\n return '<secure key too short to log>';\n } else if (len <= 15) {\n return `${value.substring(0, 2)}...`;\n } else {\n return `${value.substring(0, 3)}...${value.substring(len - 3)}`;\n }\n }\n}\n"]}