@webpieces/core-util 0.4.598 → 0.4.600

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/core-util",
3
- "version": "0.4.598",
3
+ "version": "0.4.600",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -8,30 +8,85 @@
8
8
  * TestCaseRecorder) — is a `ContextKey`.
9
9
  *
10
10
  * The fields are named for what they DO (flipped from the old model):
11
- * - `name` ALWAYS set. The context storage key, the log/MDC key, and the
12
- * recorder name. e.g. 'requestId', 'tenantId', 'authorization'.
13
- * - `httpHeader` OPTIONAL. When set, this key is transferred over the wire under
14
- * this HTTP header name (inbound request -> context, and context ->
15
- * outbound request). e.g. 'x-request-id'. When UNSET, the key is
16
- * context-only and never leaves the process (method-meta, recorder).
17
- * - `isSecured` When true, the value is masked (partially) in logs.
18
- * - `isLogged` Defaults to true. When false, the value is NEVER logged (used for
19
- * object-valued/internal keys like the recorder or method-meta that
20
- * must not be serialized into log lines).
11
+ * - `name` ALWAYS set. The context storage key, the log/MDC key, and the
12
+ * recorder name. e.g. 'requestId', 'tenantId', 'authorization'.
13
+ * - `httpHeader` OPTIONAL. When set, this key is transferred over the wire under
14
+ * this HTTP header name (inbound request -> context, and context ->
15
+ * outbound request). e.g. 'x-request-id'. When UNSET, the key is
16
+ * context-only and never leaves the process (method-meta, recorder).
17
+ * - `trust` REQUIRED, and stated by WHICH FACTORY you call. See below.
18
+ * - `maskInLogs` When true, the value is masked (partially) in logs. This is about
19
+ * LOG REDACTION and has NOTHING to do with `trust` — a userId is
20
+ * trusted AND fully logged; a bearer token is untrusted AND masked.
21
+ * (Formerly `isSecured`, renamed because sitting next to `trust` the
22
+ * old name read as "this value is secure", which it never meant.)
23
+ * - `isLogged` Defaults to true. When false, the value is NEVER logged (used for
24
+ * object-valued/internal keys like the recorder or method-meta that
25
+ * must not be serialized into log lines).
26
+ *
27
+ * ## TRUST — the whole point of this class
28
+ *
29
+ * A context value is either something the framework PROVED (`trusted`) or something a
30
+ * caller merely ASSERTED (`untrusted`). That distinction is invisible in a `Map<string,
31
+ * string>`, so code — human- or AI-written — routinely reads a spoofable header as if it
32
+ * were an authenticated fact. `userId` is the canonical example: it is a verified JWT
33
+ * claim on one route and an attacker-supplied `x-user-id` on the next, and nothing in the
34
+ * old API told them apart.
35
+ *
36
+ * So trust is declared ON THE KEY, at the single place the key is defined, and it is
37
+ * enforced at BOTH ends:
38
+ *
39
+ * ContextKey.trusted<string>('userId', 'jwt claim `sub`, stamped by AuthFilter', 'x-user-id')
40
+ * ContextKey.untrusted<string>('actionId', 'x-webpieces-actionid')
41
+ *
42
+ * `grep -rn "ContextKey.trusted"` therefore enumerates every high-assurance field in the
43
+ * codebase, each with its `provenance` string on the same line saying WHY it is trusted.
44
+ * That is the AI-facing payoff: the answer is one grep, not an audit.
45
+ *
46
+ * The constructor is PRIVATE — you cannot make a key without picking a branch, and there
47
+ * is no default. A default would make the permissive branch the shortest thing to type,
48
+ * which is exactly the "widening that is an ABSENCE rather than a token" that CLAUDE.md
49
+ * rejects. `provenance` is a REQUIRED positional argument on the trusted factory only, so
50
+ * "trusted with no stated reason" cannot be written down.
51
+ *
52
+ * ## What makes the `trusted` label HONEST at runtime
53
+ *
54
+ * The label would be a lie if anything could write a trusted key from an unverified
55
+ * source. Three enforced facts prevent that:
56
+ *
57
+ * 1. WRITES are typed: only `RequestContext.putTrusted(key, value)` accepts a trusted key,
58
+ * and it is a distinct, greppable verb an app has to type on purpose.
59
+ * 2. INBOUND wire values for trusted keys never enter the context directly.
60
+ * `RequestContextHeaders.fillFromRequest` stashes them as PENDING, and `AuthFilter`
61
+ * admits them only after it knows who the caller is (see {@link PendingWireTrust}).
62
+ * 3. READS are typed: `getTrusted(key)` does not compile for an untrusted key, and
63
+ * `getUntrusted(key)` does not compile for a trusted one. Picking a verb is
64
+ * unavoidable, so a reader always knows which kind of value it is holding.
65
+ *
66
+ * Trusted keys DO keep their `httpHeader` — service-to-service propagation of a verified
67
+ * userId is a first-class requirement, not a hole. It is safe because rule 2 gates it on
68
+ * the endpoint's own auth mode: a route that verified WHO called it (`@AuthOidc`,
69
+ * `@AuthSharedSecret`) accepts the caller's trusted headers; a route reachable by a
70
+ * browser (`@AuthJwt`, public) does not.
21
71
  *
22
72
  * Per CLAUDE.md: data-only structures are classes, not interfaces.
23
73
  *
24
74
  * The type parameter `V` is the TYPE OF THE VALUE stored under this key — `string` for the wire/log
25
75
  * keys (requestId, tenantId, ...), `ApiCallInfo` for the structured api tag, `TestCaseRecorder` for
26
- * the recorder. It is REQUIRED (no default): every key must state what it holds, so a legacy
27
- * `new ContextKey('x')` fails to compile until it declares `new ContextKey<string>('x')`the
28
- * type system does the migration for you. A heterogeneous store CANNOT be a `Record<string, string>`
29
- * the recorder and the api payload are not strings so instead each KEY carries its own value
30
- * type, and `RequestContext.getHeader/putHeader` INFER it from the key. That keeps the backing Map
31
- * honestly type-erased while the public surface stays fully typed: a caller never asserts a value
32
- * type, the key already declares it. A genuinely mixed collection of keys is spelled explicitly as
33
- * `AnyContextKey[]`, so "I mean a mixed bag" is a visible, deliberate statement, never a default.
76
+ * the recorder. It is REQUIRED (no default): every key must state what it holds. A heterogeneous
77
+ * store CANNOT be a `Record<string, string>` the recorder and the api payload are not strings so
78
+ * instead each KEY carries its own value type, and the typed accessors INFER it from the key. That
79
+ * keeps the backing Map honestly type-erased while the public surface stays fully typed: a caller
80
+ * never asserts a value type, the key already declares it. A genuinely mixed collection of keys is
81
+ * spelled explicitly as `AnyContextKey[]`, so "I mean a mixed bag" is a visible, deliberate
82
+ * statement, never a default.
83
+ *
84
+ * The type parameter `T` is the TRUST LEVEL, carried as a phantom type so the accessor verbs can
85
+ * reject the wrong kind of key at COMPILE time rather than throwing at runtime (CLAUDE.md treats a
86
+ * runtime throw standing in for an expressible type as a defect).
34
87
  */
88
+ /** The two kinds of context value. See the {@link ContextKey} class doc. */
89
+ export type Trust = 'trusted' | 'untrusted';
35
90
  /**
36
91
  * A ContextKey whose value type is intentionally UNCONSTRAINED — a "key of any value type". Use this
37
92
  * (never a bare `ContextKey`, which no longer compiles) for genuinely mixed-bag collections and
@@ -39,16 +94,36 @@
39
94
  * reader that takes whatever key it is handed. Naming the mixed case makes "I mean any key" a visible,
40
95
  * deliberate statement, and confines the one sanctioned `unknown` to this single alias instead of
41
96
  * scattering `ContextKey<unknown>` — and its disable comment — across the codebase.
97
+ *
98
+ * NOTE this is mixed in TRUST as well as in value type, so it is READ-ONLY territory: `getAny(key)`
99
+ * takes one, but no WRITE verb does. A write must name the trust level, which is what keeps the
100
+ * `trusted` label honest.
42
101
  */
43
102
  export type AnyContextKey = ContextKey<unknown>;
44
- export declare class ContextKey<V> {
103
+ /**
104
+ * A trusted key of any value type — what {@link ContextTuple} carries, and what the trusted write
105
+ * verb accepts when the value type is not statically known.
106
+ */
107
+ export type AnyTrustedContextKey = ContextKey<unknown, 'trusted'>;
108
+ /**
109
+ * An untrusted key of any value type — what the {@link ApiCallContext} seam stamps, so that seam
110
+ * cannot be used as a side door to forge a trusted value.
111
+ */
112
+ export type AnyUntrustedContextKey = ContextKey<unknown, 'untrusted'>;
113
+ export declare class ContextKey<V, T extends Trust = Trust> {
45
114
  /**
46
115
  * Phantom marker carrying the value type {@link V}. It has no runtime existence (`declare`, never
47
- * assigned) — it exists ONLY so `getHeader(key)` returns `V` and `putHeader(key, value)` checks
48
- * `value` against `V`, both inferred straight from the key. Optional, so `ContextKey<A>` stays
49
- * assignable to `AnyContextKey` (i.e. `ContextKey<unknown>`) — arrays of mixed keys keep working.
116
+ * assigned) — it exists ONLY so the read verbs return `V` and the write verbs check `value`
117
+ * against `V`, both inferred straight from the key. Optional, so `ContextKey<A>` stays assignable
118
+ * to `AnyContextKey` (i.e. `ContextKey<unknown>`) — arrays of mixed keys keep working.
50
119
  */
51
120
  readonly __valueType?: V;
121
+ /**
122
+ * Phantom marker carrying the trust level {@link T} — the reason `getTrusted(SOME_UNTRUSTED_KEY)`
123
+ * is a COMPILE error and not a runtime throw. Like `__valueType` it never exists at runtime; the
124
+ * runtime answer is the {@link trust} field below, which the fill/reconcile path reads.
125
+ */
126
+ readonly __trust?: T;
52
127
  /** Context storage key + log/MDC key + recorder name. Always set. */
53
128
  readonly name: string;
54
129
  /**
@@ -56,19 +131,58 @@ export declare class ContextKey<V> {
56
131
  * 'x-request-id'). Undefined = context-only, never transferred.
57
132
  */
58
133
  readonly httpHeader?: string;
59
- /** Mask this value (partially) in logs. */
60
- readonly isSecured: boolean;
134
+ /** The runtime twin of the phantom {@link __trust}. See the class doc. */
135
+ readonly trust: Trust;
136
+ /**
137
+ * WHY this key is trusted, in prose — 'jwt claim `sub`, stamped by AuthFilter', or
138
+ * 'whatsapp webhook phone number -> user lookup'. Required on a trusted key, absent on an
139
+ * untrusted one. It exists so that grepping the trusted keys also tells you what proves each
140
+ * one, without opening another file.
141
+ */
142
+ readonly provenance?: string;
143
+ /** Mask this value (partially) in logs. Log redaction only — unrelated to {@link trust}. */
144
+ readonly maskInLogs: boolean;
61
145
  /** Whether this key is logged at all. Default true; false = never logged. */
62
146
  readonly isLogged: boolean;
63
- constructor(name: string, httpHeader?: string, isSecured?: boolean, isLogged?: boolean);
147
+ /**
148
+ * PRIVATE — use {@link trusted} or {@link untrusted}. There is deliberately no way to build a key
149
+ * without stating its trust level: a defaulted trust argument would make the permissive branch
150
+ * the shortest thing to type and impossible to grep.
151
+ */
152
+ private constructor();
153
+ /**
154
+ * A key whose value the framework PROVED — a verified JWT claim, or a fact an app derived from a
155
+ * verified credential (a Twilio/WhatsApp webhook's signed phone number looked up to a userId).
156
+ *
157
+ * Only `RequestContext.putTrusted` can write one, only `RequestContext.getTrusted` can read one,
158
+ * and an inbound wire value for one is held PENDING until `AuthFilter` knows who the caller is.
159
+ *
160
+ * @param provenance WHY it is trusted, in prose. Required — see {@link ContextKey.provenance}.
161
+ */
162
+ static trusted<V>(name: string, provenance: string, httpHeader?: string, maskInLogs?: boolean, isLogged?: boolean): ContextKey<V, 'trusted'>;
163
+ /**
164
+ * A key whose value is merely ASSERTED by whoever sent it — a browser-minted actionId, a
165
+ * client-supplied recording flag, an in-process log tag. Perfectly fine to use; just never
166
+ * an input to an authorization decision.
167
+ *
168
+ * This is the DEFAULT choice in the sense that most keys are this — but it is never the default
169
+ * VALUE: you still type the word, so reading a key definition always tells you which it is.
170
+ */
171
+ static untrusted<V>(name: string, httpHeader?: string, maskInLogs?: boolean, isLogged?: boolean): ContextKey<V, 'untrusted'>;
64
172
  /** True when this key is transferred over HTTP (has an httpHeader). */
65
173
  isTransferred(): boolean;
174
+ /**
175
+ * True for a key built by {@link trusted}. The RUNTIME check used by the inbound fill and the
176
+ * AuthFilter reconciliation; ordinary application code should never need it, because the typed
177
+ * verbs already made the decision at compile time.
178
+ */
179
+ isTrusted(): boolean;
66
180
  /**
67
181
  * The value as it should appear in a log line: returned as-is for a normal
68
- * key, partially masked when this key is secured. Masking is length-based:
182
+ * key, partially masked when this key sets `maskInLogs`. Masking is length-based:
69
183
  * - Length > 15: first 3 + "..." + last 3
70
184
  * - Length 8-15: first 2 + "..."
71
185
  * - Length < 8: "<secure key too short to log>"
72
186
  */
73
- maskIfSecured(value: string): string;
187
+ maskForLogs(value: string): string;
74
188
  }
package/src/ContextKey.js CHANGED
@@ -1,4 +1,91 @@
1
1
  "use strict";
2
+ /**
3
+ * ContextKey - a single key that travels in the request's "magic context"
4
+ * (RequestContext on the server, MutableContextStore in the browser).
5
+ *
6
+ * This ONE class replaces the old split of `Header` (interface) + `PlatformHeader`
7
+ * (class) + `ContextKey` (class). Every context value — whether it rides over HTTP
8
+ * (request-id, tenant, authorization) or stays in-process (method-meta, the
9
+ * TestCaseRecorder) — is a `ContextKey`.
10
+ *
11
+ * The fields are named for what they DO (flipped from the old model):
12
+ * - `name` ALWAYS set. The context storage key, the log/MDC key, and the
13
+ * recorder name. e.g. 'requestId', 'tenantId', 'authorization'.
14
+ * - `httpHeader` OPTIONAL. When set, this key is transferred over the wire under
15
+ * this HTTP header name (inbound request -> context, and context ->
16
+ * outbound request). e.g. 'x-request-id'. When UNSET, the key is
17
+ * context-only and never leaves the process (method-meta, recorder).
18
+ * - `trust` REQUIRED, and stated by WHICH FACTORY you call. See below.
19
+ * - `maskInLogs` When true, the value is masked (partially) in logs. This is about
20
+ * LOG REDACTION and has NOTHING to do with `trust` — a userId is
21
+ * trusted AND fully logged; a bearer token is untrusted AND masked.
22
+ * (Formerly `isSecured`, renamed because sitting next to `trust` the
23
+ * old name read as "this value is secure", which it never meant.)
24
+ * - `isLogged` Defaults to true. When false, the value is NEVER logged (used for
25
+ * object-valued/internal keys like the recorder or method-meta that
26
+ * must not be serialized into log lines).
27
+ *
28
+ * ## TRUST — the whole point of this class
29
+ *
30
+ * A context value is either something the framework PROVED (`trusted`) or something a
31
+ * caller merely ASSERTED (`untrusted`). That distinction is invisible in a `Map<string,
32
+ * string>`, so code — human- or AI-written — routinely reads a spoofable header as if it
33
+ * were an authenticated fact. `userId` is the canonical example: it is a verified JWT
34
+ * claim on one route and an attacker-supplied `x-user-id` on the next, and nothing in the
35
+ * old API told them apart.
36
+ *
37
+ * So trust is declared ON THE KEY, at the single place the key is defined, and it is
38
+ * enforced at BOTH ends:
39
+ *
40
+ * ContextKey.trusted<string>('userId', 'jwt claim `sub`, stamped by AuthFilter', 'x-user-id')
41
+ * ContextKey.untrusted<string>('actionId', 'x-webpieces-actionid')
42
+ *
43
+ * `grep -rn "ContextKey.trusted"` therefore enumerates every high-assurance field in the
44
+ * codebase, each with its `provenance` string on the same line saying WHY it is trusted.
45
+ * That is the AI-facing payoff: the answer is one grep, not an audit.
46
+ *
47
+ * The constructor is PRIVATE — you cannot make a key without picking a branch, and there
48
+ * is no default. A default would make the permissive branch the shortest thing to type,
49
+ * which is exactly the "widening that is an ABSENCE rather than a token" that CLAUDE.md
50
+ * rejects. `provenance` is a REQUIRED positional argument on the trusted factory only, so
51
+ * "trusted with no stated reason" cannot be written down.
52
+ *
53
+ * ## What makes the `trusted` label HONEST at runtime
54
+ *
55
+ * The label would be a lie if anything could write a trusted key from an unverified
56
+ * source. Three enforced facts prevent that:
57
+ *
58
+ * 1. WRITES are typed: only `RequestContext.putTrusted(key, value)` accepts a trusted key,
59
+ * and it is a distinct, greppable verb an app has to type on purpose.
60
+ * 2. INBOUND wire values for trusted keys never enter the context directly.
61
+ * `RequestContextHeaders.fillFromRequest` stashes them as PENDING, and `AuthFilter`
62
+ * admits them only after it knows who the caller is (see {@link PendingWireTrust}).
63
+ * 3. READS are typed: `getTrusted(key)` does not compile for an untrusted key, and
64
+ * `getUntrusted(key)` does not compile for a trusted one. Picking a verb is
65
+ * unavoidable, so a reader always knows which kind of value it is holding.
66
+ *
67
+ * Trusted keys DO keep their `httpHeader` — service-to-service propagation of a verified
68
+ * userId is a first-class requirement, not a hole. It is safe because rule 2 gates it on
69
+ * the endpoint's own auth mode: a route that verified WHO called it (`@AuthOidc`,
70
+ * `@AuthSharedSecret`) accepts the caller's trusted headers; a route reachable by a
71
+ * browser (`@AuthJwt`, public) does not.
72
+ *
73
+ * Per CLAUDE.md: data-only structures are classes, not interfaces.
74
+ *
75
+ * The type parameter `V` is the TYPE OF THE VALUE stored under this key — `string` for the wire/log
76
+ * keys (requestId, tenantId, ...), `ApiCallInfo` for the structured api tag, `TestCaseRecorder` for
77
+ * the recorder. It is REQUIRED (no default): every key must state what it holds. A heterogeneous
78
+ * store CANNOT be a `Record<string, string>` — the recorder and the api payload are not strings — so
79
+ * instead each KEY carries its own value type, and the typed accessors INFER it from the key. That
80
+ * keeps the backing Map honestly type-erased while the public surface stays fully typed: a caller
81
+ * never asserts a value type, the key already declares it. A genuinely mixed collection of keys is
82
+ * spelled explicitly as `AnyContextKey[]`, so "I mean a mixed bag" is a visible, deliberate
83
+ * statement, never a default.
84
+ *
85
+ * The type parameter `T` is the TRUST LEVEL, carried as a phantom type so the accessor verbs can
86
+ * reject the wrong kind of key at COMPILE time rather than throwing at runtime (CLAUDE.md treats a
87
+ * runtime throw standing in for an expressible type as a defect).
88
+ */
2
89
  Object.defineProperty(exports, "__esModule", { value: true });
3
90
  exports.ContextKey = void 0;
4
91
  class ContextKey {
@@ -9,29 +96,78 @@ class ContextKey {
9
96
  * 'x-request-id'). Undefined = context-only, never transferred.
10
97
  */
11
98
  httpHeader;
12
- /** Mask this value (partially) in logs. */
13
- isSecured;
99
+ /** The runtime twin of the phantom {@link __trust}. See the class doc. */
100
+ trust;
101
+ /**
102
+ * WHY this key is trusted, in prose — 'jwt claim `sub`, stamped by AuthFilter', or
103
+ * 'whatsapp webhook phone number -> user lookup'. Required on a trusted key, absent on an
104
+ * untrusted one. It exists so that grepping the trusted keys also tells you what proves each
105
+ * one, without opening another file.
106
+ */
107
+ provenance;
108
+ /** Mask this value (partially) in logs. Log redaction only — unrelated to {@link trust}. */
109
+ maskInLogs;
14
110
  /** Whether this key is logged at all. Default true; false = never logged. */
15
111
  isLogged;
16
- constructor(name, httpHeader, isSecured = false, isLogged = true) {
112
+ /**
113
+ * PRIVATE — use {@link trusted} or {@link untrusted}. There is deliberately no way to build a key
114
+ * without stating its trust level: a defaulted trust argument would make the permissive branch
115
+ * the shortest thing to type and impossible to grep.
116
+ */
117
+ constructor(name, trust, provenance, httpHeader, maskInLogs, isLogged) {
17
118
  this.name = name;
119
+ this.trust = trust;
120
+ this.provenance = provenance;
18
121
  this.httpHeader = httpHeader;
19
- this.isSecured = isSecured;
122
+ this.maskInLogs = maskInLogs;
20
123
  this.isLogged = isLogged;
21
124
  }
125
+ /**
126
+ * A key whose value the framework PROVED — a verified JWT claim, or a fact an app derived from a
127
+ * verified credential (a Twilio/WhatsApp webhook's signed phone number looked up to a userId).
128
+ *
129
+ * Only `RequestContext.putTrusted` can write one, only `RequestContext.getTrusted` can read one,
130
+ * and an inbound wire value for one is held PENDING until `AuthFilter` knows who the caller is.
131
+ *
132
+ * @param provenance WHY it is trusted, in prose. Required — see {@link ContextKey.provenance}.
133
+ */
134
+ // 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
135
+ static trusted(name, provenance, httpHeader, maskInLogs = false, isLogged = true) {
136
+ return new ContextKey(name, 'trusted', provenance, httpHeader, maskInLogs, isLogged);
137
+ }
138
+ /**
139
+ * A key whose value is merely ASSERTED by whoever sent it — a browser-minted actionId, a
140
+ * client-supplied recording flag, an in-process log tag. Perfectly fine to use; just never
141
+ * an input to an authorization decision.
142
+ *
143
+ * This is the DEFAULT choice in the sense that most keys are this — but it is never the default
144
+ * VALUE: you still type the word, so reading a key definition always tells you which it is.
145
+ */
146
+ // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor; see trusted()
147
+ static untrusted(name, httpHeader, maskInLogs = false, isLogged = true) {
148
+ return new ContextKey(name, 'untrusted', undefined, httpHeader, maskInLogs, isLogged);
149
+ }
22
150
  /** True when this key is transferred over HTTP (has an httpHeader). */
23
151
  isTransferred() {
24
152
  return this.httpHeader !== undefined;
25
153
  }
154
+ /**
155
+ * True for a key built by {@link trusted}. The RUNTIME check used by the inbound fill and the
156
+ * AuthFilter reconciliation; ordinary application code should never need it, because the typed
157
+ * verbs already made the decision at compile time.
158
+ */
159
+ isTrusted() {
160
+ return this.trust === 'trusted';
161
+ }
26
162
  /**
27
163
  * The value as it should appear in a log line: returned as-is for a normal
28
- * key, partially masked when this key is secured. Masking is length-based:
164
+ * key, partially masked when this key sets `maskInLogs`. Masking is length-based:
29
165
  * - Length > 15: first 3 + "..." + last 3
30
166
  * - Length 8-15: first 2 + "..."
31
167
  * - Length < 8: "<secure key too short to log>"
32
168
  */
33
- maskIfSecured(value) {
34
- if (!this.isSecured) {
169
+ maskForLogs(value) {
170
+ if (!this.maskInLogs) {
35
171
  return value;
36
172
  }
37
173
  const len = value.length;
@@ -1 +1 @@
1
- {"version":3,"file":"ContextKey.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKey.ts"],"names":[],"mappings":";;;AA6CA,MAAa,UAAU;IASnB,qEAAqE;IAC5D,IAAI,CAAS;IAEtB;;;OAGG;IACM,UAAU,CAAU;IAE7B,2CAA2C;IAClC,SAAS,CAAU;IAE5B,6EAA6E;IACpE,QAAQ,CAAU;IAE3B,YACI,IAAY,EACZ,UAAmB,EACnB,SAAS,GAAG,KAAK,EACjB,QAAQ,GAAG,IAAI;QAEf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;IAED,uEAAuE;IACvE,aAAa;QACT,OAAO,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC;IACzC,CAAC;IAED;;;;;;OAMG;IACH,aAAa,CAAC,KAAa;QACvB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAClB,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;AA7DD,gCA6DC","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 * - `isSecured` When true, the value is masked (partially) in logs.\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 * 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, so a legacy\n * `new ContextKey('x')` fails to compile until it declares `new ContextKey<string>('x')` — the\n * type system does the migration for you. A heterogeneous store CANNOT be a `Record<string, string>`\n * — the recorder and the api payload are not strings — so instead each KEY carries its own value\n * type, and `RequestContext.getHeader/putHeader` INFER it from the key. That keeps the backing Map\n * honestly type-erased while the public surface stays fully typed: a caller never asserts a value\n * type, the key already declares it. A genuinely mixed collection of keys is spelled explicitly as\n * `AnyContextKey[]`, so \"I mean a mixed bag\" is a visible, deliberate statement, never a default.\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// 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\nexport class ContextKey<V> {\n /**\n * Phantom marker carrying the value type {@link V}. It has no runtime existence (`declare`, never\n * assigned) — it exists ONLY so `getHeader(key)` returns `V` and `putHeader(key, value)` checks\n * `value` against `V`, both inferred straight from the key. Optional, so `ContextKey<A>` stays\n * assignable to `AnyContextKey` (i.e. `ContextKey<unknown>`) — arrays of mixed keys keep working.\n */\n declare readonly __valueType?: V;\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 /** Mask this value (partially) in logs. */\n readonly isSecured: boolean;\n\n /** Whether this key is logged at all. Default true; false = never logged. */\n readonly isLogged: boolean;\n\n constructor(\n name: string,\n httpHeader?: string,\n isSecured = false,\n isLogged = true,\n ) {\n this.name = name;\n this.httpHeader = httpHeader;\n this.isSecured = isSecured;\n this.isLogged = 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 * The value as it should appear in a log line: returned as-is for a normal\n * key, partially masked when this key is secured. 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 maskIfSecured(value: string): string {\n if (!this.isSecured) {\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;;;;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"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * COMPILE-TIME assertions that the trust system cannot be bypassed by writing the wrong thing.
3
+ *
4
+ * Every `@ts-expect-error` below FAILS THE BUILD (TS2578, "unused '@ts-expect-error' directive") the
5
+ * day its line starts compiling — so this file is a tripwire, not documentation that can rot.
6
+ *
7
+ * It lives in COMPILED source, never in a `.spec.ts`, and that placement is load-bearing:
8
+ * `tsconfig.lib.json` excludes specs and vitest strips types with esbuild, so a `@ts-expect-error`
9
+ * in a spec is inert and the suite would pass either way. See the same pattern in
10
+ * `AuthJwtCompileAssertions.ts`.
11
+ *
12
+ * The verb-level assertions (`getTrusted` refusing an untrusted key, and vice versa) live in
13
+ * core-context beside `RequestContext`, which is where those verbs are declared.
14
+ */
15
+ export declare class ContextKeyTrustCompileAssertions {
16
+ /** A trusted key MUST state its provenance — "trusted because reasons unstated" is unwritable. */
17
+ trustedRequiresProvenance(): void;
18
+ /**
19
+ * There is NO public constructor. A key cannot exist without picking a trust branch, so trust can
20
+ * never be defaulted, forgotten, or silently widened by an omitted argument.
21
+ */
22
+ noConstructorEscapeHatch(): void;
23
+ /**
24
+ * The two branches are NOT interchangeable types. This is what makes `getTrusted(SOME_UNTRUSTED)`
25
+ * a compile error rather than a runtime throw — assigning one to the other must not typecheck.
26
+ */
27
+ branchesAreNotInterchangeable(): void;
28
+ /**
29
+ * BOTH branches must still flow into the mixed-bag alias, or the registry's key arrays
30
+ * (`ALL_HEADERS`, `getLoggedKeys()`, `getTransferredKeys()`) would stop compiling. This one is a
31
+ * POSITIVE assertion: it must keep working.
32
+ */
33
+ bothBranchesAreAnyContextKey(): void;
34
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContextKeyTrustCompileAssertions = void 0;
4
+ const ContextKey_1 = require("./ContextKey");
5
+ /**
6
+ * COMPILE-TIME assertions that the trust system cannot be bypassed by writing the wrong thing.
7
+ *
8
+ * Every `@ts-expect-error` below FAILS THE BUILD (TS2578, "unused '@ts-expect-error' directive") the
9
+ * day its line starts compiling — so this file is a tripwire, not documentation that can rot.
10
+ *
11
+ * It lives in COMPILED source, never in a `.spec.ts`, and that placement is load-bearing:
12
+ * `tsconfig.lib.json` excludes specs and vitest strips types with esbuild, so a `@ts-expect-error`
13
+ * in a spec is inert and the suite would pass either way. See the same pattern in
14
+ * `AuthJwtCompileAssertions.ts`.
15
+ *
16
+ * The verb-level assertions (`getTrusted` refusing an untrusted key, and vice versa) live in
17
+ * core-context beside `RequestContext`, which is where those verbs are declared.
18
+ */
19
+ class ContextKeyTrustCompileAssertions {
20
+ /** A trusted key MUST state its provenance — "trusted because reasons unstated" is unwritable. */
21
+ trustedRequiresProvenance() {
22
+ // @ts-expect-error - provenance is required on ContextKey.trusted
23
+ ContextKey_1.ContextKey.trusted('userId');
24
+ }
25
+ /**
26
+ * There is NO public constructor. A key cannot exist without picking a trust branch, so trust can
27
+ * never be defaulted, forgotten, or silently widened by an omitted argument.
28
+ */
29
+ noConstructorEscapeHatch() {
30
+ // @ts-expect-error - the constructor is private; use ContextKey.trusted / ContextKey.untrusted
31
+ new ContextKey_1.ContextKey('userId', 'trusted', undefined, 'x-user-id', false, true);
32
+ }
33
+ /**
34
+ * The two branches are NOT interchangeable types. This is what makes `getTrusted(SOME_UNTRUSTED)`
35
+ * a compile error rather than a runtime throw — assigning one to the other must not typecheck.
36
+ */
37
+ branchesAreNotInterchangeable() {
38
+ const untrusted = ContextKey_1.ContextKey.untrusted('actionId');
39
+ // @ts-expect-error - an untrusted key is not a trusted key
40
+ const asTrusted = untrusted;
41
+ void asTrusted;
42
+ const trusted = ContextKey_1.ContextKey.trusted('userId', 'jwt claim');
43
+ // @ts-expect-error - a trusted key is not an untrusted key
44
+ const asUntrusted = trusted;
45
+ void asUntrusted;
46
+ }
47
+ /**
48
+ * BOTH branches must still flow into the mixed-bag alias, or the registry's key arrays
49
+ * (`ALL_HEADERS`, `getLoggedKeys()`, `getTransferredKeys()`) would stop compiling. This one is a
50
+ * POSITIVE assertion: it must keep working.
51
+ */
52
+ bothBranchesAreAnyContextKey() {
53
+ const keys = [
54
+ ContextKey_1.ContextKey.trusted('userId', 'jwt claim `sub`', 'x-user-id'),
55
+ ContextKey_1.ContextKey.untrusted('actionId', 'x-webpieces-actionid'),
56
+ ];
57
+ void keys;
58
+ }
59
+ }
60
+ exports.ContextKeyTrustCompileAssertions = ContextKeyTrustCompileAssertions;
61
+ //# sourceMappingURL=ContextKeyTrustCompileAssertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContextKeyTrustCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKeyTrustCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,6CAAyD;AAEzD;;;;;;;;;;;;;GAaG;AACH,MAAa,gCAAgC;IACzC,kGAAkG;IAClG,yBAAyB;QACrB,kEAAkE;QAClE,uBAAU,CAAC,OAAO,CAAS,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED;;;OAGG;IACH,wBAAwB;QACpB,+FAA+F;QAC/F,IAAI,uBAAU,CAAS,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IACrF,CAAC;IAED;;;OAGG;IACH,6BAA6B;QACzB,MAAM,SAAS,GAAoC,uBAAU,CAAC,SAAS,CAAS,UAAU,CAAC,CAAC;QAC5F,2DAA2D;QAC3D,MAAM,SAAS,GAAkC,SAAS,CAAC;QAC3D,KAAK,SAAS,CAAC;QAEf,MAAM,OAAO,GAAkC,uBAAU,CAAC,OAAO,CAAS,QAAQ,EAAE,WAAW,CAAC,CAAC;QACjG,2DAA2D;QAC3D,MAAM,WAAW,GAAoC,OAAO,CAAC;QAC7D,KAAK,WAAW,CAAC;IACrB,CAAC;IAED;;;;OAIG;IACH,4BAA4B;QACxB,MAAM,IAAI,GAAoB;YAC1B,uBAAU,CAAC,OAAO,CAAS,QAAQ,EAAE,iBAAiB,EAAE,WAAW,CAAC;YACpE,uBAAU,CAAC,SAAS,CAAS,UAAU,EAAE,sBAAsB,CAAC;SACnE,CAAC;QACF,KAAK,IAAI,CAAC;IACd,CAAC;CACJ;AA5CD,4EA4CC","sourcesContent":["import { ContextKey, AnyContextKey } from './ContextKey';\n\n/**\n * COMPILE-TIME assertions that the trust system cannot be bypassed by writing the wrong thing.\n *\n * Every `@ts-expect-error` below FAILS THE BUILD (TS2578, \"unused '@ts-expect-error' directive\") the\n * day its line starts compiling — so this file is a tripwire, not documentation that can rot.\n *\n * It lives in COMPILED source, never in a `.spec.ts`, and that placement is load-bearing:\n * `tsconfig.lib.json` excludes specs and vitest strips types with esbuild, so a `@ts-expect-error`\n * in a spec is inert and the suite would pass either way. See the same pattern in\n * `AuthJwtCompileAssertions.ts`.\n *\n * The verb-level assertions (`getTrusted` refusing an untrusted key, and vice versa) live in\n * core-context beside `RequestContext`, which is where those verbs are declared.\n */\nexport class ContextKeyTrustCompileAssertions {\n /** A trusted key MUST state its provenance — \"trusted because reasons unstated\" is unwritable. */\n trustedRequiresProvenance(): void {\n // @ts-expect-error - provenance is required on ContextKey.trusted\n ContextKey.trusted<string>('userId');\n }\n\n /**\n * There is NO public constructor. A key cannot exist without picking a trust branch, so trust can\n * never be defaulted, forgotten, or silently widened by an omitted argument.\n */\n noConstructorEscapeHatch(): void {\n // @ts-expect-error - the constructor is private; use ContextKey.trusted / ContextKey.untrusted\n new ContextKey<string>('userId', 'trusted', undefined, 'x-user-id', false, true);\n }\n\n /**\n * The two branches are NOT interchangeable types. This is what makes `getTrusted(SOME_UNTRUSTED)`\n * a compile error rather than a runtime throw — assigning one to the other must not typecheck.\n */\n branchesAreNotInterchangeable(): void {\n const untrusted: ContextKey<string, 'untrusted'> = ContextKey.untrusted<string>('actionId');\n // @ts-expect-error - an untrusted key is not a trusted key\n const asTrusted: ContextKey<string, 'trusted'> = untrusted;\n void asTrusted;\n\n const trusted: ContextKey<string, 'trusted'> = ContextKey.trusted<string>('userId', 'jwt claim');\n // @ts-expect-error - a trusted key is not an untrusted key\n const asUntrusted: ContextKey<string, 'untrusted'> = trusted;\n void asUntrusted;\n }\n\n /**\n * BOTH branches must still flow into the mixed-bag alias, or the registry's key arrays\n * (`ALL_HEADERS`, `getLoggedKeys()`, `getTransferredKeys()`) would stop compiling. This one is a\n * POSITIVE assertion: it must keep working.\n */\n bothBranchesAreAnyContextKey(): void {\n const keys: AnyContextKey[] = [\n ContextKey.trusted<string>('userId', 'jwt claim `sub`', 'x-user-id'),\n ContextKey.untrusted<string>('actionId', 'x-webpieces-actionid'),\n ];\n void keys;\n }\n}\n"]}
@@ -1,12 +1,17 @@
1
- import { AnyContextKey } from './ContextKey';
1
+ import { AnyTrustedContextKey } from './ContextKey';
2
2
  /**
3
3
  * ContextTuple - one (ContextKey, value) pair to be stamped into the request's
4
4
  * "magic context" (RequestContext on the server) — e.g. USER_ID, ORG_ID. The JWT
5
- * parse plugin returns these so the framework can set them via RequestContext.putHeader.
5
+ * parse plugin returns these so the framework can set them via `RequestContext.putTrusted`.
6
6
  * Data-only structure (a class, per the guidelines).
7
+ *
8
+ * The key is deliberately an {@link AnyTrustedContextKey}, not any old key: everything in here was
9
+ * derived from a VERIFIED credential, so stamping an untrusted key from a JWT parse is a category
10
+ * error and does not compile. This is also what lets `AuthFilter` treat "the authenticator claimed
11
+ * this key" as the definition of trusted — see its reconciliation of pending wire values.
7
12
  */
8
13
  export declare class ContextTuple {
9
- readonly key: AnyContextKey;
14
+ readonly key: AnyTrustedContextKey;
10
15
  readonly value: unknown;
11
- constructor(key: AnyContextKey, value: unknown);
16
+ constructor(key: AnyTrustedContextKey, value: unknown);
12
17
  }
@@ -4,8 +4,13 @@ exports.ContextTuple = void 0;
4
4
  /**
5
5
  * ContextTuple - one (ContextKey, value) pair to be stamped into the request's
6
6
  * "magic context" (RequestContext on the server) — e.g. USER_ID, ORG_ID. The JWT
7
- * parse plugin returns these so the framework can set them via RequestContext.putHeader.
7
+ * parse plugin returns these so the framework can set them via `RequestContext.putTrusted`.
8
8
  * Data-only structure (a class, per the guidelines).
9
+ *
10
+ * The key is deliberately an {@link AnyTrustedContextKey}, not any old key: everything in here was
11
+ * derived from a VERIFIED credential, so stamping an untrusted key from a JWT parse is a category
12
+ * error and does not compile. This is also what lets `AuthFilter` treat "the authenticator claimed
13
+ * this key" as the definition of trusted — see its reconciliation of pending wire values.
9
14
  */
10
15
  class ContextTuple {
11
16
  key;
@@ -1 +1 @@
1
- {"version":3,"file":"ContextTuple.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextTuple.ts"],"names":[],"mappings":";;;AAEA;;;;;GAKG;AACH,MAAa,YAAY;IAED;IAEA;IAHpB,YACoB,GAAkB;IAClC,oFAAoF;IACpE,KAAc;QAFd,QAAG,GAAH,GAAG,CAAe;QAElB,UAAK,GAAL,KAAK,CAAS;IAC/B,CAAC;CACP;AAND,oCAMC","sourcesContent":["import { ContextKey, AnyContextKey } from './ContextKey';\n\n/**\n * ContextTuple - one (ContextKey, value) pair to be stamped into the request's\n * \"magic context\" (RequestContext on the server) — e.g. USER_ID, ORG_ID. The JWT\n * parse plugin returns these so the framework can set them via RequestContext.putHeader.\n * Data-only structure (a class, per the guidelines).\n */\nexport class ContextTuple {\n constructor(\n public readonly key: AnyContextKey,\n // webpieces-disable no-any-unknown -- context values are arbitrary app-defined data\n public readonly value: unknown,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"ContextTuple.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextTuple.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;GAUG;AACH,MAAa,YAAY;IAED;IAEA;IAHpB,YACoB,GAAyB;IACzC,oFAAoF;IACpE,KAAc;QAFd,QAAG,GAAH,GAAG,CAAsB;QAEzB,UAAK,GAAL,KAAK,CAAS;IAC/B,CAAC;CACP;AAND,oCAMC","sourcesContent":["import { AnyTrustedContextKey } from './ContextKey';\n\n/**\n * ContextTuple - one (ContextKey, value) pair to be stamped into the request's\n * \"magic context\" (RequestContext on the server) — e.g. USER_ID, ORG_ID. The JWT\n * parse plugin returns these so the framework can set them via `RequestContext.putTrusted`.\n * Data-only structure (a class, per the guidelines).\n *\n * The key is deliberately an {@link AnyTrustedContextKey}, not any old key: everything in here was\n * derived from a VERIFIED credential, so stamping an untrusted key from a JWT parse is a category\n * error and does not compile. This is also what lets `AuthFilter` treat \"the authenticator claimed\n * this key\" as the definition of trusted — see its reconciliation of pending wire values.\n */\nexport class ContextTuple {\n constructor(\n public readonly key: AnyTrustedContextKey,\n // webpieces-disable no-any-unknown -- context values are arbitrary app-defined data\n public readonly value: unknown,\n ) {}\n}\n"]}