@webpieces/core-context 0.4.605 → 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 +231 -0
- package/src/CapturedContext.js +296 -0
- package/src/CapturedContext.js.map +1 -0
- package/src/CapturedContextCompileAssertions.d.ts +52 -0
- package/src/CapturedContextCompileAssertions.js +111 -0
- package/src/CapturedContextCompileAssertions.js.map +1 -0
- 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 +117 -20
- package/src/RequestContext.js +131 -34
- 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 +2 -0
- package/src/index.js.map +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CapturedContextCompileAssertions = void 0;
|
|
4
|
+
const CapturedContext_1 = require("./CapturedContext");
|
|
5
|
+
const RequestContext_1 = require("./RequestContext");
|
|
6
|
+
/**
|
|
7
|
+
* COMPILE-TIME assertions that a context can only be restored from a REAL capture.
|
|
8
|
+
*
|
|
9
|
+
* The runtime half — that a snapshot round-trips, and that mutating the source afterwards does not
|
|
10
|
+
* reach it — is in `CapturedContext.spec.ts`. What a spec CANNOT express is the half that matters
|
|
11
|
+
* most: that `RequestContext.restoreContext(new Map([['userId', 'victim']]))` does not compile. Each
|
|
12
|
+
* `@ts-expect-error` below fails the build with TS2578 the day its line starts compiling again.
|
|
13
|
+
*
|
|
14
|
+
* In COMPILED source deliberately — `tsconfig.lib.json` excludes specs and vitest strips types with
|
|
15
|
+
* esbuild, so a `@ts-expect-error` in a `.spec.ts` is inert and the suite would pass either way. See
|
|
16
|
+
* `RequestContextTrustCompileAssertions` for the trust-verb half.
|
|
17
|
+
*/
|
|
18
|
+
class CapturedContextCompileAssertions {
|
|
19
|
+
/** THE hole this change closes: a hand-assembled payload forging a trusted value. */
|
|
20
|
+
cannotRestoreAHandBuiltMap() {
|
|
21
|
+
// @ts-expect-error - restoreContext takes an opaque CapturedContext, never a raw Map
|
|
22
|
+
RequestContext_1.RequestContext.restoreContext(new Map([['userId', 'victim']]));
|
|
23
|
+
}
|
|
24
|
+
/** Same hole through the other door. */
|
|
25
|
+
cannotRunWithAHandBuiltMap() {
|
|
26
|
+
// @ts-expect-error - runWithContext takes an opaque CapturedContext, never a raw Map
|
|
27
|
+
RequestContext_1.RequestContext.runWithContext(new Map([['userId', 'victim']]), () => undefined);
|
|
28
|
+
}
|
|
29
|
+
/** Nor by asserting an object literal into the shape — the private `#entries` makes it nominal. */
|
|
30
|
+
cannotFakeTheShape() {
|
|
31
|
+
// @ts-expect-error - an object literal is not a CapturedContext; #entries is not satisfiable
|
|
32
|
+
const forged = { size: () => 1 };
|
|
33
|
+
void forged;
|
|
34
|
+
}
|
|
35
|
+
/** Nor by constructing one directly — the constructor is private. */
|
|
36
|
+
cannotConstructOneDirectly() {
|
|
37
|
+
// @ts-expect-error - the constructor is private; copyContext() is the only producer
|
|
38
|
+
const forged = new CapturedContext_1.CapturedContext(new Map([['userId', 'victim']]));
|
|
39
|
+
void forged;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Nor by calling the factory: it demands a capability token whose own constructor is private, so
|
|
43
|
+
* even code that can NAME the token (this package) cannot mint a second one.
|
|
44
|
+
*/
|
|
45
|
+
cannotMintAnAuthority() {
|
|
46
|
+
// @ts-expect-error - ContextCaptureAuthority's constructor is private; INTERNAL is the only one
|
|
47
|
+
const forgedAuthority = new CapturedContext_1.ContextCaptureAuthority();
|
|
48
|
+
void forgedAuthority;
|
|
49
|
+
}
|
|
50
|
+
/** And the factory is not callable without one at all. */
|
|
51
|
+
cannotCaptureWithoutAnAuthority() {
|
|
52
|
+
// @ts-expect-error - capture() requires a ContextCaptureAuthority as its first argument
|
|
53
|
+
CapturedContext_1.CapturedContext.capture(new Map([['userId', 'victim']]));
|
|
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
|
+
}
|
|
102
|
+
/** POSITIVE: the real round trip must keep compiling — restoring a proven value IS the point. */
|
|
103
|
+
theRealRoundTripCompiles() {
|
|
104
|
+
const captured = RequestContext_1.RequestContext.copyContext().withTrusted();
|
|
105
|
+
RequestContext_1.RequestContext.runWithContext(captured, () => {
|
|
106
|
+
RequestContext_1.RequestContext.restoreContext(captured);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.CapturedContextCompileAssertions = CapturedContextCompileAssertions;
|
|
111
|
+
//# sourceMappingURL=CapturedContextCompileAssertions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AnyContextKey } from '@webpieces/core-util';
|
|
2
|
+
/**
|
|
3
|
+
* COMPILE-TIME assertions for {@link RequestContext.runDetachedScope}.
|
|
4
|
+
*
|
|
5
|
+
* The runtime half — that the detached scope starts empty, that the enclosing scope survives, that a
|
|
6
|
+
* throw unwinds it — is in `DetachedScope.spec.ts`. What a spec CANNOT express is the half that makes
|
|
7
|
+
* the browser-log path safe BY CONSTRUCTION rather than by remembering to filter:
|
|
8
|
+
*
|
|
9
|
+
* - `runDetachedScope` has NO container-taking form, so the deleted `runWithContext(map, fn)` forgery
|
|
10
|
+
* path cannot come back through this door;
|
|
11
|
+
* - a loop over a mixed `AnyContextKey[]` cannot write ANY key until it has tested that key's trust;
|
|
12
|
+
* - and a TRUSTED key can never reach `putUntrusted`, so a browser — which proves nothing — cannot
|
|
13
|
+
* FABRICATE a proven value by naming `userId` in its payload. It is a compile error at the write,
|
|
14
|
+
* not a filter someone has to remember to write.
|
|
15
|
+
*
|
|
16
|
+
* That is about the SOURCE, not about the key. Writing a trusted key is ordinary and legitimate — see
|
|
17
|
+
* {@link putTrustedIsLegitimateInsideADetachedScope} — whenever the caller has actually proven the
|
|
18
|
+
* value. What cannot be written down is a claim of proof by code that has none.
|
|
19
|
+
*
|
|
20
|
+
* In COMPILED source deliberately — `tsconfig.lib.json` excludes specs and vitest strips types with
|
|
21
|
+
* esbuild, so a `@ts-expect-error` in a `.spec.ts` is inert and the suite would pass either way. Each
|
|
22
|
+
* one below fails the build with TS2578 the day its line starts compiling. See
|
|
23
|
+
* `CapturedContextCompileAssertions` and `RequestContextTrustCompileAssertions` for the sibling halves.
|
|
24
|
+
*/
|
|
25
|
+
export declare class DetachedScopeCompileAssertions {
|
|
26
|
+
private readonly trusted;
|
|
27
|
+
private readonly untrusted;
|
|
28
|
+
/**
|
|
29
|
+
* THE hole this whole shape exists to keep shut: no Map/object/array of entries may cross the
|
|
30
|
+
* boundary. Values are written INSIDE the closure, through the trust verbs.
|
|
31
|
+
*/
|
|
32
|
+
cannotHandItAContainerOfEntries(): void;
|
|
33
|
+
/** Nor a plain object of entries, which is the same hole spelled differently. */
|
|
34
|
+
cannotHandItAnObjectOfEntries(): void;
|
|
35
|
+
/** A key of unknown trust cannot be written AT ALL — the branch is not optional. */
|
|
36
|
+
cannotWriteAMixedKeyWithoutTestingItsTrust(key: AnyContextKey): void;
|
|
37
|
+
/**
|
|
38
|
+
* And a TRUSTED key cannot be LAUNDERED through the untrusted verb, detached or not — which is
|
|
39
|
+
* what a caller with no proof would have to do, since `putTrusted` is the only other way in and
|
|
40
|
+
* saying it is a deliberate, greppable claim.
|
|
41
|
+
*/
|
|
42
|
+
cannotLaunderATrustedKeyThroughTheUntrustedVerb(): void;
|
|
43
|
+
/**
|
|
44
|
+
* POSITIVE, and the point the negative above must not be mistaken for: writing a TRUSTED value
|
|
45
|
+
* inside a detached scope is ordinary and correct when the caller has actually proven it — the
|
|
46
|
+
* signed-webhook case (Twilio/WhatsApp proves the phone number, the app looks up the userId), or a
|
|
47
|
+
* verified JWT claim. `putTrusted` is the verb for exactly that, and a detached scope does not
|
|
48
|
+
* change it. Trust is about tamper-resistance, not secrecy: a trusted `userId` is a plain,
|
|
49
|
+
* fully-logged GUID; masking is the separate `maskInLogs` axis.
|
|
50
|
+
*/
|
|
51
|
+
putTrustedIsLegitimateInsideADetachedScope(): void;
|
|
52
|
+
/**
|
|
53
|
+
* POSITIVE, and the shape the real consumer writes: emit one browser line under a fresh scope
|
|
54
|
+
* rebuilt from an EXTERNAL payload, driven off the registry's logged keys.
|
|
55
|
+
*
|
|
56
|
+
* The `isTrusted()` skip is what a reader sees; the compiler is what enforces it. Delete the skip
|
|
57
|
+
* and `isUntrusted()` still refuses the trusted key, so the trusted branch has no write at all.
|
|
58
|
+
*/
|
|
59
|
+
theBrowserLogLoopCompiles(loggedKeys: AnyContextKey[], payload: Record<string, unknown>): void;
|
|
60
|
+
/** The return value flows through, and an async closure is a promise the caller can await. */
|
|
61
|
+
returnValuesFlowThrough(): Promise<void>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DetachedScopeCompileAssertions = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
const RequestContext_1 = require("./RequestContext");
|
|
6
|
+
/**
|
|
7
|
+
* COMPILE-TIME assertions for {@link RequestContext.runDetachedScope}.
|
|
8
|
+
*
|
|
9
|
+
* The runtime half — that the detached scope starts empty, that the enclosing scope survives, that a
|
|
10
|
+
* throw unwinds it — is in `DetachedScope.spec.ts`. What a spec CANNOT express is the half that makes
|
|
11
|
+
* the browser-log path safe BY CONSTRUCTION rather than by remembering to filter:
|
|
12
|
+
*
|
|
13
|
+
* - `runDetachedScope` has NO container-taking form, so the deleted `runWithContext(map, fn)` forgery
|
|
14
|
+
* path cannot come back through this door;
|
|
15
|
+
* - a loop over a mixed `AnyContextKey[]` cannot write ANY key until it has tested that key's trust;
|
|
16
|
+
* - and a TRUSTED key can never reach `putUntrusted`, so a browser — which proves nothing — cannot
|
|
17
|
+
* FABRICATE a proven value by naming `userId` in its payload. It is a compile error at the write,
|
|
18
|
+
* not a filter someone has to remember to write.
|
|
19
|
+
*
|
|
20
|
+
* That is about the SOURCE, not about the key. Writing a trusted key is ordinary and legitimate — see
|
|
21
|
+
* {@link putTrustedIsLegitimateInsideADetachedScope} — whenever the caller has actually proven the
|
|
22
|
+
* value. What cannot be written down is a claim of proof by code that has none.
|
|
23
|
+
*
|
|
24
|
+
* In COMPILED source deliberately — `tsconfig.lib.json` excludes specs and vitest strips types with
|
|
25
|
+
* esbuild, so a `@ts-expect-error` in a `.spec.ts` is inert and the suite would pass either way. Each
|
|
26
|
+
* one below fails the build with TS2578 the day its line starts compiling. See
|
|
27
|
+
* `CapturedContextCompileAssertions` and `RequestContextTrustCompileAssertions` for the sibling halves.
|
|
28
|
+
*/
|
|
29
|
+
class DetachedScopeCompileAssertions {
|
|
30
|
+
trusted = core_util_1.ContextKey.trusted('assertDetachedUserId', 'jwt claim `sub`');
|
|
31
|
+
untrusted = core_util_1.ContextKey.untrusted('assertDetachedActionId');
|
|
32
|
+
/**
|
|
33
|
+
* THE hole this whole shape exists to keep shut: no Map/object/array of entries may cross the
|
|
34
|
+
* boundary. Values are written INSIDE the closure, through the trust verbs.
|
|
35
|
+
*/
|
|
36
|
+
cannotHandItAContainerOfEntries() {
|
|
37
|
+
// @ts-expect-error - runDetachedScope takes ONLY a closure; there is no map-taking form
|
|
38
|
+
RequestContext_1.RequestContext.runDetachedScope(new Map([['userId', 'victim']]), () => undefined);
|
|
39
|
+
}
|
|
40
|
+
/** Nor a plain object of entries, which is the same hole spelled differently. */
|
|
41
|
+
cannotHandItAnObjectOfEntries() {
|
|
42
|
+
// @ts-expect-error - the single parameter is the closure, not a bag of values
|
|
43
|
+
RequestContext_1.RequestContext.runDetachedScope({ userId: 'victim' });
|
|
44
|
+
}
|
|
45
|
+
/** A key of unknown trust cannot be written AT ALL — the branch is not optional. */
|
|
46
|
+
cannotWriteAMixedKeyWithoutTestingItsTrust(key) {
|
|
47
|
+
RequestContext_1.RequestContext.runDetachedScope(() => {
|
|
48
|
+
// @ts-expect-error - putUntrusted needs a key KNOWN to be untrusted; AnyContextKey is mixed
|
|
49
|
+
RequestContext_1.RequestContext.putUntrusted(key, 'from-the-browser');
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* And a TRUSTED key cannot be LAUNDERED through the untrusted verb, detached or not — which is
|
|
54
|
+
* what a caller with no proof would have to do, since `putTrusted` is the only other way in and
|
|
55
|
+
* saying it is a deliberate, greppable claim.
|
|
56
|
+
*/
|
|
57
|
+
cannotLaunderATrustedKeyThroughTheUntrustedVerb() {
|
|
58
|
+
RequestContext_1.RequestContext.runDetachedScope(() => {
|
|
59
|
+
// @ts-expect-error - putUntrusted does not accept a trusted key
|
|
60
|
+
RequestContext_1.RequestContext.putUntrusted(this.trusted, 'browser-said-so');
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* POSITIVE, and the point the negative above must not be mistaken for: writing a TRUSTED value
|
|
65
|
+
* inside a detached scope is ordinary and correct when the caller has actually proven it — the
|
|
66
|
+
* signed-webhook case (Twilio/WhatsApp proves the phone number, the app looks up the userId), or a
|
|
67
|
+
* verified JWT claim. `putTrusted` is the verb for exactly that, and a detached scope does not
|
|
68
|
+
* change it. Trust is about tamper-resistance, not secrecy: a trusted `userId` is a plain,
|
|
69
|
+
* fully-logged GUID; masking is the separate `maskInLogs` axis.
|
|
70
|
+
*/
|
|
71
|
+
putTrustedIsLegitimateInsideADetachedScope() {
|
|
72
|
+
RequestContext_1.RequestContext.runDetachedScope(() => {
|
|
73
|
+
RequestContext_1.RequestContext.putTrusted(this.trusted, 'proven-out-of-band');
|
|
74
|
+
const proven = RequestContext_1.RequestContext.getTrusted(this.trusted);
|
|
75
|
+
void proven;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* POSITIVE, and the shape the real consumer writes: emit one browser line under a fresh scope
|
|
80
|
+
* rebuilt from an EXTERNAL payload, driven off the registry's logged keys.
|
|
81
|
+
*
|
|
82
|
+
* The `isTrusted()` skip is what a reader sees; the compiler is what enforces it. Delete the skip
|
|
83
|
+
* and `isUntrusted()` still refuses the trusted key, so the trusted branch has no write at all.
|
|
84
|
+
*/
|
|
85
|
+
// webpieces-disable no-any-unknown -- the EXTERNAL payload is by definition untyped JSON off the wire; that is precisely why every value below is trust-branched and typeof-checked before it is written
|
|
86
|
+
theBrowserLogLoopCompiles(loggedKeys, payload) {
|
|
87
|
+
RequestContext_1.RequestContext.runDetachedScope(() => {
|
|
88
|
+
for (const key of loggedKeys) {
|
|
89
|
+
if (key.isTrusted()) {
|
|
90
|
+
// A browser cannot vouch for a proven fact. There is no write in this branch, and
|
|
91
|
+
// no cast that could produce one.
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const value = payload[key.name];
|
|
95
|
+
if (key.isUntrusted() && typeof value === 'string') {
|
|
96
|
+
RequestContext_1.RequestContext.putUntrusted(key, value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
RequestContext_1.RequestContext.putUntrusted(this.untrusted, 'click-7');
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** The return value flows through, and an async closure is a promise the caller can await. */
|
|
103
|
+
async returnValuesFlowThrough() {
|
|
104
|
+
const sync = RequestContext_1.RequestContext.runDetachedScope(() => 'done');
|
|
105
|
+
const asyncResult = await RequestContext_1.RequestContext.runDetachedScope(async () => 'done');
|
|
106
|
+
void sync;
|
|
107
|
+
void asyncResult;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.DetachedScopeCompileAssertions = DetachedScopeCompileAssertions;
|
|
111
|
+
//# sourceMappingURL=DetachedScopeCompileAssertions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DetachedScopeCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/DetachedScopeCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,oDAAiE;AACjE,qDAAkD;AAElD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,8BAA8B;IACtB,OAAO,GAAG,sBAAU,CAAC,OAAO,CAAS,sBAAsB,EAAE,iBAAiB,CAAC,CAAC;IAChF,SAAS,GAAG,sBAAU,CAAC,SAAS,CAAS,wBAAwB,CAAC,CAAC;IAEpF;;;OAGG;IACH,+BAA+B;QAC3B,wFAAwF;QACxF,+BAAc,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACtF,CAAC;IAED,iFAAiF;IACjF,6BAA6B;QACzB,8EAA8E;QAC9E,+BAAc,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,oFAAoF;IACpF,0CAA0C,CAAC,GAAkB;QACzD,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,4FAA4F;YAC5F,+BAAc,CAAC,YAAY,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;OAIG;IACH,+CAA+C;QAC3C,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,gEAAgE;YAChE,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;;;;OAOG;IACH,0CAA0C;QACtC,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC;YAC9D,MAAM,MAAM,GAAuB,+BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3E,KAAK,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,yMAAyM;IACzM,yBAAyB,CAAC,UAA2B,EAAE,OAAgC;QACnF,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACjC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC3B,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;oBAClB,kFAAkF;oBAClF,kCAAkC;oBAClC,SAAS;gBACb,CAAC;gBACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBAChC,IAAI,GAAG,CAAC,WAAW,EAAE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;oBACjD,+BAAc,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBAC5C,CAAC;YACL,CAAC;YACD,+BAAc,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;IACP,CAAC;IAED,8FAA8F;IAC9F,KAAK,CAAC,uBAAuB;QACzB,MAAM,IAAI,GAAW,+BAAc,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;QACnE,MAAM,WAAW,GAAW,MAAM,+BAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;QACtF,KAAK,IAAI,CAAC;QACV,KAAK,WAAW,CAAC;IACrB,CAAC;CACJ;AAvFD,wEAuFC","sourcesContent":["import { AnyContextKey, ContextKey } from '@webpieces/core-util';\nimport { RequestContext } from './RequestContext';\n\n/**\n * COMPILE-TIME assertions for {@link RequestContext.runDetachedScope}.\n *\n * The runtime half — that the detached scope starts empty, that the enclosing scope survives, that a\n * throw unwinds it — is in `DetachedScope.spec.ts`. What a spec CANNOT express is the half that makes\n * the browser-log path safe BY CONSTRUCTION rather than by remembering to filter:\n *\n * - `runDetachedScope` has NO container-taking form, so the deleted `runWithContext(map, fn)` forgery\n * path cannot come back through this door;\n * - a loop over a mixed `AnyContextKey[]` cannot write ANY key until it has tested that key's trust;\n * - and a TRUSTED key can never reach `putUntrusted`, so a browser — which proves nothing — cannot\n * FABRICATE a proven value by naming `userId` in its payload. It is a compile error at the write,\n * not a filter someone has to remember to write.\n *\n * That is about the SOURCE, not about the key. Writing a trusted key is ordinary and legitimate — see\n * {@link putTrustedIsLegitimateInsideADetachedScope} — whenever the caller has actually proven the\n * value. What cannot be written down is a claim of proof by code that has none.\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. Each\n * one below fails the build with TS2578 the day its line starts compiling. See\n * `CapturedContextCompileAssertions` and `RequestContextTrustCompileAssertions` for the sibling halves.\n */\nexport class DetachedScopeCompileAssertions {\n private readonly trusted = ContextKey.trusted<string>('assertDetachedUserId', 'jwt claim `sub`');\n private readonly untrusted = ContextKey.untrusted<string>('assertDetachedActionId');\n\n /**\n * THE hole this whole shape exists to keep shut: no Map/object/array of entries may cross the\n * boundary. Values are written INSIDE the closure, through the trust verbs.\n */\n cannotHandItAContainerOfEntries(): void {\n // @ts-expect-error - runDetachedScope takes ONLY a closure; there is no map-taking form\n RequestContext.runDetachedScope(new Map([['userId', 'victim']]), () => undefined);\n }\n\n /** Nor a plain object of entries, which is the same hole spelled differently. */\n cannotHandItAnObjectOfEntries(): void {\n // @ts-expect-error - the single parameter is the closure, not a bag of values\n RequestContext.runDetachedScope({ userId: 'victim' });\n }\n\n /** A key of unknown trust cannot be written AT ALL — the branch is not optional. */\n cannotWriteAMixedKeyWithoutTestingItsTrust(key: AnyContextKey): void {\n RequestContext.runDetachedScope(() => {\n // @ts-expect-error - putUntrusted needs a key KNOWN to be untrusted; AnyContextKey is mixed\n RequestContext.putUntrusted(key, 'from-the-browser');\n });\n }\n\n /**\n * And a TRUSTED key cannot be LAUNDERED through the untrusted verb, detached or not — which is\n * what a caller with no proof would have to do, since `putTrusted` is the only other way in and\n * saying it is a deliberate, greppable claim.\n */\n cannotLaunderATrustedKeyThroughTheUntrustedVerb(): void {\n RequestContext.runDetachedScope(() => {\n // @ts-expect-error - putUntrusted does not accept a trusted key\n RequestContext.putUntrusted(this.trusted, 'browser-said-so');\n });\n }\n\n /**\n * POSITIVE, and the point the negative above must not be mistaken for: writing a TRUSTED value\n * inside a detached scope is ordinary and correct when the caller has actually proven it — the\n * signed-webhook case (Twilio/WhatsApp proves the phone number, the app looks up the userId), or a\n * verified JWT claim. `putTrusted` is the verb for exactly that, and a detached scope does not\n * change it. Trust is about tamper-resistance, not secrecy: a trusted `userId` is a plain,\n * fully-logged GUID; masking is the separate `maskInLogs` axis.\n */\n putTrustedIsLegitimateInsideADetachedScope(): void {\n RequestContext.runDetachedScope(() => {\n RequestContext.putTrusted(this.trusted, 'proven-out-of-band');\n const proven: string | undefined = RequestContext.getTrusted(this.trusted);\n void proven;\n });\n }\n\n /**\n * POSITIVE, and the shape the real consumer writes: emit one browser line under a fresh scope\n * rebuilt from an EXTERNAL payload, driven off the registry's logged keys.\n *\n * The `isTrusted()` skip is what a reader sees; the compiler is what enforces it. Delete the skip\n * and `isUntrusted()` still refuses the trusted key, so the trusted branch has no write at all.\n */\n // webpieces-disable no-any-unknown -- the EXTERNAL payload is by definition untyped JSON off the wire; that is precisely why every value below is trust-branched and typeof-checked before it is written\n theBrowserLogLoopCompiles(loggedKeys: AnyContextKey[], payload: Record<string, unknown>): void {\n RequestContext.runDetachedScope(() => {\n for (const key of loggedKeys) {\n if (key.isTrusted()) {\n // A browser cannot vouch for a proven fact. There is no write in this branch, and\n // no cast that could produce one.\n continue;\n }\n const value = payload[key.name];\n if (key.isUntrusted() && typeof value === 'string') {\n RequestContext.putUntrusted(key, value);\n }\n }\n RequestContext.putUntrusted(this.untrusted, 'click-7');\n });\n }\n\n /** The return value flows through, and an async closure is a promise the caller can await. */\n async returnValuesFlowThrough(): Promise<void> {\n const sync: string = RequestContext.runDetachedScope(() => 'done');\n const asyncResult: string = await RequestContext.runDetachedScope(async () => 'done');\n void sync;\n void asyncResult;\n }\n}\n"]}
|
package/src/RequestContext.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ContextKey, AnyContextKey } from '@webpieces/core-util';
|
|
2
2
|
import { HttpRequest } from './HttpRequest';
|
|
3
|
+
import { CapturedContext, RestorableContext } from './CapturedContext';
|
|
3
4
|
/**
|
|
4
5
|
* Context management using AsyncLocalStorage.
|
|
5
6
|
* Similar to Java WebPieces Context class that uses ThreadLocal.
|
|
@@ -28,20 +29,100 @@ declare class RequestContextImpl {
|
|
|
28
29
|
* With this guard the setup is right or it is loud. It mirrors
|
|
29
30
|
* `RequestContextHeaders.fillFromRequest()`, which throws when there is NO active scope.
|
|
30
31
|
*
|
|
32
|
+
* If you genuinely WANT a fresh empty scope inside an active one — work that must not inherit the
|
|
33
|
+
* surrounding request's actionId/requestId — that is {@link runDetachedScope}, which says so by
|
|
34
|
+
* name. This guard exists to stop the ACCIDENTAL empty scope, not the deliberate one.
|
|
35
|
+
*
|
|
31
36
|
* @throws Error when a RequestContext is already active.
|
|
32
37
|
*/
|
|
33
38
|
run<T>(fn: () => T): T;
|
|
34
39
|
/**
|
|
35
|
-
*
|
|
36
|
-
* a
|
|
40
|
+
* Open a NEW scope pre-loaded with a snapshot — the restore half of {@link copyContext}, for work
|
|
41
|
+
* whose async chain was broken and re-rooted elsewhere (a queued job drained by a background loop,
|
|
42
|
+
* a batch flushed on a timer, an event listener fired from a socket the request does not own). See
|
|
43
|
+
* {@link CapturedContext} for the full list and for why the payload is opaque.
|
|
44
|
+
*
|
|
45
|
+
* A restored context legitimately contains TRUSTED values — reinstating what the original scope
|
|
46
|
+
* proved is the entire point — so this cannot type-check its contents the way the trust verbs do.
|
|
47
|
+
* The guarantee instead comes from the PAYLOAD: a {@link RestorableContext} can only be narrowed
|
|
48
|
+
* out of a {@link CapturedContext}, which only {@link copyContext} produces, so there is no
|
|
49
|
+
* hand-assembled Map to hand it and no way to forge one.
|
|
50
|
+
*
|
|
51
|
+
* The caller must SAY whether the proven identity travels — `snapshot.withTrusted()` (runs as that
|
|
52
|
+
* user) or `snapshot.withoutTrusted()` (runs as the system, keeping only the trace fields). A bare
|
|
53
|
+
* `CapturedContext` is deliberately not accepted; see {@link CapturedContext} for the three-case
|
|
54
|
+
* table and why the wide branch is spelled out rather than defaulted.
|
|
55
|
+
*
|
|
56
|
+
* The snapshot is copied into a fresh store, so writes inside `fn` stay inside `fn` and the
|
|
57
|
+
* snapshot stays reusable.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately NOT guarded against nesting the way {@link run} is. `run`'s guard exists because a
|
|
60
|
+
* second EMPTY scope shadowing the first is always a bug; here the inner scope is a faithful copy
|
|
61
|
+
* of a real one, which is the whole point — a worker that restores a snapshot inside a scope it
|
|
62
|
+
* opened per job is correct, not a mistake. Prefer this over {@link restoreContext} unless you
|
|
63
|
+
* specifically need the CURRENT scope overwritten in place.
|
|
64
|
+
*/
|
|
65
|
+
runWithContext<T>(captured: RestorableContext, fn: () => T): T;
|
|
66
|
+
/**
|
|
67
|
+
* Open a FRESH, EMPTY, NESTED scope. Nothing is inherited from the enclosing scope, and nothing
|
|
68
|
+
* crosses the boundary as data — every value the work runs under is WRITTEN INSIDE `fn`, through
|
|
69
|
+
* the ordinary trust-typed verbs:
|
|
70
|
+
*
|
|
71
|
+
* ```typescript
|
|
72
|
+
* RequestContext.runDetachedScope(() => {
|
|
73
|
+
* RequestContext.putUntrusted(WebpiecesCoreHeaders.ACTION_ID, line.actionId);
|
|
74
|
+
* emit(); // runs under exactly what this closure wrote, and nothing else
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* ## When you want THIS and not {@link runWithContext}
|
|
79
|
+
*
|
|
80
|
+
* The two look similar and are opposites. `runWithContext` faithfully RE-ROOTS a real snapshot of a
|
|
81
|
+
* real scope, for work whose async chain was broken (a queued job, a timer flush) — it exists to
|
|
82
|
+
* PRESERVE a context. This one exists to DISCARD one: the values do not come from any scope this
|
|
83
|
+
* process ever had, they were reconstructed from somewhere else, and inheriting the ambient scope
|
|
84
|
+
* would be actively wrong.
|
|
85
|
+
*
|
|
86
|
+
* The live case is a browser-log shipper. A batch of browser lines arrives on one HTTP request; each
|
|
87
|
+
* line carries the context the BROWSER captured when it was written, and a single batch routinely
|
|
88
|
+
* spans several user actions. Emitting a line under the shipping request's own scope would stamp
|
|
89
|
+
* every line with that request's actionId and requestId, silently destroying the ability to grep an
|
|
90
|
+
* action while the feature still appeared to work. So each line is emitted detached, under exactly
|
|
91
|
+
* the keys the closure re-stated from the browser's payload.
|
|
37
92
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
93
|
+
* ## Why it MAY nest when {@link run} may not
|
|
94
|
+
*
|
|
95
|
+
* `run`'s nesting guard is right and is not softened here. It refuses a second EMPTY scope because
|
|
96
|
+
* there an empty scope is always an ACCIDENT — a transport opening the request scope twice, whose
|
|
97
|
+
* only effect is to hide the outer scope's values and mint a second request id. Here an empty scope
|
|
98
|
+
* is the thing that was ASKED for, in a distinctly-named verb, and the caller is normally already
|
|
99
|
+
* inside a request scope (the shipper's own). A guard would refuse the only situation the method has.
|
|
100
|
+
*
|
|
101
|
+
* ## No Map-taking form, ever
|
|
102
|
+
*
|
|
103
|
+
* There is deliberately no overload accepting a `Map`, an object, or an array of entries. That was
|
|
104
|
+
* the DELETED `runWithContext(map, fn)`, and it was a forgery path: a hand-built map is
|
|
105
|
+
* indistinguishable from a genuine snapshot, so `new Map([['userId','victim']])` minted a proven
|
|
106
|
+
* identity in one line without ever typing a trust verb. Writing the values INSIDE the closure is
|
|
107
|
+
* what closes it — a loop over a mixed `AnyContextKey[]` must branch on `key.isTrusted()` before it
|
|
108
|
+
* can write anything, and `putUntrusted` does not compile for a trusted key, so code fed by a
|
|
109
|
+
* BROWSER (which proves nothing) cannot fabricate a proven value. (See
|
|
110
|
+
* `DetachedScopeCompileAssertions`.)
|
|
111
|
+
*
|
|
112
|
+
* That is a limit on the SOURCE, not on the key. `putTrusted` inside a detached scope is ordinary
|
|
113
|
+
* and correct whenever the caller has actually proven the value — a verified JWT claim, or the
|
|
114
|
+
* signed-webhook case where Twilio/WhatsApp proves the phone number and the app looks up the
|
|
115
|
+
* userId. Trust is tamper-resistance, not secrecy (a trusted `userId` is a plain, fully-logged
|
|
116
|
+
* GUID; redaction is the separate `maskInLogs` axis on the key).
|
|
117
|
+
*
|
|
118
|
+
* SYNC AND ASYNC BOTH: `fn` may return a promise, and the detached scope follows every `await`
|
|
119
|
+
* inside it exactly as `run`/`runWithContext` do — same `AsyncLocalStorage.run` underneath. The
|
|
120
|
+
* enclosing scope is reinstated for everything after the synchronous return, INCLUDING when `fn`
|
|
121
|
+
* throws (AsyncLocalStorage unwinds the store as the frame unwinds); an async `fn` that is not
|
|
122
|
+
* awaited will therefore keep the detached scope for its own continuation while the caller has
|
|
123
|
+
* already resumed under the enclosing one, which is the intended and only sane reading of "detached".
|
|
43
124
|
*/
|
|
44
|
-
|
|
125
|
+
runDetachedScope<T>(fn: () => T): T;
|
|
45
126
|
/**
|
|
46
127
|
* Read a value the framework PROVED — a verified JWT claim, or a fact an app derived from a
|
|
47
128
|
* verified credential. Does not compile for an untrusted key, so a reader can never mistake a
|
|
@@ -193,22 +274,38 @@ declare class RequestContextImpl {
|
|
|
193
274
|
*/
|
|
194
275
|
clear(): void;
|
|
195
276
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
*
|
|
277
|
+
* Snapshot this scope so the work you are about to hand off keeps its request id, log fields and
|
|
278
|
+
* proven identity. The ONLY producer of a {@link CapturedContext} — which is what makes the
|
|
279
|
+
* restore side unforgeable, since there is no other way to obtain the payload it accepts.
|
|
280
|
+
*
|
|
281
|
+
* Outside a `run(...)` block this returns an EMPTY snapshot rather than throwing: capturing "no
|
|
282
|
+
* context" is a legitimate thing for a background caller to do, and restoring it simply installs
|
|
283
|
+
* nothing.
|
|
202
284
|
*
|
|
203
|
-
*
|
|
204
|
-
* `copyContext()`. It cannot be trust-checked, because a faithful restore has to reinstate the
|
|
205
|
-
* trusted values the original scope had proven.
|
|
285
|
+
* The snapshot is a defensive COPY — writes to this context after capturing do not reach it.
|
|
206
286
|
*/
|
|
207
|
-
|
|
287
|
+
copyContext(): CapturedContext;
|
|
208
288
|
/**
|
|
209
|
-
*
|
|
289
|
+
* Overwrite the ACTIVE scope with a snapshot. The in-place twin of {@link runWithContext}, and the
|
|
290
|
+
* one you almost never want: prefer `runWithContext`, which gives the restored work its OWN scope
|
|
291
|
+
* and cannot disturb the caller's. Reach for this only when something else owns the scope and it
|
|
292
|
+
* must be re-pointed in place.
|
|
293
|
+
*
|
|
294
|
+
* OVERWRITE, not merge — `clear()` runs first, so every entry the active scope holds and the
|
|
295
|
+
* snapshot does not is DROPPED. That includes the empty case:
|
|
296
|
+
* `restoreContext(copyContext().withTrusted())` taken outside a scope wipes the request id and
|
|
297
|
+
* every proven identity from a live request, and says nothing. That is faithful (a snapshot
|
|
298
|
+
* restores exactly what it captured) but it is the sharp edge of this method and the reason
|
|
299
|
+
* `runWithContext` is the default.
|
|
300
|
+
*
|
|
301
|
+
* Takes only a {@link RestorableContext} for the reason spelled out there — the DELETED Map-taking
|
|
302
|
+
* form let `new Map([['userId','victim']])` forge a proven identity in one line — and that type
|
|
303
|
+
* exists only via `withTrusted()` / `withoutTrusted()`, so this call site states whether the proven
|
|
304
|
+
* identity survives the re-point.
|
|
305
|
+
*
|
|
306
|
+
* @throws Error when no RequestContext is active.
|
|
210
307
|
*/
|
|
211
|
-
|
|
308
|
+
restoreContext(captured: RestorableContext): void;
|
|
212
309
|
/**
|
|
213
310
|
* Check if a key exists in the context.
|
|
214
311
|
*/
|