@webpieces/core-context 0.4.606 → 0.4.607
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/CapturedContext.d.ts +122 -11
- package/src/CapturedContext.js +154 -13
- package/src/CapturedContext.js.map +1 -1
- package/src/CapturedContextCompileAssertions.d.ts +21 -0
- package/src/CapturedContextCompileAssertions.js +48 -1
- package/src/CapturedContextCompileAssertions.js.map +1 -1
- package/src/DetachedScopeCompileAssertions.d.ts +62 -0
- package/src/DetachedScopeCompileAssertions.js +111 -0
- package/src/DetachedScopeCompileAssertions.js.map +1 -0
- package/src/RequestContext.d.ts +84 -11
- package/src/RequestContext.js +91 -10
- package/src/RequestContext.js.map +1 -1
- package/src/RequestContextHeaders.d.ts +6 -3
- package/src/RequestContextHeaders.js +9 -4
- package/src/RequestContextHeaders.js.map +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/core-context",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.607",
|
|
4
4
|
"description": "AsyncLocalStorage-based context management for request-scoped data",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/core-util": "0.4.
|
|
25
|
+
"@webpieces/core-util": "0.4.607",
|
|
26
26
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
27
27
|
"inversify": "7.10.4",
|
|
28
28
|
"reflect-metadata": "0.2.2"
|
package/src/CapturedContext.d.ts
CHANGED
|
@@ -33,6 +33,29 @@ export declare class ContextCaptureAuthority {
|
|
|
33
33
|
/**
|
|
34
34
|
* CapturedContext — an OPAQUE snapshot of a {@link RequestContext} scope.
|
|
35
35
|
*
|
|
36
|
+
* ## THE THREE CASES — pick by where the values came from and what may keep the identity
|
|
37
|
+
*
|
|
38
|
+
* | you have | you want | write |
|
|
39
|
+
* |---|---|---|
|
|
40
|
+
* | a genuine prior scope | ALL of it, trusted values included | `runWithContext(snapshot.withTrusted(), fn)` |
|
|
41
|
+
* | a genuine prior scope | the trace fields but NOT the identity | `runWithContext(snapshot.withoutTrusted(), fn)` |
|
|
42
|
+
* | values from OUTSIDE this process | exactly what you re-state, nothing inherited | `RequestContext.runDetachedScope(fn)` |
|
|
43
|
+
*
|
|
44
|
+
* Row 1 is a faithful re-root of a broken async chain — the work continues AS that user. Row 2 is a
|
|
45
|
+
* deliberate PRIVILEGE DROP: a background job keeps `requestId`/`actionId` so it stays greppable, and
|
|
46
|
+
* loses `userId`/`orgId`/roles because it runs as the system (see {@link withoutTrusted}). Row 3 is a
|
|
47
|
+
* different question entirely — the values were never in a scope here, so there is no snapshot to take;
|
|
48
|
+
* see `RequestContext.runDetachedScope`, where each value is written inside the closure with the trust
|
|
49
|
+
* verbs.
|
|
50
|
+
*
|
|
51
|
+
* There is NO bare form of rows 1 and 2. `copyContext()` hands back a `CapturedContext`, and
|
|
52
|
+
* `runWithContext`/`restoreContext` do not accept one — they take the {@link RestorableContext} that
|
|
53
|
+
* {@link withTrusted} and {@link withoutTrusted} produce. Every call site therefore STATES whether the
|
|
54
|
+
* proven identity travels, and neither intent is shorter to type than the other. That is CLAUDE.md shim
|
|
55
|
+
* shape #5 applied here: a bare snapshot silently carrying a user identity is a widening that is an
|
|
56
|
+
* absence rather than a token, and it is ungreppable. Now `grep -rn withTrusted` enumerates every place
|
|
57
|
+
* an identity crosses a scope boundary and `grep -rn withoutTrusted` every deliberate drop.
|
|
58
|
+
*
|
|
36
59
|
* ## What it is for
|
|
37
60
|
*
|
|
38
61
|
* `AsyncLocalStorage` follows `await`, `.then()` and ordinary callbacks on its own, so the vast
|
|
@@ -44,7 +67,8 @@ export declare class ContextCaptureAuthority {
|
|
|
44
67
|
* log fields and the proven identity would silently vanish from everything the work logs or calls.
|
|
45
68
|
*
|
|
46
69
|
* The answer is two halves: `RequestContext.copyContext()` where the work is ENQUEUED, and
|
|
47
|
-
* `RequestContext.runWithContext(captured, fn)`
|
|
70
|
+
* `RequestContext.runWithContext(captured.withTrusted(), fn)` — or `.withoutTrusted()`, or
|
|
71
|
+
* `restoreContext(...)` of either — where it RUNS. The narrowing is not optional; see the table above.
|
|
48
72
|
*
|
|
49
73
|
* ## Why it is opaque instead of a `Map<string, unknown>`
|
|
50
74
|
*
|
|
@@ -69,16 +93,17 @@ export declare class ContextCaptureAuthority {
|
|
|
69
93
|
* type-checks. Withholding the class object is what actually closes it;
|
|
70
94
|
* - the entries live in `#entries`, a genuine ECMAScript private field, so they are unreachable at
|
|
71
95
|
* RUNTIME as well as at compile time — no `Object.keys`, no cast, no index signature;
|
|
72
|
-
* - the map is defensively copied ON CAPTURE and again ON RESTORE, so a
|
|
73
|
-
* live context (or who keeps writing to it after capturing) cannot reach
|
|
74
|
-
*
|
|
96
|
+
* - the map is defensively copied ON CAPTURE, again on each NARROWING, and again ON RESTORE, so a
|
|
97
|
+
* caller who still holds the live context (or who keeps writing to it after capturing) cannot reach
|
|
98
|
+
* through the snapshot, narrowing never mutates the capture it came from, and a snapshot can be
|
|
99
|
+
* restored repeatedly without the first restore's mutations bleeding into the second.
|
|
75
100
|
*
|
|
76
101
|
* There is deliberately no reader: nothing hands the entries back out. That is why `getAll()` is gone
|
|
77
102
|
* rather than re-typed to return one of these — a `CapturedContext` you cannot read is useless as a
|
|
78
103
|
* `getAll`, and a readable one would be the raw enumeration of every trusted value all over again.
|
|
79
104
|
*
|
|
80
|
-
* The one residual: a consumer holding a snapshot can still cast a token into
|
|
81
|
-
* the entries back out as a plain Map. That is knowingly accepted, and it is the same asymmetry
|
|
105
|
+
* The one residual: a consumer holding a NARROWED snapshot can still cast a token into
|
|
106
|
+
* {@link RestorableContext.toFreshStore} and read the entries back out as a plain Map. That is knowingly accepted, and it is the same asymmetry
|
|
82
107
|
* `RequestContext.getAny` states — FORGING a trusted value is the dangerous direction and is closed
|
|
83
108
|
* here; reading one you were already legitimately handed, without saying `getTrusted`, costs you
|
|
84
109
|
* nothing but the type. Closing it too would mean no method could take the token at all, which is to
|
|
@@ -99,6 +124,96 @@ export declare class CapturedContext {
|
|
|
99
124
|
* ONLY, so no consumer ever holds the class object this static hangs off. See the class doc.
|
|
100
125
|
*/
|
|
101
126
|
static capture(authority: ContextCaptureAuthority, live: Map<string, unknown>): CapturedContext;
|
|
127
|
+
/**
|
|
128
|
+
* Carry EVERY value onward, the proven identity included — the faithful re-root. The work runs AS
|
|
129
|
+
* that user: `getTrusted(USER_ID)` inside it answers exactly what it answered in the original scope,
|
|
130
|
+
* which is the entire point when a request's own continuation was re-rooted onto a queue or a timer.
|
|
131
|
+
*
|
|
132
|
+
* Said OUT LOUD, because it is the wide branch. A bare snapshot is deliberately not accepted by
|
|
133
|
+
* `runWithContext`/`restoreContext` (see the class doc), so the identity never crosses a scope
|
|
134
|
+
* boundary by default or by omission, and `grep -rn withTrusted` enumerates every place it does.
|
|
135
|
+
*
|
|
136
|
+
* NON-MUTATING, like its sibling — the receiver is unchanged, so ONE snapshot can be narrowed both
|
|
137
|
+
* ways at two different call sites.
|
|
138
|
+
*/
|
|
139
|
+
withTrusted(): RestorableContext;
|
|
140
|
+
/**
|
|
141
|
+
* Carry only the UNTRUSTED values — a deliberate PRIVILEGE DROP.
|
|
142
|
+
*
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const snapshot = RequestContext.copyContext();
|
|
145
|
+
* RequestContext.runWithContext(snapshot.withTrusted(), fn); // runs AS that user
|
|
146
|
+
* RequestContext.runWithContext(snapshot.withoutTrusted(), fn); // runs as the SYSTEM
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* The case: a background job or fire-and-forget task spawned during a request should keep the
|
|
150
|
+
* untrusted trace fields — `requestId`, `actionId` — so its log lines are still greppable back to
|
|
151
|
+
* the click that caused them, but it must NOT keep `userId` / `orgId` / roles, because it executes
|
|
152
|
+
* as the system rather than as that user. Carrying the proven identity onward would make every
|
|
153
|
+
* downstream authorization decision think the user is still on the other end of the wire.
|
|
154
|
+
*
|
|
155
|
+
* A METHOD PAIR, never a `keepTrusted: boolean` on {@link RequestContext.runWithContext}: a
|
|
156
|
+
* parameter makes the two intents equally easy to type and impossible to grep, and a defaulted one
|
|
157
|
+
* makes the permissive branch the shortest thing to write — CLAUDE.md shim shape #5, "a widening
|
|
158
|
+
* that is an ABSENCE rather than a token", the same reason `@AuthJwt({allRolesAllowed: true})` says
|
|
159
|
+
* the wide grant out loud. As a transform on the SNAPSHOT rather than a second capture mechanism it
|
|
160
|
+
* composes with BOTH consumers — `runWithContext` and `restoreContext` — for free.
|
|
161
|
+
*
|
|
162
|
+
* NON-MUTATING: the receiver is untouched, so one snapshot can be used both ways.
|
|
163
|
+
*
|
|
164
|
+
* NO AUTHORITY TOKEN, deliberately, and it must not grow one. The token on {@link capture} exists
|
|
165
|
+
* because CONSTRUCTING a snapshot from arbitrary entries forges trust. This direction only ever
|
|
166
|
+
* REMOVES entries: whatever survives was already in a real capture of a real scope, so the result
|
|
167
|
+
* is strictly less privileged than the object the caller is already holding. Dropping cannot forge.
|
|
168
|
+
*
|
|
169
|
+
* WHAT SURVIVES is exactly "registered as an UNTRUSTED {@link ContextKey}". Trusted keys go, and so
|
|
170
|
+
* do names the {@link HeaderRegistry} does not know — the framework's reserved slots (the
|
|
171
|
+
* `HttpRequest`, the AuthFilter principal, the Cloud Tasks schedule frame), which carry no declared
|
|
172
|
+
* trust and are the caller's identity and connection rather than trace fields. A privilege drop
|
|
173
|
+
* that guessed in the permissive direction would not be one. For the same reason, with no registry
|
|
174
|
+
* configured NOTHING is knowably untrusted and the result is empty — always the safe answer, since
|
|
175
|
+
* this method's only job is to remove.
|
|
176
|
+
*/
|
|
177
|
+
withoutTrusted(): RestorableContext;
|
|
178
|
+
/**
|
|
179
|
+
* How many entries the snapshot holds. The one thing it will tell you about itself — a count is
|
|
180
|
+
* not a value, so it leaks nothing, and it lets a caller (and a test) see that a capture taken
|
|
181
|
+
* outside an active scope is simply empty rather than an error.
|
|
182
|
+
*/
|
|
183
|
+
size(): number;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* A snapshot whose TRUST INTENT has been stated — the only thing `RequestContext.restoreContext` and
|
|
187
|
+
* `RequestContext.runWithContext` accept.
|
|
188
|
+
*
|
|
189
|
+
* It exists to make the wide choice unskippable. A single type would have meant
|
|
190
|
+
* `runWithContext(snapshot, fn)` compiling next to `runWithContext(snapshot.withTrusted(), fn)`: two
|
|
191
|
+
* spellings of one thing (shim shape #1), with the shorter one silently carrying a user identity into
|
|
192
|
+
* work that may have no business running as that user. Splitting the type deletes the default — a
|
|
193
|
+
* capture is inert until it says which it means — so there is exactly one spelling per intent, and
|
|
194
|
+
* `grep -rn withTrusted` / `grep -rn withoutTrusted` enumerate the two populations of call sites.
|
|
195
|
+
*
|
|
196
|
+
* Two CLASSES rather than a phantom type parameter on {@link CapturedContext}, even though this repo
|
|
197
|
+
* uses that trick on `ContextKey<V, T extends Trust>`. There the parameter rides along with a key that
|
|
198
|
+
* consumers name constantly and read values through, so it earns its complexity. Here the two states
|
|
199
|
+
* have DIFFERENT MEMBERS — a capture can only be narrowed, a narrowed one can only be restored — and a
|
|
200
|
+
* type that changes its members between states is a second class, not a second type argument. Naming
|
|
201
|
+
* it also gives the field/queue-entry type a consumer holds a name that says what it is.
|
|
202
|
+
*
|
|
203
|
+
* The #622 opacity guarantees are unchanged and are why this class is also barrel-exported as a TYPE
|
|
204
|
+
* ONLY: private constructor, a capability token on the producer, a real `#entries` private field, and
|
|
205
|
+
* defensive copies on the way in and on the way out.
|
|
206
|
+
*/
|
|
207
|
+
export declare class RestorableContext {
|
|
208
|
+
#private;
|
|
209
|
+
/** PRIVATE — {@link of} is the only producer, and only this module can call it. */
|
|
210
|
+
private constructor();
|
|
211
|
+
/**
|
|
212
|
+
* The ONLY producer, called by `CapturedContext.withTrusted()` / `withoutTrusted()`. Guarded the
|
|
213
|
+
* same way `capture` is: a token no consumer can name, and a barrel that exports this class as a
|
|
214
|
+
* TYPE ONLY, so the class object — and with it this static — never reaches a consumer at all.
|
|
215
|
+
*/
|
|
216
|
+
static of(authority: ContextCaptureAuthority, entries: Map<string, unknown>): RestorableContext;
|
|
102
217
|
/**
|
|
103
218
|
* Overwrite a LIVE store with this snapshot — the engine behind `RequestContext.restoreContext`.
|
|
104
219
|
* Write-only by design: it pushes entries in and hands nothing back, so it is not a side door onto
|
|
@@ -111,10 +226,6 @@ export declare class CapturedContext {
|
|
|
111
226
|
* everything the restored scope writes stays in that scope and the snapshot remains reusable.
|
|
112
227
|
*/
|
|
113
228
|
toFreshStore(authority: ContextCaptureAuthority): Map<string, unknown>;
|
|
114
|
-
/**
|
|
115
|
-
* How many entries the snapshot holds. The one thing it will tell you about itself — a count is
|
|
116
|
-
* not a value, so it leaks nothing, and it lets a caller (and a test) see that a capture taken
|
|
117
|
-
* outside an active scope is simply empty rather than an error.
|
|
118
|
-
*/
|
|
229
|
+
/** How many entries survived the narrowing. A count is not a value, so it leaks nothing. */
|
|
119
230
|
size(): number;
|
|
120
231
|
}
|
package/src/CapturedContext.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CapturedContext = exports.ContextCaptureAuthority = void 0;
|
|
3
|
+
exports.RestorableContext = exports.CapturedContext = exports.ContextCaptureAuthority = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
4
5
|
/**
|
|
5
6
|
* A capability TOKEN, not data — the thing you must be holding to build a {@link CapturedContext} or
|
|
6
7
|
* to unpack one.
|
|
@@ -39,6 +40,29 @@ exports.ContextCaptureAuthority = ContextCaptureAuthority;
|
|
|
39
40
|
/**
|
|
40
41
|
* CapturedContext — an OPAQUE snapshot of a {@link RequestContext} scope.
|
|
41
42
|
*
|
|
43
|
+
* ## THE THREE CASES — pick by where the values came from and what may keep the identity
|
|
44
|
+
*
|
|
45
|
+
* | you have | you want | write |
|
|
46
|
+
* |---|---|---|
|
|
47
|
+
* | a genuine prior scope | ALL of it, trusted values included | `runWithContext(snapshot.withTrusted(), fn)` |
|
|
48
|
+
* | a genuine prior scope | the trace fields but NOT the identity | `runWithContext(snapshot.withoutTrusted(), fn)` |
|
|
49
|
+
* | values from OUTSIDE this process | exactly what you re-state, nothing inherited | `RequestContext.runDetachedScope(fn)` |
|
|
50
|
+
*
|
|
51
|
+
* Row 1 is a faithful re-root of a broken async chain — the work continues AS that user. Row 2 is a
|
|
52
|
+
* deliberate PRIVILEGE DROP: a background job keeps `requestId`/`actionId` so it stays greppable, and
|
|
53
|
+
* loses `userId`/`orgId`/roles because it runs as the system (see {@link withoutTrusted}). Row 3 is a
|
|
54
|
+
* different question entirely — the values were never in a scope here, so there is no snapshot to take;
|
|
55
|
+
* see `RequestContext.runDetachedScope`, where each value is written inside the closure with the trust
|
|
56
|
+
* verbs.
|
|
57
|
+
*
|
|
58
|
+
* There is NO bare form of rows 1 and 2. `copyContext()` hands back a `CapturedContext`, and
|
|
59
|
+
* `runWithContext`/`restoreContext` do not accept one — they take the {@link RestorableContext} that
|
|
60
|
+
* {@link withTrusted} and {@link withoutTrusted} produce. Every call site therefore STATES whether the
|
|
61
|
+
* proven identity travels, and neither intent is shorter to type than the other. That is CLAUDE.md shim
|
|
62
|
+
* shape #5 applied here: a bare snapshot silently carrying a user identity is a widening that is an
|
|
63
|
+
* absence rather than a token, and it is ungreppable. Now `grep -rn withTrusted` enumerates every place
|
|
64
|
+
* an identity crosses a scope boundary and `grep -rn withoutTrusted` every deliberate drop.
|
|
65
|
+
*
|
|
42
66
|
* ## What it is for
|
|
43
67
|
*
|
|
44
68
|
* `AsyncLocalStorage` follows `await`, `.then()` and ordinary callbacks on its own, so the vast
|
|
@@ -50,7 +74,8 @@ exports.ContextCaptureAuthority = ContextCaptureAuthority;
|
|
|
50
74
|
* log fields and the proven identity would silently vanish from everything the work logs or calls.
|
|
51
75
|
*
|
|
52
76
|
* The answer is two halves: `RequestContext.copyContext()` where the work is ENQUEUED, and
|
|
53
|
-
* `RequestContext.runWithContext(captured, fn)`
|
|
77
|
+
* `RequestContext.runWithContext(captured.withTrusted(), fn)` — or `.withoutTrusted()`, or
|
|
78
|
+
* `restoreContext(...)` of either — where it RUNS. The narrowing is not optional; see the table above.
|
|
54
79
|
*
|
|
55
80
|
* ## Why it is opaque instead of a `Map<string, unknown>`
|
|
56
81
|
*
|
|
@@ -75,16 +100,17 @@ exports.ContextCaptureAuthority = ContextCaptureAuthority;
|
|
|
75
100
|
* type-checks. Withholding the class object is what actually closes it;
|
|
76
101
|
* - the entries live in `#entries`, a genuine ECMAScript private field, so they are unreachable at
|
|
77
102
|
* RUNTIME as well as at compile time — no `Object.keys`, no cast, no index signature;
|
|
78
|
-
* - the map is defensively copied ON CAPTURE and again ON RESTORE, so a
|
|
79
|
-
* live context (or who keeps writing to it after capturing) cannot reach
|
|
80
|
-
*
|
|
103
|
+
* - the map is defensively copied ON CAPTURE, again on each NARROWING, and again ON RESTORE, so a
|
|
104
|
+
* caller who still holds the live context (or who keeps writing to it after capturing) cannot reach
|
|
105
|
+
* through the snapshot, narrowing never mutates the capture it came from, and a snapshot can be
|
|
106
|
+
* restored repeatedly without the first restore's mutations bleeding into the second.
|
|
81
107
|
*
|
|
82
108
|
* There is deliberately no reader: nothing hands the entries back out. That is why `getAll()` is gone
|
|
83
109
|
* rather than re-typed to return one of these — a `CapturedContext` you cannot read is useless as a
|
|
84
110
|
* `getAll`, and a readable one would be the raw enumeration of every trusted value all over again.
|
|
85
111
|
*
|
|
86
|
-
* The one residual: a consumer holding a snapshot can still cast a token into
|
|
87
|
-
* the entries back out as a plain Map. That is knowingly accepted, and it is the same asymmetry
|
|
112
|
+
* The one residual: a consumer holding a NARROWED snapshot can still cast a token into
|
|
113
|
+
* {@link RestorableContext.toFreshStore} and read the entries back out as a plain Map. That is knowingly accepted, and it is the same asymmetry
|
|
88
114
|
* `RequestContext.getAny` states — FORGING a trusted value is the dangerous direction and is closed
|
|
89
115
|
* here; reading one you were already legitimately handed, without saying `getTrusted`, costs you
|
|
90
116
|
* nothing but the type. Closing it too would mean no method could take the token at all, which is to
|
|
@@ -119,6 +145,125 @@ class CapturedContext {
|
|
|
119
145
|
void authority;
|
|
120
146
|
return new CapturedContext(live);
|
|
121
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Carry EVERY value onward, the proven identity included — the faithful re-root. The work runs AS
|
|
150
|
+
* that user: `getTrusted(USER_ID)` inside it answers exactly what it answered in the original scope,
|
|
151
|
+
* which is the entire point when a request's own continuation was re-rooted onto a queue or a timer.
|
|
152
|
+
*
|
|
153
|
+
* Said OUT LOUD, because it is the wide branch. A bare snapshot is deliberately not accepted by
|
|
154
|
+
* `runWithContext`/`restoreContext` (see the class doc), so the identity never crosses a scope
|
|
155
|
+
* boundary by default or by omission, and `grep -rn withTrusted` enumerates every place it does.
|
|
156
|
+
*
|
|
157
|
+
* NON-MUTATING, like its sibling — the receiver is unchanged, so ONE snapshot can be narrowed both
|
|
158
|
+
* ways at two different call sites.
|
|
159
|
+
*/
|
|
160
|
+
withTrusted() {
|
|
161
|
+
return RestorableContext.of(ContextCaptureAuthority.INTERNAL, this.#entries);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Carry only the UNTRUSTED values — a deliberate PRIVILEGE DROP.
|
|
165
|
+
*
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const snapshot = RequestContext.copyContext();
|
|
168
|
+
* RequestContext.runWithContext(snapshot.withTrusted(), fn); // runs AS that user
|
|
169
|
+
* RequestContext.runWithContext(snapshot.withoutTrusted(), fn); // runs as the SYSTEM
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* The case: a background job or fire-and-forget task spawned during a request should keep the
|
|
173
|
+
* untrusted trace fields — `requestId`, `actionId` — so its log lines are still greppable back to
|
|
174
|
+
* the click that caused them, but it must NOT keep `userId` / `orgId` / roles, because it executes
|
|
175
|
+
* as the system rather than as that user. Carrying the proven identity onward would make every
|
|
176
|
+
* downstream authorization decision think the user is still on the other end of the wire.
|
|
177
|
+
*
|
|
178
|
+
* A METHOD PAIR, never a `keepTrusted: boolean` on {@link RequestContext.runWithContext}: a
|
|
179
|
+
* parameter makes the two intents equally easy to type and impossible to grep, and a defaulted one
|
|
180
|
+
* makes the permissive branch the shortest thing to write — CLAUDE.md shim shape #5, "a widening
|
|
181
|
+
* that is an ABSENCE rather than a token", the same reason `@AuthJwt({allRolesAllowed: true})` says
|
|
182
|
+
* the wide grant out loud. As a transform on the SNAPSHOT rather than a second capture mechanism it
|
|
183
|
+
* composes with BOTH consumers — `runWithContext` and `restoreContext` — for free.
|
|
184
|
+
*
|
|
185
|
+
* NON-MUTATING: the receiver is untouched, so one snapshot can be used both ways.
|
|
186
|
+
*
|
|
187
|
+
* NO AUTHORITY TOKEN, deliberately, and it must not grow one. The token on {@link capture} exists
|
|
188
|
+
* because CONSTRUCTING a snapshot from arbitrary entries forges trust. This direction only ever
|
|
189
|
+
* REMOVES entries: whatever survives was already in a real capture of a real scope, so the result
|
|
190
|
+
* is strictly less privileged than the object the caller is already holding. Dropping cannot forge.
|
|
191
|
+
*
|
|
192
|
+
* WHAT SURVIVES is exactly "registered as an UNTRUSTED {@link ContextKey}". Trusted keys go, and so
|
|
193
|
+
* do names the {@link HeaderRegistry} does not know — the framework's reserved slots (the
|
|
194
|
+
* `HttpRequest`, the AuthFilter principal, the Cloud Tasks schedule frame), which carry no declared
|
|
195
|
+
* trust and are the caller's identity and connection rather than trace fields. A privilege drop
|
|
196
|
+
* that guessed in the permissive direction would not be one. For the same reason, with no registry
|
|
197
|
+
* configured NOTHING is knowably untrusted and the result is empty — always the safe answer, since
|
|
198
|
+
* this method's only job is to remove.
|
|
199
|
+
*/
|
|
200
|
+
withoutTrusted() {
|
|
201
|
+
// webpieces-disable no-any-unknown -- the context store is deliberately type-erased; see #entries
|
|
202
|
+
const kept = new Map();
|
|
203
|
+
if (!core_util_1.HeaderRegistry.isConfigured()) {
|
|
204
|
+
return RestorableContext.of(ContextCaptureAuthority.INTERNAL, kept);
|
|
205
|
+
}
|
|
206
|
+
const registry = core_util_1.HeaderRegistry.get();
|
|
207
|
+
for (const name of this.#entries.keys()) {
|
|
208
|
+
const key = registry.findByName(name);
|
|
209
|
+
if (key && key.isUntrusted()) {
|
|
210
|
+
kept.set(name, this.#entries.get(name));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return RestorableContext.of(ContextCaptureAuthority.INTERNAL, kept);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* How many entries the snapshot holds. The one thing it will tell you about itself — a count is
|
|
217
|
+
* not a value, so it leaks nothing, and it lets a caller (and a test) see that a capture taken
|
|
218
|
+
* outside an active scope is simply empty rather than an error.
|
|
219
|
+
*/
|
|
220
|
+
size() {
|
|
221
|
+
return this.#entries.size;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
exports.CapturedContext = CapturedContext;
|
|
225
|
+
/**
|
|
226
|
+
* A snapshot whose TRUST INTENT has been stated — the only thing `RequestContext.restoreContext` and
|
|
227
|
+
* `RequestContext.runWithContext` accept.
|
|
228
|
+
*
|
|
229
|
+
* It exists to make the wide choice unskippable. A single type would have meant
|
|
230
|
+
* `runWithContext(snapshot, fn)` compiling next to `runWithContext(snapshot.withTrusted(), fn)`: two
|
|
231
|
+
* spellings of one thing (shim shape #1), with the shorter one silently carrying a user identity into
|
|
232
|
+
* work that may have no business running as that user. Splitting the type deletes the default — a
|
|
233
|
+
* capture is inert until it says which it means — so there is exactly one spelling per intent, and
|
|
234
|
+
* `grep -rn withTrusted` / `grep -rn withoutTrusted` enumerate the two populations of call sites.
|
|
235
|
+
*
|
|
236
|
+
* Two CLASSES rather than a phantom type parameter on {@link CapturedContext}, even though this repo
|
|
237
|
+
* uses that trick on `ContextKey<V, T extends Trust>`. There the parameter rides along with a key that
|
|
238
|
+
* consumers name constantly and read values through, so it earns its complexity. Here the two states
|
|
239
|
+
* have DIFFERENT MEMBERS — a capture can only be narrowed, a narrowed one can only be restored — and a
|
|
240
|
+
* type that changes its members between states is a second class, not a second type argument. Naming
|
|
241
|
+
* it also gives the field/queue-entry type a consumer holds a name that says what it is.
|
|
242
|
+
*
|
|
243
|
+
* The #622 opacity guarantees are unchanged and are why this class is also barrel-exported as a TYPE
|
|
244
|
+
* ONLY: private constructor, a capability token on the producer, a real `#entries` private field, and
|
|
245
|
+
* defensive copies on the way in and on the way out.
|
|
246
|
+
*/
|
|
247
|
+
class RestorableContext {
|
|
248
|
+
/** Same real ECMAScript private field, for the same reason — see {@link CapturedContext}. */
|
|
249
|
+
// webpieces-disable no-any-unknown -- the context store is deliberately type-erased; each ContextKey carries its own value type and the typed verbs re-apply it on read
|
|
250
|
+
#entries;
|
|
251
|
+
/** PRIVATE — {@link of} is the only producer, and only this module can call it. */
|
|
252
|
+
// webpieces-disable no-any-unknown -- see #entries
|
|
253
|
+
constructor(entries) {
|
|
254
|
+
this.#entries = new Map(entries);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The ONLY producer, called by `CapturedContext.withTrusted()` / `withoutTrusted()`. Guarded the
|
|
258
|
+
* same way `capture` is: a token no consumer can name, and a barrel that exports this class as a
|
|
259
|
+
* TYPE ONLY, so the class object — and with it this static — never reaches a consumer at all.
|
|
260
|
+
*/
|
|
261
|
+
// webpieces-disable no-any-unknown -- see #entries
|
|
262
|
+
// webpieces-disable no-function-outside-class -- static factory standing in for the (private) constructor; an instance method would presuppose the instance being created
|
|
263
|
+
static of(authority, entries) {
|
|
264
|
+
void authority;
|
|
265
|
+
return new RestorableContext(entries);
|
|
266
|
+
}
|
|
122
267
|
/**
|
|
123
268
|
* Overwrite a LIVE store with this snapshot — the engine behind `RequestContext.restoreContext`.
|
|
124
269
|
* Write-only by design: it pushes entries in and hands nothing back, so it is not a side door onto
|
|
@@ -142,14 +287,10 @@ class CapturedContext {
|
|
|
142
287
|
void authority;
|
|
143
288
|
return new Map(this.#entries);
|
|
144
289
|
}
|
|
145
|
-
/**
|
|
146
|
-
* How many entries the snapshot holds. The one thing it will tell you about itself — a count is
|
|
147
|
-
* not a value, so it leaks nothing, and it lets a caller (and a test) see that a capture taken
|
|
148
|
-
* outside an active scope is simply empty rather than an error.
|
|
149
|
-
*/
|
|
290
|
+
/** How many entries survived the narrowing. A count is not a value, so it leaks nothing. */
|
|
150
291
|
size() {
|
|
151
292
|
return this.#entries.size;
|
|
152
293
|
}
|
|
153
294
|
}
|
|
154
|
-
exports.
|
|
295
|
+
exports.RestorableContext = RestorableContext;
|
|
155
296
|
//# sourceMappingURL=CapturedContext.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CapturedContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/CapturedContext.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAa,uBAAuB;IAChC;;;OAGG;IACc,KAAK,GAAW,2BAA2B,CAAC;IAE7D,4FAA4F;IAC5F,MAAM,CAAU,QAAQ,GAAG,IAAI,uBAAuB,EAAE,CAAC;IAEzD,gBAAuB,CAAC;IAExB,2FAA2F;IAC3F,QAAQ;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC;IACtB,CAAC;;AAfL,0DAgBC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,MAAa,eAAe;IACxB;;;;OAIG;IACH,wKAAwK;IAC/J,QAAQ,CAAuB;IAExC;;;;OAIG;IACH,mDAAmD;IACnD,YAAoB,OAA6B;QAC7C,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IAED;;;;;OAKG;IACH,mDAAmD;IACnD,iNAAiN;IACjN,MAAM,CAAC,OAAO,CAAC,SAAkC,EAAE,IAA0B;QACzE,KAAK,SAAS,CAAC;QACf,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAED;;;;OAIG;IACH,mDAAmD;IACnD,WAAW,CAAC,SAAkC,EAAE,IAA0B;QACtE,KAAK,SAAS,CAAC;QACf,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YACtC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5C,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,mDAAmD;IACnD,YAAY,CAAC,SAAkC;QAC3C,KAAK,SAAS,CAAC;QACf,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC;IAED;;;;OAIG;IACH,IAAI;QACA,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC9B,CAAC;CACJ;AAjED,0CAiEC","sourcesContent":["/**\n * A capability TOKEN, not data — the thing you must be holding to build a {@link CapturedContext} or\n * to unpack one.\n *\n * It exists because `CapturedContext` needs a producer that {@link RequestContext} (a different class,\n * in a different file) can call, and TypeScript's `private` is class-scoped: a plain public\n * `CapturedContext.capture(map)` would be exactly the hand-assembled-payload hole this class exists to\n * close. So the producer takes a token whose constructor is private and whose only instance is\n * {@link INTERNAL}, and this module is deliberately NOT re-exported from the package barrel — in any\n * form, type or value.\n *\n * On its own that stops a consumer NAMING the token but not SUPPLYING one, since `null as never`\n * type-checks. It is the barrel's `export type { CapturedContext }` that finishes the job: with no\n * class object on the package surface there is no `capture(...)` to call, cast or not. The two halves\n * are load-bearing together, which is why each names the other.\n *\n * Per CLAUDE.md this is a class rather than a `Symbol` or an object literal: it is nominal (the private\n * `brand` field stops a structurally-identical `{}` from satisfying it) and it has exactly one\n * instantiation point.\n */\nexport class ContextCaptureAuthority {\n /**\n * Nominal brand. Without a private member the class is structurally `{}`, and any object at all\n * would typecheck as an authority.\n */\n private readonly brand: string = 'webpieces.context-capture';\n\n /** The ONE token that exists. The constructor below is private, so no other can be made. */\n static readonly INTERNAL = new ContextCaptureAuthority();\n\n private constructor() {}\n\n /** Kept honest — the brand is read here so it is a real field, not a type-only fiction. */\n describe(): string {\n return this.brand;\n }\n}\n\n/**\n * CapturedContext — an OPAQUE snapshot of a {@link RequestContext} scope.\n *\n * ## What it is for\n *\n * `AsyncLocalStorage` follows `await`, `.then()` and ordinary callbacks on its own, so the vast\n * majority of code never needs this. What it does NOT follow is work whose async chain was BROKEN and\n * re-rooted somewhere else: an item pushed onto an in-memory queue during a request and drained later\n * by a background loop, a batch flushed on a scheduler tick, an `EventEmitter` listener fired from a\n * socket the request does not own, a retry re-armed from a top-level timer, a hand-off to a worker\n * pool. In each of those the work executes under a DIFFERENT (or no) context, so the request id, the\n * log fields and the proven identity would silently vanish from everything the work logs or calls.\n *\n * The answer is two halves: `RequestContext.copyContext()` where the work is ENQUEUED, and\n * `RequestContext.runWithContext(captured, fn)` (or `restoreContext(captured)`) where it RUNS.\n *\n * ## Why it is opaque instead of a `Map<string, unknown>`\n *\n * A restored context legitimately contains TRUSTED values — reinstating what the original scope had\n * proven is the entire point — so the restore side cannot type-check its payload the way\n * `putTrusted`/`getTrusted` do. That left the `Map`-taking signature that this class DELETED (a\n * now-removed `setContext(map)`, and `runWithContext(map, fn)`) as a complete bypass of the trust\n * system: handing it `new Map([['userId', 'victim']])` forged a proven identity in one line, without\n * ever typing a trust verb, and the only thing standing against it was a doc comment saying \"the Map\n * must come from copyContext()\". An agent picks whatever compiles, so a doc comment is not\n * enforcement.\n *\n * Making the PAYLOAD opaque solves it without type-checking the contents: the only way to obtain one is\n * a real capture of a real scope, so whatever it holds was, by construction, already in a context that\n * something legitimately wrote. Concretely:\n *\n * - the constructor is `private`, and there is no public factory — {@link capture} demands a\n * {@link ContextCaptureAuthority} that cannot be named outside this package;\n * - the package barrel exports this class as a TYPE ONLY, so a consumer never receives the class\n * object at all and cannot reach `capture` even with a cast. That second half matters: a token whose\n * TYPE is unexported still stops nothing on its own, because `capture(null as never, forgedMap)`\n * type-checks. Withholding the class object is what actually closes it;\n * - the entries live in `#entries`, a genuine ECMAScript private field, so they are unreachable at\n * RUNTIME as well as at compile time — no `Object.keys`, no cast, no index signature;\n * - the map is defensively copied ON CAPTURE and again ON RESTORE, so a caller who still holds the\n * live context (or who keeps writing to it after capturing) cannot reach through the snapshot, and a\n * snapshot can be restored repeatedly without the first restore's mutations bleeding into the second.\n *\n * There is deliberately no reader: nothing hands the entries back out. That is why `getAll()` is gone\n * rather than re-typed to return one of these — a `CapturedContext` you cannot read is useless as a\n * `getAll`, and a readable one would be the raw enumeration of every trusted value all over again.\n *\n * The one residual: a consumer holding a snapshot can still cast a token into `toFreshStore` and read\n * the entries back out as a plain Map. That is knowingly accepted, and it is the same asymmetry\n * `RequestContext.getAny` states — FORGING a trusted value is the dangerous direction and is closed\n * here; reading one you were already legitimately handed, without saying `getTrusted`, costs you\n * nothing but the type. Closing it too would mean no method could take the token at all, which is to\n * say no restore could exist.\n */\nexport class CapturedContext {\n /**\n * A real ECMAScript private field, not a TypeScript `private`. The distinction matters here: `#`\n * is enforced by the runtime, so the snapshot's contents cannot be reached by a cast, by\n * `Object.entries`, or by `as unknown as { entries: Map<string, unknown> }`.\n */\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; each ContextKey carries its own value type and the typed verbs re-apply it on read\n readonly #entries: Map<string, unknown>;\n\n /**\n * PRIVATE — a CapturedContext can only come from {@link capture}, which in turn can only be called\n * by code holding a {@link ContextCaptureAuthority}. Copies the map so the snapshot is never a\n * window onto a live store.\n */\n // webpieces-disable no-any-unknown -- see #entries\n private constructor(entries: Map<string, unknown>) {\n this.#entries = new Map(entries);\n }\n\n /**\n * The ONLY producer. `authority` is a compile-time capability, not a runtime check — so it is\n * referenced below only to keep it from being an unused parameter. Note the token alone is not the\n * guarantee (a cast can supply one); the guarantee is that the barrel exports this class as a TYPE\n * ONLY, so no consumer ever holds the class object this static hangs off. See the class doc.\n */\n // webpieces-disable no-any-unknown -- see #entries\n // webpieces-disable no-function-outside-class -- static factory standing in for the (private) constructor; making it an instance method would mean an instance already existed, which is the thing being created\n static capture(authority: ContextCaptureAuthority, live: Map<string, unknown>): CapturedContext {\n void authority;\n return new CapturedContext(live);\n }\n\n /**\n * Overwrite a LIVE store with this snapshot — the engine behind `RequestContext.restoreContext`.\n * Write-only by design: it pushes entries in and hands nothing back, so it is not a side door onto\n * the snapshot's contents.\n */\n // webpieces-disable no-any-unknown -- see #entries\n restoreInto(authority: ContextCaptureAuthority, live: Map<string, unknown>): void {\n void authority;\n live.clear();\n for (const name of this.#entries.keys()) {\n live.set(name, this.#entries.get(name));\n }\n }\n\n /**\n * A FRESH store holding this snapshot — the engine behind `RequestContext.runWithContext`, which\n * opens a new AsyncLocalStorage scope around it. Fresh (a copy) rather than the internal map, so\n * everything the restored scope writes stays in that scope and the snapshot remains reusable.\n */\n // webpieces-disable no-any-unknown -- see #entries\n toFreshStore(authority: ContextCaptureAuthority): Map<string, unknown> {\n void authority;\n return new Map(this.#entries);\n }\n\n /**\n * How many entries the snapshot holds. The one thing it will tell you about itself — a count is\n * not a value, so it leaks nothing, and it lets a caller (and a test) see that a capture taken\n * outside an active scope is simply empty rather than an error.\n */\n size(): number {\n return this.#entries.size;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"CapturedContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/CapturedContext.ts"],"names":[],"mappings":"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","sourcesContent":["import { HeaderRegistry } from '@webpieces/core-util';\n\n/**\n * A capability TOKEN, not data — the thing you must be holding to build a {@link CapturedContext} or\n * to unpack one.\n *\n * It exists because `CapturedContext` needs a producer that {@link RequestContext} (a different class,\n * in a different file) can call, and TypeScript's `private` is class-scoped: a plain public\n * `CapturedContext.capture(map)` would be exactly the hand-assembled-payload hole this class exists to\n * close. So the producer takes a token whose constructor is private and whose only instance is\n * {@link INTERNAL}, and this module is deliberately NOT re-exported from the package barrel — in any\n * form, type or value.\n *\n * On its own that stops a consumer NAMING the token but not SUPPLYING one, since `null as never`\n * type-checks. It is the barrel's `export type { CapturedContext }` that finishes the job: with no\n * class object on the package surface there is no `capture(...)` to call, cast or not. The two halves\n * are load-bearing together, which is why each names the other.\n *\n * Per CLAUDE.md this is a class rather than a `Symbol` or an object literal: it is nominal (the private\n * `brand` field stops a structurally-identical `{}` from satisfying it) and it has exactly one\n * instantiation point.\n */\nexport class ContextCaptureAuthority {\n /**\n * Nominal brand. Without a private member the class is structurally `{}`, and any object at all\n * would typecheck as an authority.\n */\n private readonly brand: string = 'webpieces.context-capture';\n\n /** The ONE token that exists. The constructor below is private, so no other can be made. */\n static readonly INTERNAL = new ContextCaptureAuthority();\n\n private constructor() {}\n\n /** Kept honest — the brand is read here so it is a real field, not a type-only fiction. */\n describe(): string {\n return this.brand;\n }\n}\n\n/**\n * CapturedContext — an OPAQUE snapshot of a {@link RequestContext} scope.\n *\n * ## THE THREE CASES — pick by where the values came from and what may keep the identity\n *\n * | you have | you want | write |\n * |---|---|---|\n * | a genuine prior scope | ALL of it, trusted values included | `runWithContext(snapshot.withTrusted(), fn)` |\n * | a genuine prior scope | the trace fields but NOT the identity | `runWithContext(snapshot.withoutTrusted(), fn)` |\n * | values from OUTSIDE this process | exactly what you re-state, nothing inherited | `RequestContext.runDetachedScope(fn)` |\n *\n * Row 1 is a faithful re-root of a broken async chain — the work continues AS that user. Row 2 is a\n * deliberate PRIVILEGE DROP: a background job keeps `requestId`/`actionId` so it stays greppable, and\n * loses `userId`/`orgId`/roles because it runs as the system (see {@link withoutTrusted}). Row 3 is a\n * different question entirely — the values were never in a scope here, so there is no snapshot to take;\n * see `RequestContext.runDetachedScope`, where each value is written inside the closure with the trust\n * verbs.\n *\n * There is NO bare form of rows 1 and 2. `copyContext()` hands back a `CapturedContext`, and\n * `runWithContext`/`restoreContext` do not accept one — they take the {@link RestorableContext} that\n * {@link withTrusted} and {@link withoutTrusted} produce. Every call site therefore STATES whether the\n * proven identity travels, and neither intent is shorter to type than the other. That is CLAUDE.md shim\n * shape #5 applied here: a bare snapshot silently carrying a user identity is a widening that is an\n * absence rather than a token, and it is ungreppable. Now `grep -rn withTrusted` enumerates every place\n * an identity crosses a scope boundary and `grep -rn withoutTrusted` every deliberate drop.\n *\n * ## What it is for\n *\n * `AsyncLocalStorage` follows `await`, `.then()` and ordinary callbacks on its own, so the vast\n * majority of code never needs this. What it does NOT follow is work whose async chain was BROKEN and\n * re-rooted somewhere else: an item pushed onto an in-memory queue during a request and drained later\n * by a background loop, a batch flushed on a scheduler tick, an `EventEmitter` listener fired from a\n * socket the request does not own, a retry re-armed from a top-level timer, a hand-off to a worker\n * pool. In each of those the work executes under a DIFFERENT (or no) context, so the request id, the\n * log fields and the proven identity would silently vanish from everything the work logs or calls.\n *\n * The answer is two halves: `RequestContext.copyContext()` where the work is ENQUEUED, and\n * `RequestContext.runWithContext(captured.withTrusted(), fn)` — or `.withoutTrusted()`, or\n * `restoreContext(...)` of either — where it RUNS. The narrowing is not optional; see the table above.\n *\n * ## Why it is opaque instead of a `Map<string, unknown>`\n *\n * A restored context legitimately contains TRUSTED values — reinstating what the original scope had\n * proven is the entire point — so the restore side cannot type-check its payload the way\n * `putTrusted`/`getTrusted` do. That left the `Map`-taking signature that this class DELETED (a\n * now-removed `setContext(map)`, and `runWithContext(map, fn)`) as a complete bypass of the trust\n * system: handing it `new Map([['userId', 'victim']])` forged a proven identity in one line, without\n * ever typing a trust verb, and the only thing standing against it was a doc comment saying \"the Map\n * must come from copyContext()\". An agent picks whatever compiles, so a doc comment is not\n * enforcement.\n *\n * Making the PAYLOAD opaque solves it without type-checking the contents: the only way to obtain one is\n * a real capture of a real scope, so whatever it holds was, by construction, already in a context that\n * something legitimately wrote. Concretely:\n *\n * - the constructor is `private`, and there is no public factory — {@link capture} demands a\n * {@link ContextCaptureAuthority} that cannot be named outside this package;\n * - the package barrel exports this class as a TYPE ONLY, so a consumer never receives the class\n * object at all and cannot reach `capture` even with a cast. That second half matters: a token whose\n * TYPE is unexported still stops nothing on its own, because `capture(null as never, forgedMap)`\n * type-checks. Withholding the class object is what actually closes it;\n * - the entries live in `#entries`, a genuine ECMAScript private field, so they are unreachable at\n * RUNTIME as well as at compile time — no `Object.keys`, no cast, no index signature;\n * - the map is defensively copied ON CAPTURE, again on each NARROWING, and again ON RESTORE, so a\n * caller who still holds the live context (or who keeps writing to it after capturing) cannot reach\n * through the snapshot, narrowing never mutates the capture it came from, and a snapshot can be\n * restored repeatedly without the first restore's mutations bleeding into the second.\n *\n * There is deliberately no reader: nothing hands the entries back out. That is why `getAll()` is gone\n * rather than re-typed to return one of these — a `CapturedContext` you cannot read is useless as a\n * `getAll`, and a readable one would be the raw enumeration of every trusted value all over again.\n *\n * The one residual: a consumer holding a NARROWED snapshot can still cast a token into\n * {@link RestorableContext.toFreshStore} and read the entries back out as a plain Map. That is knowingly accepted, and it is the same asymmetry\n * `RequestContext.getAny` states — FORGING a trusted value is the dangerous direction and is closed\n * here; reading one you were already legitimately handed, without saying `getTrusted`, costs you\n * nothing but the type. Closing it too would mean no method could take the token at all, which is to\n * say no restore could exist.\n */\nexport class CapturedContext {\n /**\n * A real ECMAScript private field, not a TypeScript `private`. The distinction matters here: `#`\n * is enforced by the runtime, so the snapshot's contents cannot be reached by a cast, by\n * `Object.entries`, or by `as unknown as { entries: Map<string, unknown> }`.\n */\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; each ContextKey carries its own value type and the typed verbs re-apply it on read\n readonly #entries: Map<string, unknown>;\n\n /**\n * PRIVATE — a CapturedContext can only come from {@link capture}, which in turn can only be called\n * by code holding a {@link ContextCaptureAuthority}. Copies the map so the snapshot is never a\n * window onto a live store.\n */\n // webpieces-disable no-any-unknown -- see #entries\n private constructor(entries: Map<string, unknown>) {\n this.#entries = new Map(entries);\n }\n\n /**\n * The ONLY producer. `authority` is a compile-time capability, not a runtime check — so it is\n * referenced below only to keep it from being an unused parameter. Note the token alone is not the\n * guarantee (a cast can supply one); the guarantee is that the barrel exports this class as a TYPE\n * ONLY, so no consumer ever holds the class object this static hangs off. See the class doc.\n */\n // webpieces-disable no-any-unknown -- see #entries\n // webpieces-disable no-function-outside-class -- static factory standing in for the (private) constructor; making it an instance method would mean an instance already existed, which is the thing being created\n static capture(authority: ContextCaptureAuthority, live: Map<string, unknown>): CapturedContext {\n void authority;\n return new CapturedContext(live);\n }\n\n /**\n * Carry EVERY value onward, the proven identity included — the faithful re-root. The work runs AS\n * that user: `getTrusted(USER_ID)` inside it answers exactly what it answered in the original scope,\n * which is the entire point when a request's own continuation was re-rooted onto a queue or a timer.\n *\n * Said OUT LOUD, because it is the wide branch. A bare snapshot is deliberately not accepted by\n * `runWithContext`/`restoreContext` (see the class doc), so the identity never crosses a scope\n * boundary by default or by omission, and `grep -rn withTrusted` enumerates every place it does.\n *\n * NON-MUTATING, like its sibling — the receiver is unchanged, so ONE snapshot can be narrowed both\n * ways at two different call sites.\n */\n withTrusted(): RestorableContext {\n return RestorableContext.of(ContextCaptureAuthority.INTERNAL, this.#entries);\n }\n\n /**\n * Carry only the UNTRUSTED values — a deliberate PRIVILEGE DROP.\n *\n * ```typescript\n * const snapshot = RequestContext.copyContext();\n * RequestContext.runWithContext(snapshot.withTrusted(), fn); // runs AS that user\n * RequestContext.runWithContext(snapshot.withoutTrusted(), fn); // runs as the SYSTEM\n * ```\n *\n * The case: a background job or fire-and-forget task spawned during a request should keep the\n * untrusted trace fields — `requestId`, `actionId` — so its log lines are still greppable back to\n * the click that caused them, but it must NOT keep `userId` / `orgId` / roles, because it executes\n * as the system rather than as that user. Carrying the proven identity onward would make every\n * downstream authorization decision think the user is still on the other end of the wire.\n *\n * A METHOD PAIR, never a `keepTrusted: boolean` on {@link RequestContext.runWithContext}: a\n * parameter makes the two intents equally easy to type and impossible to grep, and a defaulted one\n * makes the permissive branch the shortest thing to write — CLAUDE.md shim shape #5, \"a widening\n * that is an ABSENCE rather than a token\", the same reason `@AuthJwt({allRolesAllowed: true})` says\n * the wide grant out loud. As a transform on the SNAPSHOT rather than a second capture mechanism it\n * composes with BOTH consumers — `runWithContext` and `restoreContext` — for free.\n *\n * NON-MUTATING: the receiver is untouched, so one snapshot can be used both ways.\n *\n * NO AUTHORITY TOKEN, deliberately, and it must not grow one. The token on {@link capture} exists\n * because CONSTRUCTING a snapshot from arbitrary entries forges trust. This direction only ever\n * REMOVES entries: whatever survives was already in a real capture of a real scope, so the result\n * is strictly less privileged than the object the caller is already holding. Dropping cannot forge.\n *\n * WHAT SURVIVES is exactly \"registered as an UNTRUSTED {@link ContextKey}\". Trusted keys go, and so\n * do names the {@link HeaderRegistry} does not know — the framework's reserved slots (the\n * `HttpRequest`, the AuthFilter principal, the Cloud Tasks schedule frame), which carry no declared\n * trust and are the caller's identity and connection rather than trace fields. A privilege drop\n * that guessed in the permissive direction would not be one. For the same reason, with no registry\n * configured NOTHING is knowably untrusted and the result is empty — always the safe answer, since\n * this method's only job is to remove.\n */\n withoutTrusted(): RestorableContext {\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; see #entries\n const kept = new Map<string, unknown>();\n if (!HeaderRegistry.isConfigured()) {\n return RestorableContext.of(ContextCaptureAuthority.INTERNAL, kept);\n }\n const registry = HeaderRegistry.get();\n for (const name of this.#entries.keys()) {\n const key = registry.findByName(name);\n if (key && key.isUntrusted()) {\n kept.set(name, this.#entries.get(name));\n }\n }\n return RestorableContext.of(ContextCaptureAuthority.INTERNAL, kept);\n }\n\n /**\n * How many entries the snapshot holds. The one thing it will tell you about itself — a count is\n * not a value, so it leaks nothing, and it lets a caller (and a test) see that a capture taken\n * outside an active scope is simply empty rather than an error.\n */\n size(): number {\n return this.#entries.size;\n }\n}\n\n/**\n * A snapshot whose TRUST INTENT has been stated — the only thing `RequestContext.restoreContext` and\n * `RequestContext.runWithContext` accept.\n *\n * It exists to make the wide choice unskippable. A single type would have meant\n * `runWithContext(snapshot, fn)` compiling next to `runWithContext(snapshot.withTrusted(), fn)`: two\n * spellings of one thing (shim shape #1), with the shorter one silently carrying a user identity into\n * work that may have no business running as that user. Splitting the type deletes the default — a\n * capture is inert until it says which it means — so there is exactly one spelling per intent, and\n * `grep -rn withTrusted` / `grep -rn withoutTrusted` enumerate the two populations of call sites.\n *\n * Two CLASSES rather than a phantom type parameter on {@link CapturedContext}, even though this repo\n * uses that trick on `ContextKey<V, T extends Trust>`. There the parameter rides along with a key that\n * consumers name constantly and read values through, so it earns its complexity. Here the two states\n * have DIFFERENT MEMBERS — a capture can only be narrowed, a narrowed one can only be restored — and a\n * type that changes its members between states is a second class, not a second type argument. Naming\n * it also gives the field/queue-entry type a consumer holds a name that says what it is.\n *\n * The #622 opacity guarantees are unchanged and are why this class is also barrel-exported as a TYPE\n * ONLY: private constructor, a capability token on the producer, a real `#entries` private field, and\n * defensive copies on the way in and on the way out.\n */\nexport class RestorableContext {\n /** Same real ECMAScript private field, for the same reason — see {@link CapturedContext}. */\n // webpieces-disable no-any-unknown -- the context store is deliberately type-erased; each ContextKey carries its own value type and the typed verbs re-apply it on read\n readonly #entries: Map<string, unknown>;\n\n /** PRIVATE — {@link of} is the only producer, and only this module can call it. */\n // webpieces-disable no-any-unknown -- see #entries\n private constructor(entries: Map<string, unknown>) {\n this.#entries = new Map(entries);\n }\n\n /**\n * The ONLY producer, called by `CapturedContext.withTrusted()` / `withoutTrusted()`. Guarded the\n * same way `capture` is: a token no consumer can name, and a barrel that exports this class as a\n * TYPE ONLY, so the class object — and with it this static — never reaches a consumer at all.\n */\n // webpieces-disable no-any-unknown -- see #entries\n // webpieces-disable no-function-outside-class -- static factory standing in for the (private) constructor; an instance method would presuppose the instance being created\n static of(authority: ContextCaptureAuthority, entries: Map<string, unknown>): RestorableContext {\n void authority;\n return new RestorableContext(entries);\n }\n\n /**\n * Overwrite a LIVE store with this snapshot — the engine behind `RequestContext.restoreContext`.\n * Write-only by design: it pushes entries in and hands nothing back, so it is not a side door onto\n * the snapshot's contents.\n */\n // webpieces-disable no-any-unknown -- see #entries\n restoreInto(authority: ContextCaptureAuthority, live: Map<string, unknown>): void {\n void authority;\n live.clear();\n for (const name of this.#entries.keys()) {\n live.set(name, this.#entries.get(name));\n }\n }\n\n /**\n * A FRESH store holding this snapshot — the engine behind `RequestContext.runWithContext`, which\n * opens a new AsyncLocalStorage scope around it. Fresh (a copy) rather than the internal map, so\n * everything the restored scope writes stays in that scope and the snapshot remains reusable.\n */\n // webpieces-disable no-any-unknown -- see #entries\n toFreshStore(authority: ContextCaptureAuthority): Map<string, unknown> {\n void authority;\n return new Map(this.#entries);\n }\n\n /** How many entries survived the narrowing. A count is not a value, so it leaks nothing. */\n size(): number {\n return this.#entries.size;\n }\n}\n"]}
|
|
@@ -26,6 +26,27 @@ export declare class CapturedContextCompileAssertions {
|
|
|
26
26
|
cannotMintAnAuthority(): void;
|
|
27
27
|
/** And the factory is not callable without one at all. */
|
|
28
28
|
cannotCaptureWithoutAnAuthority(): void;
|
|
29
|
+
/**
|
|
30
|
+
* THE new hole this pair closes: a BARE capture must not run. If it did, `runWithContext(snapshot,
|
|
31
|
+
* fn)` would sit next to `runWithContext(snapshot.withTrusted(), fn)` as a second spelling whose
|
|
32
|
+
* shorter form silently carries a user identity — a widening that is an absence rather than a
|
|
33
|
+
* token, and ungreppable. The capture is inert until it states its intent.
|
|
34
|
+
*/
|
|
35
|
+
cannotRunABareCapture(): void;
|
|
36
|
+
/** Same, through the in-place door. */
|
|
37
|
+
cannotRestoreABareCapture(): void;
|
|
38
|
+
/**
|
|
39
|
+
* And the intent is stated by NARROWING, never by a flag on the run call. A `keepTrusted: boolean`
|
|
40
|
+
* would make the wide intent as easy to type as the narrow one and impossible to grep; this line
|
|
41
|
+
* fails the build the day such a parameter appears.
|
|
42
|
+
*/
|
|
43
|
+
cannotStateTheIntentViaAFlagOnTheRunCall(): void;
|
|
44
|
+
/** Nor can a narrowed snapshot be minted directly — same private constructor, same token. */
|
|
45
|
+
cannotConstructARestorableContextDirectly(): void;
|
|
46
|
+
/** Nor through its factory, which demands the same unobtainable authority. */
|
|
47
|
+
cannotMintARestorableContextWithoutAnAuthority(): void;
|
|
48
|
+
/** POSITIVE: both narrowings produce the ONE type both consumers take. */
|
|
49
|
+
bothNarrowingsFeedBothConsumers(): void;
|
|
29
50
|
/** POSITIVE: the real round trip must keep compiling — restoring a proven value IS the point. */
|
|
30
51
|
theRealRoundTripCompiles(): void;
|
|
31
52
|
}
|
|
@@ -52,9 +52,56 @@ class CapturedContextCompileAssertions {
|
|
|
52
52
|
// @ts-expect-error - capture() requires a ContextCaptureAuthority as its first argument
|
|
53
53
|
CapturedContext_1.CapturedContext.capture(new Map([['userId', 'victim']]));
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* THE new hole this pair closes: a BARE capture must not run. If it did, `runWithContext(snapshot,
|
|
57
|
+
* fn)` would sit next to `runWithContext(snapshot.withTrusted(), fn)` as a second spelling whose
|
|
58
|
+
* shorter form silently carries a user identity — a widening that is an absence rather than a
|
|
59
|
+
* token, and ungreppable. The capture is inert until it states its intent.
|
|
60
|
+
*/
|
|
61
|
+
cannotRunABareCapture() {
|
|
62
|
+
const captured = RequestContext_1.RequestContext.copyContext();
|
|
63
|
+
// @ts-expect-error - runWithContext takes a RestorableContext: say withTrusted() or withoutTrusted()
|
|
64
|
+
RequestContext_1.RequestContext.runWithContext(captured, () => undefined);
|
|
65
|
+
}
|
|
66
|
+
/** Same, through the in-place door. */
|
|
67
|
+
cannotRestoreABareCapture() {
|
|
68
|
+
const captured = RequestContext_1.RequestContext.copyContext();
|
|
69
|
+
// @ts-expect-error - restoreContext takes a RestorableContext: say withTrusted() or withoutTrusted()
|
|
70
|
+
RequestContext_1.RequestContext.restoreContext(captured);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* And the intent is stated by NARROWING, never by a flag on the run call. A `keepTrusted: boolean`
|
|
74
|
+
* would make the wide intent as easy to type as the narrow one and impossible to grep; this line
|
|
75
|
+
* fails the build the day such a parameter appears.
|
|
76
|
+
*/
|
|
77
|
+
cannotStateTheIntentViaAFlagOnTheRunCall() {
|
|
78
|
+
const captured = RequestContext_1.RequestContext.copyContext().withTrusted();
|
|
79
|
+
// @ts-expect-error - there is no third parameter; narrow the snapshot instead
|
|
80
|
+
RequestContext_1.RequestContext.runWithContext(captured, () => undefined, false);
|
|
81
|
+
}
|
|
82
|
+
/** Nor can a narrowed snapshot be minted directly — same private constructor, same token. */
|
|
83
|
+
cannotConstructARestorableContextDirectly() {
|
|
84
|
+
// @ts-expect-error - the constructor is private; withTrusted()/withoutTrusted() are the producers
|
|
85
|
+
const forged = new CapturedContext_1.RestorableContext(new Map([['userId', 'victim']]));
|
|
86
|
+
void forged;
|
|
87
|
+
}
|
|
88
|
+
/** Nor through its factory, which demands the same unobtainable authority. */
|
|
89
|
+
cannotMintARestorableContextWithoutAnAuthority() {
|
|
90
|
+
// @ts-expect-error - of() requires a ContextCaptureAuthority as its first argument
|
|
91
|
+
CapturedContext_1.RestorableContext.of(new Map([['userId', 'victim']]));
|
|
92
|
+
}
|
|
93
|
+
/** POSITIVE: both narrowings produce the ONE type both consumers take. */
|
|
94
|
+
bothNarrowingsFeedBothConsumers() {
|
|
95
|
+
const captured = RequestContext_1.RequestContext.copyContext();
|
|
96
|
+
const wide = captured.withTrusted();
|
|
97
|
+
const dropped = captured.withoutTrusted();
|
|
98
|
+
RequestContext_1.RequestContext.runWithContext(wide, () => {
|
|
99
|
+
RequestContext_1.RequestContext.restoreContext(dropped);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
55
102
|
/** POSITIVE: the real round trip must keep compiling — restoring a proven value IS the point. */
|
|
56
103
|
theRealRoundTripCompiles() {
|
|
57
|
-
const captured = RequestContext_1.RequestContext.copyContext();
|
|
104
|
+
const captured = RequestContext_1.RequestContext.copyContext().withTrusted();
|
|
58
105
|
RequestContext_1.RequestContext.runWithContext(captured, () => {
|
|
59
106
|
RequestContext_1.RequestContext.restoreContext(captured);
|
|
60
107
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CapturedContextCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/CapturedContextCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"CapturedContextCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/CapturedContextCompileAssertions.ts"],"names":[],"mappings":"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","sourcesContent":["import { CapturedContext, ContextCaptureAuthority, RestorableContext } from './CapturedContext';\nimport { RequestContext } from './RequestContext';\n\n/**\n * COMPILE-TIME assertions that a context can only be restored from a REAL capture.\n *\n * The runtime half — that a snapshot round-trips, and that mutating the source afterwards does not\n * reach it — is in `CapturedContext.spec.ts`. What a spec CANNOT express is the half that matters\n * most: that `RequestContext.restoreContext(new Map([['userId', 'victim']]))` does not compile. Each\n * `@ts-expect-error` below fails the build with TS2578 the day its line starts compiling again.\n *\n * In COMPILED source deliberately — `tsconfig.lib.json` excludes specs and vitest strips types with\n * esbuild, so a `@ts-expect-error` in a `.spec.ts` is inert and the suite would pass either way. See\n * `RequestContextTrustCompileAssertions` for the trust-verb half.\n */\nexport class CapturedContextCompileAssertions {\n /** THE hole this change closes: a hand-assembled payload forging a trusted value. */\n cannotRestoreAHandBuiltMap(): void {\n // @ts-expect-error - restoreContext takes an opaque CapturedContext, never a raw Map\n RequestContext.restoreContext(new Map([['userId', 'victim']]));\n }\n\n /** Same hole through the other door. */\n cannotRunWithAHandBuiltMap(): void {\n // @ts-expect-error - runWithContext takes an opaque CapturedContext, never a raw Map\n RequestContext.runWithContext(new Map([['userId', 'victim']]), () => undefined);\n }\n\n /** Nor by asserting an object literal into the shape — the private `#entries` makes it nominal. */\n cannotFakeTheShape(): void {\n // @ts-expect-error - an object literal is not a CapturedContext; #entries is not satisfiable\n const forged: CapturedContext = { size: () => 1 };\n void forged;\n }\n\n /** Nor by constructing one directly — the constructor is private. */\n cannotConstructOneDirectly(): void {\n // @ts-expect-error - the constructor is private; copyContext() is the only producer\n const forged = new CapturedContext(new Map([['userId', 'victim']]));\n void forged;\n }\n\n /**\n * Nor by calling the factory: it demands a capability token whose own constructor is private, so\n * even code that can NAME the token (this package) cannot mint a second one.\n */\n cannotMintAnAuthority(): void {\n // @ts-expect-error - ContextCaptureAuthority's constructor is private; INTERNAL is the only one\n const forgedAuthority = new ContextCaptureAuthority();\n void forgedAuthority;\n }\n\n /** And the factory is not callable without one at all. */\n cannotCaptureWithoutAnAuthority(): void {\n // @ts-expect-error - capture() requires a ContextCaptureAuthority as its first argument\n CapturedContext.capture(new Map([['userId', 'victim']]));\n }\n\n /**\n * THE new hole this pair closes: a BARE capture must not run. If it did, `runWithContext(snapshot,\n * fn)` would sit next to `runWithContext(snapshot.withTrusted(), fn)` as a second spelling whose\n * shorter form silently carries a user identity — a widening that is an absence rather than a\n * token, and ungreppable. The capture is inert until it states its intent.\n */\n cannotRunABareCapture(): void {\n const captured: CapturedContext = RequestContext.copyContext();\n // @ts-expect-error - runWithContext takes a RestorableContext: say withTrusted() or withoutTrusted()\n RequestContext.runWithContext(captured, () => undefined);\n }\n\n /** Same, through the in-place door. */\n cannotRestoreABareCapture(): void {\n const captured: CapturedContext = RequestContext.copyContext();\n // @ts-expect-error - restoreContext takes a RestorableContext: say withTrusted() or withoutTrusted()\n RequestContext.restoreContext(captured);\n }\n\n /**\n * And the intent is stated by NARROWING, never by a flag on the run call. A `keepTrusted: boolean`\n * would make the wide intent as easy to type as the narrow one and impossible to grep; this line\n * fails the build the day such a parameter appears.\n */\n cannotStateTheIntentViaAFlagOnTheRunCall(): void {\n const captured: RestorableContext = RequestContext.copyContext().withTrusted();\n // @ts-expect-error - there is no third parameter; narrow the snapshot instead\n RequestContext.runWithContext(captured, () => undefined, false);\n }\n\n /** Nor can a narrowed snapshot be minted directly — same private constructor, same token. */\n cannotConstructARestorableContextDirectly(): void {\n // @ts-expect-error - the constructor is private; withTrusted()/withoutTrusted() are the producers\n const forged = new RestorableContext(new Map([['userId', 'victim']]));\n void forged;\n }\n\n /** Nor through its factory, which demands the same unobtainable authority. */\n cannotMintARestorableContextWithoutAnAuthority(): void {\n // @ts-expect-error - of() requires a ContextCaptureAuthority as its first argument\n RestorableContext.of(new Map([['userId', 'victim']]));\n }\n\n /** POSITIVE: both narrowings produce the ONE type both consumers take. */\n bothNarrowingsFeedBothConsumers(): void {\n const captured: CapturedContext = RequestContext.copyContext();\n const wide: RestorableContext = captured.withTrusted();\n const dropped: RestorableContext = captured.withoutTrusted();\n RequestContext.runWithContext(wide, () => {\n RequestContext.restoreContext(dropped);\n });\n }\n\n /** POSITIVE: the real round trip must keep compiling — restoring a proven value IS the point. */\n theRealRoundTripCompiles(): void {\n const captured: RestorableContext = RequestContext.copyContext().withTrusted();\n RequestContext.runWithContext(captured, () => {\n RequestContext.restoreContext(captured);\n });\n }\n}\n"]}
|