@webpieces/core-util 0.4.644 → 0.4.646
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 +1 -1
- package/src/ContextKey.d.ts +34 -34
- package/src/ContextKey.js +14 -24
- package/src/ContextKey.js.map +1 -1
- package/src/ContextKeyTrustCompileAssertions.d.ts +13 -0
- package/src/ContextKeyTrustCompileAssertions.js +23 -0
- package/src/ContextKeyTrustCompileAssertions.js.map +1 -1
package/package.json
CHANGED
package/src/ContextKey.d.ts
CHANGED
|
@@ -87,19 +87,6 @@
|
|
|
87
87
|
*/
|
|
88
88
|
/** The two kinds of context value. See the {@link ContextKey} class doc. */
|
|
89
89
|
export type Trust = 'trusted' | 'untrusted';
|
|
90
|
-
/**
|
|
91
|
-
* A ContextKey whose value type is intentionally UNCONSTRAINED — a "key of any value type". Use this
|
|
92
|
-
* (never a bare `ContextKey`, which no longer compiles) for genuinely mixed-bag collections and
|
|
93
|
-
* key-agnostic code: `ALL_HEADERS: AnyContextKey[]`, the {@link HeaderRegistry}'s key arrays, a
|
|
94
|
-
* reader that takes whatever key it is handed. Naming the mixed case makes "I mean any key" a visible,
|
|
95
|
-
* deliberate statement, and confines the one sanctioned `unknown` to this single alias instead of
|
|
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.
|
|
101
|
-
*/
|
|
102
|
-
export type AnyContextKey = ContextKey<unknown>;
|
|
103
90
|
/**
|
|
104
91
|
* A trusted key of any value type — what {@link ContextTuple} carries, and what the trusted write
|
|
105
92
|
* verb accepts when the value type is not statically known.
|
|
@@ -110,6 +97,27 @@ export type AnyTrustedContextKey = ContextKey<unknown, 'trusted'>;
|
|
|
110
97
|
* cannot be used as a side door to forge a trusted value.
|
|
111
98
|
*/
|
|
112
99
|
export type AnyUntrustedContextKey = ContextKey<unknown, 'untrusted'>;
|
|
100
|
+
/**
|
|
101
|
+
* A ContextKey whose value type is intentionally UNCONSTRAINED — a "key of any value type". Use this
|
|
102
|
+
* (never a bare `ContextKey`, which no longer compiles) for genuinely mixed-bag collections and
|
|
103
|
+
* key-agnostic code: `ALL_HEADERS: AnyContextKey[]`, the {@link HeaderRegistry}'s key arrays, a
|
|
104
|
+
* reader that takes whatever key it is handed. Naming the mixed case makes "I mean any key" a visible,
|
|
105
|
+
* deliberate statement.
|
|
106
|
+
*
|
|
107
|
+
* It is a UNION of the two branches, not `ContextKey<unknown, Trust>`, and that is load-bearing rather
|
|
108
|
+
* than cosmetic. Trust is BINARY, so `if (key.isTrusted())` should type BOTH of its branches — and it
|
|
109
|
+
* does only against a union: TypeScript narrows the negative of a `this is X` predicate by dropping the
|
|
110
|
+
* union constituents assignable to `X`, so the `else` here lands on {@link AnyUntrustedContextKey} and
|
|
111
|
+
* goes straight to `putUntrusted` with no cast. Written as one type with a mixed `Trust` parameter
|
|
112
|
+
* there would be nothing to drop, the `else` would stay mixed, and the class would need a second
|
|
113
|
+
* `isUntrusted()` predicate to type the branch its own negative already decided — one runtime question
|
|
114
|
+
* with two spellings, which is the shim shape CLAUDE.md rejects.
|
|
115
|
+
*
|
|
116
|
+
* Mixed in TRUST as well as in value type, so it is READ-ONLY territory: `getAny(key)` takes one, but
|
|
117
|
+
* no WRITE verb does. A write must name the trust level, which is what keeps the `trusted` label
|
|
118
|
+
* honest.
|
|
119
|
+
*/
|
|
120
|
+
export type AnyContextKey = AnyTrustedContextKey | AnyUntrustedContextKey;
|
|
113
121
|
export declare class ContextKey<V, T extends Trust = Trust> {
|
|
114
122
|
/**
|
|
115
123
|
* Phantom marker carrying the value type {@link V}. It has no runtime existence (`declare`, never
|
|
@@ -172,35 +180,27 @@ export declare class ContextKey<V, T extends Trust = Trust> {
|
|
|
172
180
|
/** True when this key is transferred over HTTP (has an httpHeader). */
|
|
173
181
|
isTransferred(): boolean;
|
|
174
182
|
/**
|
|
175
|
-
* True for a key built by {@link trusted}.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
183
|
+
* True for a key built by {@link trusted}. Trust is BINARY, so this ONE predicate answers it in
|
|
184
|
+
* both directions and there is deliberately no `isUntrusted()` twin. The RUNTIME check used by the
|
|
185
|
+
* inbound fill and the AuthFilter reconciliation; ordinary application code should never need it,
|
|
186
|
+
* because the typed verbs already made the decision at compile time.
|
|
178
187
|
*
|
|
179
|
-
* It is a TYPE PREDICATE, so
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
* It is a TYPE PREDICATE, so BOTH branches are typed, with NO cast on either side: the `if` holds a
|
|
189
|
+
* `ContextKey<V, 'trusted'>` for `putTrusted` / `PendingWireTrust.stash`, and the `else` holds a
|
|
190
|
+
* `ContextKey<V, 'untrusted'>` for `putUntrusted`. The `else` types only because
|
|
191
|
+
* {@link AnyContextKey} is a UNION of the two branches — see that alias for why the negative of a
|
|
192
|
+
* predicate needs something to drop. Before this, every such site wrote `key as
|
|
193
|
+
* AnyTrustedContextKey`; a cast is exactly the thing an agent copies to the one place it is not
|
|
194
|
+
* warranted, so the runtime check produces the type it proves instead.
|
|
188
195
|
*
|
|
189
|
-
*
|
|
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
|
|
196
|
+
* That is what makes a loop over a mixed `AnyContextKey[]` — the {@link HeaderRegistry} arrays, a
|
|
197
197
|
* browser-log payload re-stated into a detached scope — safe BY CONSTRUCTION: the loop can only
|
|
198
198
|
* write a key whose trust it has just tested, and a trusted key cannot reach `putUntrusted` at all,
|
|
199
199
|
* so a loop fed by a source that proves nothing cannot fabricate a proven value, and nobody has to
|
|
200
200
|
* remember to filter. That is a limit on the SOURCE, not on the key: a trusted key is written all
|
|
201
201
|
* the time via `putTrusted`, by an authenticator or by app code that proved the value out of band.
|
|
202
202
|
*/
|
|
203
|
-
|
|
203
|
+
isTrusted(): this is ContextKey<V, 'trusted'>;
|
|
204
204
|
/**
|
|
205
205
|
* The value as it should appear in a log line: returned as-is for a normal
|
|
206
206
|
* key, partially masked when this key sets `maskInLogs`. Masking is length-based:
|
package/src/ContextKey.js
CHANGED
|
@@ -152,38 +152,28 @@ class ContextKey {
|
|
|
152
152
|
return this.httpHeader !== undefined;
|
|
153
153
|
}
|
|
154
154
|
/**
|
|
155
|
-
* True for a key built by {@link trusted}.
|
|
156
|
-
*
|
|
157
|
-
*
|
|
155
|
+
* True for a key built by {@link trusted}. Trust is BINARY, so this ONE predicate answers it in
|
|
156
|
+
* both directions and there is deliberately no `isUntrusted()` twin. The RUNTIME check used by the
|
|
157
|
+
* inbound fill and the AuthFilter reconciliation; ordinary application code should never need it,
|
|
158
|
+
* because the typed verbs already made the decision at compile time.
|
|
158
159
|
*
|
|
159
|
-
* It is a TYPE PREDICATE, so
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
160
|
+
* It is a TYPE PREDICATE, so BOTH branches are typed, with NO cast on either side: the `if` holds a
|
|
161
|
+
* `ContextKey<V, 'trusted'>` for `putTrusted` / `PendingWireTrust.stash`, and the `else` holds a
|
|
162
|
+
* `ContextKey<V, 'untrusted'>` for `putUntrusted`. The `else` types only because
|
|
163
|
+
* {@link AnyContextKey} is a UNION of the two branches — see that alias for why the negative of a
|
|
164
|
+
* predicate needs something to drop. Before this, every such site wrote `key as
|
|
165
|
+
* AnyTrustedContextKey`; a cast is exactly the thing an agent copies to the one place it is not
|
|
166
|
+
* warranted, so the runtime check produces the type it proves instead.
|
|
177
167
|
*
|
|
178
|
-
*
|
|
168
|
+
* That is what makes a loop over a mixed `AnyContextKey[]` — the {@link HeaderRegistry} arrays, a
|
|
179
169
|
* browser-log payload re-stated into a detached scope — safe BY CONSTRUCTION: the loop can only
|
|
180
170
|
* write a key whose trust it has just tested, and a trusted key cannot reach `putUntrusted` at all,
|
|
181
171
|
* so a loop fed by a source that proves nothing cannot fabricate a proven value, and nobody has to
|
|
182
172
|
* remember to filter. That is a limit on the SOURCE, not on the key: a trusted key is written all
|
|
183
173
|
* the time via `putTrusted`, by an authenticator or by app code that proved the value out of band.
|
|
184
174
|
*/
|
|
185
|
-
|
|
186
|
-
return this.trust === '
|
|
175
|
+
isTrusted() {
|
|
176
|
+
return this.trust === 'trusted';
|
|
187
177
|
}
|
|
188
178
|
/**
|
|
189
179
|
* The value as it should appear in a log line: returned as-is for a normal
|
package/src/ContextKey.js.map
CHANGED
|
@@ -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;;;;;;;;;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"]}
|
|
1
|
+
{"version":3,"file":"ContextKey.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKey.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;;;AAyCH,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;;;;;;;;;;;;;;;;;;;;OAoBG;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;AAvJD,gCAuJC","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 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 -- the ONE sanctioned `unknown`: a key whose value type is deliberately unconstrained (mixed-bag collections / key-agnostic code). Every other site names one of these aliases instead of repeating it.\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, narrowed to the untrusted branch\nexport type AnyUntrustedContextKey = ContextKey<unknown, '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.\n *\n * It is a UNION of the two branches, not `ContextKey<unknown, Trust>`, and that is load-bearing rather\n * than cosmetic. Trust is BINARY, so `if (key.isTrusted())` should type BOTH of its branches — and it\n * does only against a union: TypeScript narrows the negative of a `this is X` predicate by dropping the\n * union constituents assignable to `X`, so the `else` here lands on {@link AnyUntrustedContextKey} and\n * goes straight to `putUntrusted` with no cast. Written as one type with a mixed `Trust` parameter\n * there would be nothing to drop, the `else` would stay mixed, and the class would need a second\n * `isUntrusted()` predicate to type the branch its own negative already decided — one runtime question\n * with two spellings, which is the shim shape CLAUDE.md rejects.\n *\n * Mixed in TRUST as well as in value type, so it is READ-ONLY territory: `getAny(key)` takes one, but\n * no WRITE verb does. A write must name the trust level, which is what keeps the `trusted` label\n * honest.\n */\nexport type AnyContextKey = AnyTrustedContextKey | AnyUntrustedContextKey;\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}. Trust is BINARY, so this ONE predicate answers it in\n * both directions and there is deliberately no `isUntrusted()` twin. The RUNTIME check used by the\n * inbound fill and the AuthFilter reconciliation; ordinary application code should never need it,\n * because the typed verbs already made the decision at compile time.\n *\n * It is a TYPE PREDICATE, so BOTH branches are typed, with NO cast on either side: the `if` holds a\n * `ContextKey<V, 'trusted'>` for `putTrusted` / `PendingWireTrust.stash`, and the `else` holds a\n * `ContextKey<V, 'untrusted'>` for `putUntrusted`. The `else` types only because\n * {@link AnyContextKey} is a UNION of the two branches — see that alias for why the negative of a\n * predicate needs something to drop. Before this, every such site wrote `key as\n * AnyTrustedContextKey`; a cast is exactly the thing an agent copies to the one place it is not\n * warranted, so the runtime check produces the type it proves instead.\n *\n * That 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 isTrusted(): this is ContextKey<V, 'trusted'> {\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,3 +1,4 @@
|
|
|
1
|
+
import { AnyContextKey } from './ContextKey';
|
|
1
2
|
/**
|
|
2
3
|
* COMPILE-TIME assertions that the trust system cannot be bypassed by writing the wrong thing.
|
|
3
4
|
*
|
|
@@ -31,4 +32,16 @@ export declare class ContextKeyTrustCompileAssertions {
|
|
|
31
32
|
* POSITIVE assertion: it must keep working.
|
|
32
33
|
*/
|
|
33
34
|
bothBranchesAreAnyContextKey(): void;
|
|
35
|
+
/**
|
|
36
|
+
* THE assertion that keeps trust a ONE-METHOD question: `isTrusted()` types BOTH of its branches,
|
|
37
|
+
* so the `else` needs no `isUntrusted()` twin and no cast.
|
|
38
|
+
*
|
|
39
|
+
* POSITIVE, and a tripwire on the SHAPE of {@link AnyContextKey}: this compiles only while that
|
|
40
|
+
* alias is a UNION of the two branches. Respell it as one type with a mixed `Trust` parameter and
|
|
41
|
+
* the negative has nothing to drop, `takesUntrusted(key)` below stops compiling, and the pressure
|
|
42
|
+
* to re-add a second predicate for a binary question comes straight back.
|
|
43
|
+
*/
|
|
44
|
+
isTrustedTypesTheElseBranchToo(key: AnyContextKey): void;
|
|
45
|
+
private takesTrusted;
|
|
46
|
+
private takesUntrusted;
|
|
34
47
|
}
|
|
@@ -56,6 +56,29 @@ class ContextKeyTrustCompileAssertions {
|
|
|
56
56
|
];
|
|
57
57
|
void keys;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* THE assertion that keeps trust a ONE-METHOD question: `isTrusted()` types BOTH of its branches,
|
|
61
|
+
* so the `else` needs no `isUntrusted()` twin and no cast.
|
|
62
|
+
*
|
|
63
|
+
* POSITIVE, and a tripwire on the SHAPE of {@link AnyContextKey}: this compiles only while that
|
|
64
|
+
* alias is a UNION of the two branches. Respell it as one type with a mixed `Trust` parameter and
|
|
65
|
+
* the negative has nothing to drop, `takesUntrusted(key)` below stops compiling, and the pressure
|
|
66
|
+
* to re-add a second predicate for a binary question comes straight back.
|
|
67
|
+
*/
|
|
68
|
+
isTrustedTypesTheElseBranchToo(key) {
|
|
69
|
+
if (key.isTrusted()) {
|
|
70
|
+
this.takesTrusted(key);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
this.takesUntrusted(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
takesTrusted(key) {
|
|
77
|
+
void key;
|
|
78
|
+
}
|
|
79
|
+
takesUntrusted(key) {
|
|
80
|
+
void key;
|
|
81
|
+
}
|
|
59
82
|
}
|
|
60
83
|
exports.ContextKeyTrustCompileAssertions = ContextKeyTrustCompileAssertions;
|
|
61
84
|
//# sourceMappingURL=ContextKeyTrustCompileAssertions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextKeyTrustCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKeyTrustCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"ContextKeyTrustCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKeyTrustCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,6CAAuG;AAEvG;;;;;;;;;;;;;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;IAED;;;;;;;;OAQG;IACH,8BAA8B,CAAC,GAAkB;QAC7C,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YAClB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,GAAyB;QAC1C,KAAK,GAAG,CAAC;IACb,CAAC;IAEO,cAAc,CAAC,GAA2B;QAC9C,KAAK,GAAG,CAAC;IACb,CAAC;CACJ;AArED,4EAqEC","sourcesContent":["import { ContextKey, AnyContextKey, AnyTrustedContextKey, AnyUntrustedContextKey } 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 /**\n * THE assertion that keeps trust a ONE-METHOD question: `isTrusted()` types BOTH of its branches,\n * so the `else` needs no `isUntrusted()` twin and no cast.\n *\n * POSITIVE, and a tripwire on the SHAPE of {@link AnyContextKey}: this compiles only while that\n * alias is a UNION of the two branches. Respell it as one type with a mixed `Trust` parameter and\n * the negative has nothing to drop, `takesUntrusted(key)` below stops compiling, and the pressure\n * to re-add a second predicate for a binary question comes straight back.\n */\n isTrustedTypesTheElseBranchToo(key: AnyContextKey): void {\n if (key.isTrusted()) {\n this.takesTrusted(key);\n } else {\n this.takesUntrusted(key);\n }\n }\n\n private takesTrusted(key: AnyTrustedContextKey): void {\n void key;\n }\n\n private takesUntrusted(key: AnyUntrustedContextKey): void {\n void key;\n }\n}\n"]}
|