@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 +1 -1
- package/src/ContextKey.d.ts +141 -27
- package/src/ContextKey.js +143 -7
- package/src/ContextKey.js.map +1 -1
- package/src/ContextKeyTrustCompileAssertions.d.ts +34 -0
- package/src/ContextKeyTrustCompileAssertions.js +61 -0
- package/src/ContextKeyTrustCompileAssertions.js.map +1 -0
- package/src/ContextTuple.d.ts +9 -4
- package/src/ContextTuple.js +6 -1
- package/src/ContextTuple.js.map +1 -1
- package/src/http/ApiCallContext.d.ts +8 -4
- package/src/http/ApiCallContext.js.map +1 -1
- package/src/http/HeaderRegistry.d.ts +15 -4
- package/src/http/HeaderRegistry.js +28 -7
- package/src/http/HeaderRegistry.js.map +1 -1
- package/src/http/WebpiecesCoreHeaders.d.ts +32 -13
- package/src/http/WebpiecesCoreHeaders.js +32 -13
- package/src/http/WebpiecesCoreHeaders.js.map +1 -1
- package/src/http/recorder/TestCaseRecorder.d.ts +1 -1
- package/src/http/recorder/TestCaseRecorder.js +1 -1
- package/src/http/recorder/TestCaseRecorder.js.map +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js.map +1 -1
package/package.json
CHANGED
package/src/ContextKey.d.ts
CHANGED
|
@@ -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`
|
|
12
|
-
*
|
|
13
|
-
* - `httpHeader`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - `
|
|
18
|
-
* - `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* type,
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
|
|
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
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
/**
|
|
60
|
-
readonly
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
13
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
34
|
-
if (!this.
|
|
169
|
+
maskForLogs(value) {
|
|
170
|
+
if (!this.maskInLogs) {
|
|
35
171
|
return value;
|
|
36
172
|
}
|
|
37
173
|
const len = value.length;
|
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":";;;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"]}
|
package/src/ContextTuple.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
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:
|
|
14
|
+
readonly key: AnyTrustedContextKey;
|
|
10
15
|
readonly value: unknown;
|
|
11
|
-
constructor(key:
|
|
16
|
+
constructor(key: AnyTrustedContextKey, value: unknown);
|
|
12
17
|
}
|
package/src/ContextTuple.js
CHANGED
|
@@ -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.
|
|
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;
|
package/src/ContextTuple.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextTuple.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextTuple.ts"],"names":[],"mappings":";;;AAEA
|
|
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"]}
|