@webpieces/core-context 0.4.606 → 0.4.608

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.
@@ -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"]}
@@ -1,6 +1,6 @@
1
1
  import { ContextKey, AnyContextKey } from '@webpieces/core-util';
2
2
  import { HttpRequest } from './HttpRequest';
3
- import { CapturedContext } from './CapturedContext';
3
+ import { CapturedContext, RestorableContext } from './CapturedContext';
4
4
  /**
5
5
  * Context management using AsyncLocalStorage.
6
6
  * Similar to Java WebPieces Context class that uses ThreadLocal.
@@ -29,6 +29,10 @@ declare class RequestContextImpl {
29
29
  * With this guard the setup is right or it is loud. It mirrors
30
30
  * `RequestContextHeaders.fillFromRequest()`, which throws when there is NO active scope.
31
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
+ *
32
36
  * @throws Error when a RequestContext is already active.
33
37
  */
34
38
  run<T>(fn: () => T): T;
@@ -40,8 +44,14 @@ declare class RequestContextImpl {
40
44
  *
41
45
  * A restored context legitimately contains TRUSTED values — reinstating what the original scope
42
46
  * proved is the entire point — so this cannot type-check its contents the way the trust verbs do.
43
- * The guarantee instead comes from the PAYLOAD: a {@link CapturedContext} can only be produced by
44
- * {@link copyContext}, so there is no hand-assembled Map to hand it and no way to forge one.
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.
45
55
  *
46
56
  * The snapshot is copied into a fresh store, so writes inside `fn` stay inside `fn` and the
47
57
  * snapshot stays reusable.
@@ -52,7 +62,67 @@ declare class RequestContextImpl {
52
62
  * opened per job is correct, not a mistake. Prefer this over {@link restoreContext} unless you
53
63
  * specifically need the CURRENT scope overwritten in place.
54
64
  */
55
- runWithContext<T>(captured: CapturedContext, fn: () => T): T;
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.
92
+ *
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".
124
+ */
125
+ runDetachedScope<T>(fn: () => T): T;
56
126
  /**
57
127
  * Read a value the framework PROVED — a verified JWT claim, or a fact an app derived from a
58
128
  * verified credential. Does not compile for an untrusted key, so a reader can never mistake a
@@ -222,17 +292,20 @@ declare class RequestContextImpl {
222
292
  * must be re-pointed in place.
223
293
  *
224
294
  * OVERWRITE, not merge — `clear()` runs first, so every entry the active scope holds and the
225
- * snapshot does not is DROPPED. That includes the empty case: `restoreContext(copyContext())`
226
- * taken outside a scope wipes the request id and every proven identity from a live request, and
227
- * says nothing. That is faithful (a snapshot restores exactly what it captured) but it is the
228
- * sharp edge of this method and the reason `runWithContext` is the default.
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.
229
300
  *
230
- * Takes only a {@link CapturedContext} for the reason spelled out there — the DELETED Map-taking
231
- * form let `new Map([['userId','victim']])` forge a proven identity in one line.
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.
232
305
  *
233
306
  * @throws Error when no RequestContext is active.
234
307
  */
235
- restoreContext(captured: CapturedContext): void;
308
+ restoreContext(captured: RestorableContext): void;
236
309
  /**
237
310
  * Check if a key exists in the context.
238
311
  */
@@ -36,13 +36,20 @@ class RequestContextImpl {
36
36
  * With this guard the setup is right or it is loud. It mirrors
37
37
  * `RequestContextHeaders.fillFromRequest()`, which throws when there is NO active scope.
38
38
  *
39
+ * If you genuinely WANT a fresh empty scope inside an active one — work that must not inherit the
40
+ * surrounding request's actionId/requestId — that is {@link runDetachedScope}, which says so by
41
+ * name. This guard exists to stop the ACCIDENTAL empty scope, not the deliberate one.
42
+ *
39
43
  * @throws Error when a RequestContext is already active.
40
44
  */
41
45
  run(fn) {
42
46
  if (this.isActive()) {
43
47
  throw new Error('RequestContext.run(...) called inside an active RequestContext. Nesting installs a ' +
44
48
  'fresh empty context that shadows the outer one: its values go invisible and a second ' +
45
- 'request id is minted. Exactly ONE scope per request — the transport opens it.');
49
+ 'request id is minted. Exactly ONE scope per request — the transport opens it. If you ' +
50
+ 'MEANT a fresh empty scope that does not inherit this one (a browser-log line, work ' +
51
+ 'reconstructed from an external payload), use RequestContext.runDetachedScope(fn) and ' +
52
+ 'write its values inside the closure with putTrusted/putUntrusted.');
46
53
  }
47
54
  // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)
48
55
  const store = new Map();
@@ -56,8 +63,14 @@ class RequestContextImpl {
56
63
  *
57
64
  * A restored context legitimately contains TRUSTED values — reinstating what the original scope
58
65
  * proved is the entire point — so this cannot type-check its contents the way the trust verbs do.
59
- * The guarantee instead comes from the PAYLOAD: a {@link CapturedContext} can only be produced by
60
- * {@link copyContext}, so there is no hand-assembled Map to hand it and no way to forge one.
66
+ * The guarantee instead comes from the PAYLOAD: a {@link RestorableContext} can only be narrowed
67
+ * out of a {@link CapturedContext}, which only {@link copyContext} produces, so there is no
68
+ * hand-assembled Map to hand it and no way to forge one.
69
+ *
70
+ * The caller must SAY whether the proven identity travels — `snapshot.withTrusted()` (runs as that
71
+ * user) or `snapshot.withoutTrusted()` (runs as the system, keeping only the trace fields). A bare
72
+ * `CapturedContext` is deliberately not accepted; see {@link CapturedContext} for the three-case
73
+ * table and why the wide branch is spelled out rather than defaulted.
61
74
  *
62
75
  * The snapshot is copied into a fresh store, so writes inside `fn` stay inside `fn` and the
63
76
  * snapshot stays reusable.
@@ -71,6 +84,70 @@ class RequestContextImpl {
71
84
  runWithContext(captured, fn) {
72
85
  return this.storage.run(captured.toFreshStore(CapturedContext_1.ContextCaptureAuthority.INTERNAL), fn);
73
86
  }
87
+ /**
88
+ * Open a FRESH, EMPTY, NESTED scope. Nothing is inherited from the enclosing scope, and nothing
89
+ * crosses the boundary as data — every value the work runs under is WRITTEN INSIDE `fn`, through
90
+ * the ordinary trust-typed verbs:
91
+ *
92
+ * ```typescript
93
+ * RequestContext.runDetachedScope(() => {
94
+ * RequestContext.putUntrusted(WebpiecesCoreHeaders.ACTION_ID, line.actionId);
95
+ * emit(); // runs under exactly what this closure wrote, and nothing else
96
+ * });
97
+ * ```
98
+ *
99
+ * ## When you want THIS and not {@link runWithContext}
100
+ *
101
+ * The two look similar and are opposites. `runWithContext` faithfully RE-ROOTS a real snapshot of a
102
+ * real scope, for work whose async chain was broken (a queued job, a timer flush) — it exists to
103
+ * PRESERVE a context. This one exists to DISCARD one: the values do not come from any scope this
104
+ * process ever had, they were reconstructed from somewhere else, and inheriting the ambient scope
105
+ * would be actively wrong.
106
+ *
107
+ * The live case is a browser-log shipper. A batch of browser lines arrives on one HTTP request; each
108
+ * line carries the context the BROWSER captured when it was written, and a single batch routinely
109
+ * spans several user actions. Emitting a line under the shipping request's own scope would stamp
110
+ * every line with that request's actionId and requestId, silently destroying the ability to grep an
111
+ * action while the feature still appeared to work. So each line is emitted detached, under exactly
112
+ * the keys the closure re-stated from the browser's payload.
113
+ *
114
+ * ## Why it MAY nest when {@link run} may not
115
+ *
116
+ * `run`'s nesting guard is right and is not softened here. It refuses a second EMPTY scope because
117
+ * there an empty scope is always an ACCIDENT — a transport opening the request scope twice, whose
118
+ * only effect is to hide the outer scope's values and mint a second request id. Here an empty scope
119
+ * is the thing that was ASKED for, in a distinctly-named verb, and the caller is normally already
120
+ * inside a request scope (the shipper's own). A guard would refuse the only situation the method has.
121
+ *
122
+ * ## No Map-taking form, ever
123
+ *
124
+ * There is deliberately no overload accepting a `Map`, an object, or an array of entries. That was
125
+ * the DELETED `runWithContext(map, fn)`, and it was a forgery path: a hand-built map is
126
+ * indistinguishable from a genuine snapshot, so `new Map([['userId','victim']])` minted a proven
127
+ * identity in one line without ever typing a trust verb. Writing the values INSIDE the closure is
128
+ * what closes it — a loop over a mixed `AnyContextKey[]` must branch on `key.isTrusted()` before it
129
+ * can write anything, and `putUntrusted` does not compile for a trusted key, so code fed by a
130
+ * BROWSER (which proves nothing) cannot fabricate a proven value. (See
131
+ * `DetachedScopeCompileAssertions`.)
132
+ *
133
+ * That is a limit on the SOURCE, not on the key. `putTrusted` inside a detached scope is ordinary
134
+ * and correct whenever the caller has actually proven the value — a verified JWT claim, or the
135
+ * signed-webhook case where Twilio/WhatsApp proves the phone number and the app looks up the
136
+ * userId. Trust is tamper-resistance, not secrecy (a trusted `userId` is a plain, fully-logged
137
+ * GUID; redaction is the separate `maskInLogs` axis on the key).
138
+ *
139
+ * SYNC AND ASYNC BOTH: `fn` may return a promise, and the detached scope follows every `await`
140
+ * inside it exactly as `run`/`runWithContext` do — same `AsyncLocalStorage.run` underneath. The
141
+ * enclosing scope is reinstated for everything after the synchronous return, INCLUDING when `fn`
142
+ * throws (AsyncLocalStorage unwinds the store as the frame unwinds); an async `fn` that is not
143
+ * awaited will therefore keep the detached scope for its own continuation while the caller has
144
+ * already resumed under the enclosing one, which is the intended and only sane reading of "detached".
145
+ */
146
+ runDetachedScope(fn) {
147
+ // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)
148
+ const store = new Map();
149
+ return this.storage.run(store, fn);
150
+ }
74
151
  /**
75
152
  * Read a value the framework PROVED — a verified JWT claim, or a fact an app derived from a
76
153
  * verified credential. Does not compile for an untrusted key, so a reader can never mistake a
@@ -355,13 +432,16 @@ class RequestContextImpl {
355
432
  * must be re-pointed in place.
356
433
  *
357
434
  * OVERWRITE, not merge — `clear()` runs first, so every entry the active scope holds and the
358
- * snapshot does not is DROPPED. That includes the empty case: `restoreContext(copyContext())`
359
- * taken outside a scope wipes the request id and every proven identity from a live request, and
360
- * says nothing. That is faithful (a snapshot restores exactly what it captured) but it is the
361
- * sharp edge of this method and the reason `runWithContext` is the default.
435
+ * snapshot does not is DROPPED. That includes the empty case:
436
+ * `restoreContext(copyContext().withTrusted())` taken outside a scope wipes the request id and
437
+ * every proven identity from a live request, and says nothing. That is faithful (a snapshot
438
+ * restores exactly what it captured) but it is the sharp edge of this method and the reason
439
+ * `runWithContext` is the default.
362
440
  *
363
- * Takes only a {@link CapturedContext} for the reason spelled out there — the DELETED Map-taking
364
- * form let `new Map([['userId','victim']])` forge a proven identity in one line.
441
+ * Takes only a {@link RestorableContext} for the reason spelled out there — the DELETED Map-taking
442
+ * form let `new Map([['userId','victim']])` forge a proven identity in one line — and that type
443
+ * exists only via `withTrusted()` / `withoutTrusted()`, so this call site states whether the proven
444
+ * identity survives the re-point.
365
445
  *
366
446
  * @throws Error when no RequestContext is active.
367
447
  */
@@ -369,7 +449,8 @@ class RequestContextImpl {
369
449
  const store = this.storage.getStore();
370
450
  if (!store) {
371
451
  throw new Error('No context available to restore into. Either open one with RequestContext.run(...) ' +
372
- 'first, or use RequestContext.runWithContext(captured, fn), which opens its own.');
452
+ 'first, or use RequestContext.runWithContext(captured.withTrusted(), fn) or ' +
453
+ '.withoutTrusted() — which opens its own.');
373
454
  }
374
455
  captured.restoreInto(CapturedContext_1.ContextCaptureAuthority.INTERNAL, store);
375
456
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RequestContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContext.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'async_hooks';\nimport { ContextKey, AnyContextKey, HeaderRegistry, ServiceInfo } from '@webpieces/core-util';\nimport { HttpRequest } from './HttpRequest';\nimport { CapturedContext, ContextCaptureAuthority } from './CapturedContext';\n\n/** Reserved context key under which the current HttpRequest is stored. */\nconst HTTP_REQUEST_KEY = '__webpieces_http_request__';\n\n/**\n * Context management using AsyncLocalStorage.\n * Similar to Java WebPieces Context class that uses ThreadLocal.\n *\n * This allows storing request-scoped data that is automatically available\n * throughout the async call chain, similar to MDC (Mapped Diagnostic Context).\n *\n * Example usage:\n * ```typescript\n * Context.put('REQUEST_ID', '12345');\n * await someAsyncOperation();\n * const id = Context.get('REQUEST_ID'); // Still available!\n * ```\n */\nclass RequestContextImpl {\n private storage: AsyncLocalStorage<Map<string, any>>;\n\n constructor() {\n this.storage = new AsyncLocalStorage<Map<string, any>>();\n }\n\n /**\n * Open THE request scope. A transport calls this once, at the beginning of a request.\n *\n * Nesting is a bug, not a feature, so it throws. AsyncLocalStorage would happily let a second\n * `run()` install a fresh empty Map that SHADOWS the outer one: every value the outer scope\n * holds becomes invisible, `fillFromRequest` mints a second request id, and the two halves of a\n * request end up in different traces. Nothing would tell you.\n *\n * With this guard the setup is right or it is loud. It mirrors\n * `RequestContextHeaders.fillFromRequest()`, which throws when there is NO active scope.\n *\n * @throws Error when a RequestContext is already active.\n */\n run<T>(fn: () => T): T {\n if (this.isActive()) {\n throw new Error(\n 'RequestContext.run(...) called inside an active RequestContext. Nesting installs a ' +\n 'fresh empty context that shadows the outer one: its values go invisible and a second ' +\n 'request id is minted. Exactly ONE scope per request — the transport opens it.',\n );\n }\n // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)\n const store = new Map<string, any>();\n return this.storage.run(store, fn);\n }\n\n /**\n * Open a NEW scope pre-loaded with a snapshot — the restore half of {@link copyContext}, for work\n * whose async chain was broken and re-rooted elsewhere (a queued job drained by a background loop,\n * a batch flushed on a timer, an event listener fired from a socket the request does not own). See\n * {@link CapturedContext} for the full list and for why the payload is opaque.\n *\n * A restored context legitimately contains TRUSTED values — reinstating what the original scope\n * proved is the entire point — so this cannot type-check its contents the way the trust verbs do.\n * The guarantee instead comes from the PAYLOAD: a {@link CapturedContext} can only be produced by\n * {@link copyContext}, so there is no hand-assembled Map to hand it and no way to forge one.\n *\n * The snapshot is copied into a fresh store, so writes inside `fn` stay inside `fn` and the\n * snapshot stays reusable.\n *\n * Deliberately NOT guarded against nesting the way {@link run} is. `run`'s guard exists because a\n * second EMPTY scope shadowing the first is always a bug; here the inner scope is a faithful copy\n * of a real one, which is the whole point — a worker that restores a snapshot inside a scope it\n * opened per job is correct, not a mistake. Prefer this over {@link restoreContext} unless you\n * specifically need the CURRENT scope overwritten in place.\n */\n runWithContext<T>(captured: CapturedContext, fn: () => T): T {\n return this.storage.run(captured.toFreshStore(ContextCaptureAuthority.INTERNAL), fn);\n }\n\n /**\n * Read a value the framework PROVED — a verified JWT claim, or a fact an app derived from a\n * verified credential. Does not compile for an untrusted key, so a reader can never mistake a\n * caller-asserted value for an authenticated one.\n *\n * This is the ONLY read that is safe to feed into an authorization decision. If you find\n * yourself wanting `getUntrusted` for that, the fix is to make the key trusted and have an\n * authenticator vouch for it — not to use the other verb.\n *\n * The return type is the key's OWN value type `V` — `string` for wire/log keys, `ApiCallInfo`\n * for the api tag, `TestCaseRecorder` for the recorder — INFERRED from the key, never asserted\n * by the caller. This is the typed public surface over the deliberately type-erased backing Map.\n */\n getTrusted<V>(key: ContextKey<V, 'trusted'>): V | undefined {\n return this.readByName<V>(key.name);\n }\n\n /**\n * Read a value a caller merely ASSERTED — a browser-minted actionId, a recording flag, an\n * in-process log tag. Does not compile for a trusted key: reading a proven fact through the\n * untrusted verb would under-claim and hide, at the call site, that the value IS reliable.\n *\n * Treat everything this returns as attacker-controlled. It is fine for logging, tracing,\n * routing hints and rate-limit bucketing; it is never an input to \"may they do this?\".\n */\n getUntrusted<V>(key: ContextKey<V, 'untrusted'>): V | undefined {\n return this.readByName<V>(key.name);\n }\n\n /**\n * Store a value the framework PROVED. A distinct, greppable verb precisely so that writing a\n * trusted value is something code has to do ON PURPOSE — `grep -rn putTrusted` lists every place\n * in the repo that claims to have proven something, which is a reviewable set.\n *\n * Callers are the framework `AuthFilter` (stamping {@link ContextTuple}s an app's JwtHook derived\n * from a verified credential) and app code that has itself verified something out-of-band — the\n * signed-webhook case: Twilio/WhatsApp proves the phone number, the app looks up the userId, and\n * that userId is every bit as proven as a JWT claim.\n *\n * Does not compile for an untrusted key.\n */\n putTrusted<V>(key: ContextKey<V, 'trusted'>, value: V): void {\n this.writeByName(key.name, value);\n }\n\n /**\n * Store a caller-asserted value. `value` is type-checked against the key's value type `V`, so you\n * cannot put a number under a `ContextKey<string>` or a raw object under a typed key.\n *\n * Does not compile for a trusted key — which is what stops the inbound-header path, the api-tag\n * seam and ordinary app code from being side doors that forge a trusted value.\n */\n putUntrusted<V>(key: ContextKey<V, 'untrusted'>, value: V): void {\n this.writeByName(key.name, value);\n }\n\n /**\n * Read a key of ANY trust level and ANY value type, as `unknown`.\n *\n * FRAMEWORK SERIALIZATION ONLY — the log-field builders below, the outbound header builder, and\n * the {@link ContextReader} seam. Those loop over `HeaderRegistry` key arrays that are mixed in\n * both value type and trust, and they are not making a trust DECISION: they are copying values to\n * a log line or to the wire.\n *\n * It is deliberately read-only and has no write twin. A `putAny` would re-open the exact hole the\n * typed verbs close, because forging a trusted value is the dangerous direction; reading one\n * without saying `getTrusted` only costs you the `unknown` return type.\n */\n // webpieces-disable no-any-unknown -- key-agnostic serialization read: the key array is mixed in value type, so unknown is the honest return\n getAny(key: AnyContextKey): unknown {\n return this.readByName<unknown>(key.name);\n }\n\n /** Clear one context key. Used by the api-tag seam's set → log → remove span (see LogApiCall). */\n removeKey(key: AnyContextKey): void {\n this.storage.getStore()?.delete(key.name);\n }\n\n hasKey(key: AnyContextKey): boolean {\n return this.storage.getStore()?.has(key.name) ?? false;\n }\n\n /**\n * Build the masked field map for LOGGING: every logged key in the global\n * {@link HeaderRegistry} read straight from this context, secured values\n * masked (via {@link ContextKey.maskForLogs}), keyed by each key's `name`.\n *\n * Callers: RecordingFilter + NodeProxyClient.recordCall, which snapshot the context into a\n * test FIXTURE. The @webpieces/winston and @webpieces/bunyan backends also stamp these fields\n * onto every record, and they own the \"log emitted outside RequestContext.run(...)\" complaint —\n * reporting it HERE would recurse (the error line itself re-enters buildLogFields).\n *\n * Returns an EMPTY map outside a `run(...)` block rather than throwing: a fixture snapshot or a\n * log line is never worth crashing a request over.\n */\n buildLogFields(): Map<string, string> {\n const fields = new Map<string, string>();\n if (!this.isActive()) {\n return fields;\n }\n // The registry owns WHICH keys log (getLoggedKeys); we read each straight from THIS context\n // and each ContextKey masks its own secured value. String-only — this map feeds wire/MDC +\n // recorder fixtures — so an object-valued key (API_CALL_INFO) is guarded out by the\n // typeof-string check; objects ride buildStructuredLogFields instead. (Was a HeaderRegistry\n // method taking a read callback; only the server ever called it, so the seam was dead weight.)\n for (const key of HeaderRegistry.get().getLoggedKeys()) {\n // getLoggedKeys() is AnyContextKey[] — mixed in BOTH value type and trust — so this reads\n // through getAny (serialization, not a trust decision) and narrows with the typeof-string\n // guard rather than asserting a value type per key.\n const value = this.getAny(key);\n if (typeof value === 'string' && value) {\n fields.set(key.name, key.maskForLogs(value));\n }\n }\n return fields;\n }\n\n /**\n * The STRUCTURED field map for the node logging backends: like {@link buildLogFields}, but values\n * may be OBJECTS, so an object-valued logged key ({@link WebpiecesCoreHeaders.API_CALL_INFO} holding\n * an {@link ApiCallInfo}) survives as an object and the winston/bunyan backends nest it into\n * `jsonPayload.api`. Reads values UNTYPED (not `<string>`) so the object comes through intact.\n *\n * Outside a `run(...)` block it returns just the `svcName` + `version` entries below (not a fully\n * empty map): a log line is never worth crashing over, and startup/background lines must still say\n * which service and build emitted them.\n *\n * PLUS this service's `svcName` and this build's `version` from {@link ServiceInfo}. Neither is a\n * {@link ContextKey} — they are process-global identity facts, added HERE (BEFORE the active-context\n * check) so EVERY log line of BOTH node backends (winston/bunyan read this one map) says which\n * service and build emitted it — request path, startup, and background jobs alike — with no\n * per-backend duplication. This is the SINGLE place both are stamped, keeping the two backends\n * symmetrical (jsonPayload.svcName + jsonPayload.version). Read via the non-throwing\n * {@link ServiceInfo.getName} / {@link ServiceInfo.getVersion}, so each is simply ABSENT until\n * `setInfo` has run — logging keeps working before the service is identified, then the fields start\n * appearing. Caller-set `svcName`/`version` headers (there are none by convention) would be\n * overwritten here; that is intentional — the ServiceInfo identity is authoritative.\n */\n buildStructuredLogFields(): Map<string, string | object> {\n const fields = new Map<string, string | object>();\n // This service's `svcName` + this build's `version` from ServiceInfo — NOT ContextKeys, they are\n // process-global identity facts. Added FIRST, BEFORE the active-context check, so they ride EVERY\n // line of both node backends (they read this one map) — including startup and background-job lines\n // emitted with NO active RequestContext. Treated identically and read per-record via the\n // non-throwing getters, so each is simply ABSENT until setInfo has run, then starts appearing —\n // even if setInfo runs after a backend was constructed. This is the ONE place both facts are\n // stamped, so winston and bunyan stay symmetrical (jsonPayload.svcName + jsonPayload.version).\n const svcName = ServiceInfo.getName();\n if (svcName) {\n fields.set('svcName', svcName);\n }\n const version = ServiceInfo.getVersion();\n if (version) {\n fields.set('version', version);\n }\n if (!this.isActive()) {\n return fields;\n }\n // Like buildLogFields, but values may be OBJECTS (API_CALL_INFO): read UNTYPED so the object\n // survives and winston/bunyan nest it into jsonPayload.<name>. Secured STRING values are still\n // masked per key; non-string primitives are ignored rather than String()-flattened. (Inlined\n // from HeaderRegistry for the same reason as buildLogFields — only the server called it.)\n for (const key of HeaderRegistry.get().getLoggedKeys()) {\n const value = this.getAny(key);\n if (value === undefined || value === null) {\n continue;\n }\n if (typeof value === 'string') {\n if (value) {\n fields.set(key.name, key.maskForLogs(value));\n }\n } else if (typeof value === 'object') {\n fields.set(key.name, value);\n }\n }\n return fields;\n }\n\n\n /**\n * Store the transport-neutral {@link HttpRequest} for this request. Called once, above the\n * api boundary, by whichever transport is driving the router (the express adapter, or the\n * in-process client). Filters/auth read it back via {@link getRequest} so they never touch\n * express — the same chain then runs over HTTP and in-process.\n */\n setRequest(request: HttpRequest): void {\n this.put(HTTP_REQUEST_KEY, request);\n }\n\n /** The current {@link HttpRequest}, or undefined if none was set for this context. */\n getRequest(): HttpRequest | undefined {\n return this.get<HttpRequest>(HTTP_REQUEST_KEY);\n }\n\n /**\n * Store a value under a RAW STRING key — the escape hatch for the framework's own reserved,\n * UNREGISTERED slots ('__webpieces_http_request__', the AuthFilter principal, the Cloud Tasks\n * schedule frame). Those are internal plumbing, not context keys, so they have no ContextKey and\n * no trust level.\n *\n * REJECTS any name that belongs to a registered {@link ContextKey}. Without that check this\n * method is a complete bypass of the trust system — `put('userId', req.body.userId)` would forge\n * a trusted value while never typing `putTrusted`, and an agent picks whatever compiles. The\n * check is necessarily a RUNTIME one: the registry is populated at `configure()` time, so \"is\n * this string a registered key name\" is not a fact a type can express.\n *\n * @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.\n */\n // webpieces-disable no-any-unknown -- reserved-slot values are heterogeneous (HttpRequest, principal, schedule frame)\n put(key: string, value: any): void {\n this.rejectRegisteredName(key, 'putTrusted / putUntrusted');\n this.writeByName(key, value);\n }\n\n /**\n * Retrieve a value stored under a RAW STRING key. Same reserved-slot purpose, and the same\n * rejection, as {@link put} — reading `get('userId')` would hand back a trusted value without the\n * call site ever saying `getTrusted`, which is exactly the ambiguity this whole change removes.\n *\n * @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.\n */\n // webpieces-disable no-any-unknown -- reserved-slot values are heterogeneous; callers name the concrete type\n get<T = any>(key: string): T | undefined {\n this.rejectRegisteredName(key, 'getTrusted / getUntrusted / getAny');\n return this.readByName<T>(key);\n }\n\n /**\n * Remove a value stored under a RAW STRING key. Registered names are rejected here too: deleting\n * a trusted key out from under a reader is a trust decision, so it goes through {@link removeKey}\n * with the key in hand.\n *\n * @throws Error when `key` is a registered ContextKey name.\n */\n remove(key: string): void {\n this.rejectRegisteredName(key, 'removeKey(key)');\n this.storage.getStore()?.delete(key);\n }\n\n /**\n * The guard behind the three raw-string accessors above. Silent (a no-op) until\n * `HeaderRegistry.configure(...)` has run, which is correct rather than lax: with no registry\n * there are no registered keys, so there is no trusted value to launder.\n */\n private rejectRegisteredName(name: string, useInstead: string): void {\n if (!HeaderRegistry.isConfigured()) {\n return;\n }\n const key = HeaderRegistry.get().findByName(name);\n if (key) {\n throw new Error(\n `RequestContext string accessors cannot touch '${name}' — it is a registered ` +\n `ContextKey (trust: '${key.trust}'). The raw string form hides whether the value is ` +\n `a proven fact or something a caller asserted, so it is a bypass of the trust ` +\n `system. Use ${useInstead} with the ContextKey itself.`,\n );\n }\n }\n\n /** The type-erased read. Every typed verb above funnels here; nothing else reads the store. */\n private readByName<T>(name: string): T | undefined {\n return this.storage.getStore()?.get(name);\n }\n\n /** The type-erased write. Every typed verb above funnels here; nothing else writes the store. */\n // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)\n private writeByName(name: string, value: any): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error('No context available. Did you call Context.run() first?');\n }\n store.set(name, value);\n }\n\n /**\n * Clear all values from the current context.\n */\n clear(): void {\n const store = this.storage.getStore();\n store?.clear();\n }\n\n /**\n * Snapshot this scope so the work you are about to hand off keeps its request id, log fields and\n * proven identity. The ONLY producer of a {@link CapturedContext} — which is what makes the\n * restore side unforgeable, since there is no other way to obtain the payload it accepts.\n *\n * Outside a `run(...)` block this returns an EMPTY snapshot rather than throwing: capturing \"no\n * context\" is a legitimate thing for a background caller to do, and restoring it simply installs\n * nothing.\n *\n * The snapshot is a defensive COPY — writes to this context after capturing do not reach it.\n */\n copyContext(): CapturedContext {\n const store = this.storage.getStore();\n return CapturedContext.capture(ContextCaptureAuthority.INTERNAL, store ?? new Map());\n }\n\n /**\n * Overwrite the ACTIVE scope with a snapshot. The in-place twin of {@link runWithContext}, and the\n * one you almost never want: prefer `runWithContext`, which gives the restored work its OWN scope\n * and cannot disturb the caller's. Reach for this only when something else owns the scope and it\n * must be re-pointed in place.\n *\n * OVERWRITE, not merge — `clear()` runs first, so every entry the active scope holds and the\n * snapshot does not is DROPPED. That includes the empty case: `restoreContext(copyContext())`\n * taken outside a scope wipes the request id and every proven identity from a live request, and\n * says nothing. That is faithful (a snapshot restores exactly what it captured) but it is the\n * sharp edge of this method and the reason `runWithContext` is the default.\n *\n * Takes only a {@link CapturedContext} for the reason spelled out there — the DELETED Map-taking\n * form let `new Map([['userId','victim']])` forge a proven identity in one line.\n *\n * @throws Error when no RequestContext is active.\n */\n restoreContext(captured: CapturedContext): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error(\n 'No context available to restore into. Either open one with RequestContext.run(...) ' +\n 'first, or use RequestContext.runWithContext(captured, fn), which opens its own.',\n );\n }\n captured.restoreInto(ContextCaptureAuthority.INTERNAL, store);\n }\n\n /**\n * Check if a key exists in the context.\n */\n /**\n * Presence of a value under a RAW STRING key. Guarded like its three siblings: `has('userId')`\n * alongside `hasKey(WebpiecesCoreHeaders.USER_ID)` would be a second spelling of one question,\n * and the string form is the one that says nothing about whether the value can be believed.\n *\n * @throws Error when `key` is a registered ContextKey name.\n */\n has(key: string): boolean {\n this.rejectRegisteredName(key, 'hasKey(key)');\n return this.storage.getStore()?.has(key) ?? false;\n }\n\n /**\n * Check if RequestContext is currently active.\n * Returns true if we're inside a RequestContext.run() block, false otherwise.\n *\n * Useful for tests to verify context is set up before making API calls.\n */\n isActive(): boolean {\n return this.storage.getStore() !== undefined;\n }\n\n}\n\n\n\n/**\n * Global singleton instance of RequestContext.\n * Use this throughout your application.\n */\nexport const RequestContext = new RequestContextImpl();\n"]}
1
+ {"version":3,"file":"RequestContext.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContext.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'async_hooks';\nimport { ContextKey, AnyContextKey, HeaderRegistry, ServiceInfo } from '@webpieces/core-util';\nimport { HttpRequest } from './HttpRequest';\nimport { CapturedContext, ContextCaptureAuthority, RestorableContext } from './CapturedContext';\n\n/** Reserved context key under which the current HttpRequest is stored. */\nconst HTTP_REQUEST_KEY = '__webpieces_http_request__';\n\n/**\n * Context management using AsyncLocalStorage.\n * Similar to Java WebPieces Context class that uses ThreadLocal.\n *\n * This allows storing request-scoped data that is automatically available\n * throughout the async call chain, similar to MDC (Mapped Diagnostic Context).\n *\n * Example usage:\n * ```typescript\n * Context.put('REQUEST_ID', '12345');\n * await someAsyncOperation();\n * const id = Context.get('REQUEST_ID'); // Still available!\n * ```\n */\nclass RequestContextImpl {\n private storage: AsyncLocalStorage<Map<string, any>>;\n\n constructor() {\n this.storage = new AsyncLocalStorage<Map<string, any>>();\n }\n\n /**\n * Open THE request scope. A transport calls this once, at the beginning of a request.\n *\n * Nesting is a bug, not a feature, so it throws. AsyncLocalStorage would happily let a second\n * `run()` install a fresh empty Map that SHADOWS the outer one: every value the outer scope\n * holds becomes invisible, `fillFromRequest` mints a second request id, and the two halves of a\n * request end up in different traces. Nothing would tell you.\n *\n * With this guard the setup is right or it is loud. It mirrors\n * `RequestContextHeaders.fillFromRequest()`, which throws when there is NO active scope.\n *\n * If you genuinely WANT a fresh empty scope inside an active one — work that must not inherit the\n * surrounding request's actionId/requestId — that is {@link runDetachedScope}, which says so by\n * name. This guard exists to stop the ACCIDENTAL empty scope, not the deliberate one.\n *\n * @throws Error when a RequestContext is already active.\n */\n run<T>(fn: () => T): T {\n if (this.isActive()) {\n throw new Error(\n 'RequestContext.run(...) called inside an active RequestContext. Nesting installs a ' +\n 'fresh empty context that shadows the outer one: its values go invisible and a second ' +\n 'request id is minted. Exactly ONE scope per request — the transport opens it. If you ' +\n 'MEANT a fresh empty scope that does not inherit this one (a browser-log line, work ' +\n 'reconstructed from an external payload), use RequestContext.runDetachedScope(fn) and ' +\n 'write its values inside the closure with putTrusted/putUntrusted.',\n );\n }\n // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)\n const store = new Map<string, any>();\n return this.storage.run(store, fn);\n }\n\n /**\n * Open a NEW scope pre-loaded with a snapshot — the restore half of {@link copyContext}, for work\n * whose async chain was broken and re-rooted elsewhere (a queued job drained by a background loop,\n * a batch flushed on a timer, an event listener fired from a socket the request does not own). See\n * {@link CapturedContext} for the full list and for why the payload is opaque.\n *\n * A restored context legitimately contains TRUSTED values — reinstating what the original scope\n * proved is the entire point — so this cannot type-check its contents the way the trust verbs do.\n * The guarantee instead comes from the PAYLOAD: a {@link RestorableContext} can only be narrowed\n * out of a {@link CapturedContext}, which only {@link copyContext} produces, so there is no\n * hand-assembled Map to hand it and no way to forge one.\n *\n * The caller must SAY whether the proven identity travels — `snapshot.withTrusted()` (runs as that\n * user) or `snapshot.withoutTrusted()` (runs as the system, keeping only the trace fields). A bare\n * `CapturedContext` is deliberately not accepted; see {@link CapturedContext} for the three-case\n * table and why the wide branch is spelled out rather than defaulted.\n *\n * The snapshot is copied into a fresh store, so writes inside `fn` stay inside `fn` and the\n * snapshot stays reusable.\n *\n * Deliberately NOT guarded against nesting the way {@link run} is. `run`'s guard exists because a\n * second EMPTY scope shadowing the first is always a bug; here the inner scope is a faithful copy\n * of a real one, which is the whole point — a worker that restores a snapshot inside a scope it\n * opened per job is correct, not a mistake. Prefer this over {@link restoreContext} unless you\n * specifically need the CURRENT scope overwritten in place.\n */\n runWithContext<T>(captured: RestorableContext, fn: () => T): T {\n return this.storage.run(captured.toFreshStore(ContextCaptureAuthority.INTERNAL), fn);\n }\n\n /**\n * Open a FRESH, EMPTY, NESTED scope. Nothing is inherited from the enclosing scope, and nothing\n * crosses the boundary as data — every value the work runs under is WRITTEN INSIDE `fn`, through\n * the ordinary trust-typed verbs:\n *\n * ```typescript\n * RequestContext.runDetachedScope(() => {\n * RequestContext.putUntrusted(WebpiecesCoreHeaders.ACTION_ID, line.actionId);\n * emit(); // runs under exactly what this closure wrote, and nothing else\n * });\n * ```\n *\n * ## When you want THIS and not {@link runWithContext}\n *\n * The two look similar and are opposites. `runWithContext` faithfully RE-ROOTS a real snapshot of a\n * real scope, for work whose async chain was broken (a queued job, a timer flush) — it exists to\n * PRESERVE a context. This one exists to DISCARD one: the values do not come from any scope this\n * process ever had, they were reconstructed from somewhere else, and inheriting the ambient scope\n * would be actively wrong.\n *\n * The live case is a browser-log shipper. A batch of browser lines arrives on one HTTP request; each\n * line carries the context the BROWSER captured when it was written, and a single batch routinely\n * spans several user actions. Emitting a line under the shipping request's own scope would stamp\n * every line with that request's actionId and requestId, silently destroying the ability to grep an\n * action while the feature still appeared to work. So each line is emitted detached, under exactly\n * the keys the closure re-stated from the browser's payload.\n *\n * ## Why it MAY nest when {@link run} may not\n *\n * `run`'s nesting guard is right and is not softened here. It refuses a second EMPTY scope because\n * there an empty scope is always an ACCIDENT — a transport opening the request scope twice, whose\n * only effect is to hide the outer scope's values and mint a second request id. Here an empty scope\n * is the thing that was ASKED for, in a distinctly-named verb, and the caller is normally already\n * inside a request scope (the shipper's own). A guard would refuse the only situation the method has.\n *\n * ## No Map-taking form, ever\n *\n * There is deliberately no overload accepting a `Map`, an object, or an array of entries. That was\n * the DELETED `runWithContext(map, fn)`, and it was a forgery path: a hand-built map is\n * indistinguishable from a genuine snapshot, so `new Map([['userId','victim']])` minted a proven\n * identity in one line without ever typing a trust verb. Writing the values INSIDE the closure is\n * what closes it — a loop over a mixed `AnyContextKey[]` must branch on `key.isTrusted()` before it\n * can write anything, and `putUntrusted` does not compile for a trusted key, so code fed by a\n * BROWSER (which proves nothing) cannot fabricate a proven value. (See\n * `DetachedScopeCompileAssertions`.)\n *\n * That is a limit on the SOURCE, not on the key. `putTrusted` inside a detached scope is ordinary\n * and correct whenever the caller has actually proven the value — a verified JWT claim, or the\n * signed-webhook case where Twilio/WhatsApp proves the phone number and the app looks up the\n * userId. Trust is tamper-resistance, not secrecy (a trusted `userId` is a plain, fully-logged\n * GUID; redaction is the separate `maskInLogs` axis on the key).\n *\n * SYNC AND ASYNC BOTH: `fn` may return a promise, and the detached scope follows every `await`\n * inside it exactly as `run`/`runWithContext` do — same `AsyncLocalStorage.run` underneath. The\n * enclosing scope is reinstated for everything after the synchronous return, INCLUDING when `fn`\n * throws (AsyncLocalStorage unwinds the store as the frame unwinds); an async `fn` that is not\n * awaited will therefore keep the detached scope for its own continuation while the caller has\n * already resumed under the enclosing one, which is the intended and only sane reading of \"detached\".\n */\n runDetachedScope<T>(fn: () => T): T {\n // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)\n const store = new Map<string, any>();\n return this.storage.run(store, fn);\n }\n\n /**\n * Read a value the framework PROVED — a verified JWT claim, or a fact an app derived from a\n * verified credential. Does not compile for an untrusted key, so a reader can never mistake a\n * caller-asserted value for an authenticated one.\n *\n * This is the ONLY read that is safe to feed into an authorization decision. If you find\n * yourself wanting `getUntrusted` for that, the fix is to make the key trusted and have an\n * authenticator vouch for it — not to use the other verb.\n *\n * The return type is the key's OWN value type `V` — `string` for wire/log keys, `ApiCallInfo`\n * for the api tag, `TestCaseRecorder` for the recorder — INFERRED from the key, never asserted\n * by the caller. This is the typed public surface over the deliberately type-erased backing Map.\n */\n getTrusted<V>(key: ContextKey<V, 'trusted'>): V | undefined {\n return this.readByName<V>(key.name);\n }\n\n /**\n * Read a value a caller merely ASSERTED — a browser-minted actionId, a recording flag, an\n * in-process log tag. Does not compile for a trusted key: reading a proven fact through the\n * untrusted verb would under-claim and hide, at the call site, that the value IS reliable.\n *\n * Treat everything this returns as attacker-controlled. It is fine for logging, tracing,\n * routing hints and rate-limit bucketing; it is never an input to \"may they do this?\".\n */\n getUntrusted<V>(key: ContextKey<V, 'untrusted'>): V | undefined {\n return this.readByName<V>(key.name);\n }\n\n /**\n * Store a value the framework PROVED. A distinct, greppable verb precisely so that writing a\n * trusted value is something code has to do ON PURPOSE — `grep -rn putTrusted` lists every place\n * in the repo that claims to have proven something, which is a reviewable set.\n *\n * Callers are the framework `AuthFilter` (stamping {@link ContextTuple}s an app's JwtHook derived\n * from a verified credential) and app code that has itself verified something out-of-band — the\n * signed-webhook case: Twilio/WhatsApp proves the phone number, the app looks up the userId, and\n * that userId is every bit as proven as a JWT claim.\n *\n * Does not compile for an untrusted key.\n */\n putTrusted<V>(key: ContextKey<V, 'trusted'>, value: V): void {\n this.writeByName(key.name, value);\n }\n\n /**\n * Store a caller-asserted value. `value` is type-checked against the key's value type `V`, so you\n * cannot put a number under a `ContextKey<string>` or a raw object under a typed key.\n *\n * Does not compile for a trusted key — which is what stops the inbound-header path, the api-tag\n * seam and ordinary app code from being side doors that forge a trusted value.\n */\n putUntrusted<V>(key: ContextKey<V, 'untrusted'>, value: V): void {\n this.writeByName(key.name, value);\n }\n\n /**\n * Read a key of ANY trust level and ANY value type, as `unknown`.\n *\n * FRAMEWORK SERIALIZATION ONLY — the log-field builders below, the outbound header builder, and\n * the {@link ContextReader} seam. Those loop over `HeaderRegistry` key arrays that are mixed in\n * both value type and trust, and they are not making a trust DECISION: they are copying values to\n * a log line or to the wire.\n *\n * It is deliberately read-only and has no write twin. A `putAny` would re-open the exact hole the\n * typed verbs close, because forging a trusted value is the dangerous direction; reading one\n * without saying `getTrusted` only costs you the `unknown` return type.\n */\n // webpieces-disable no-any-unknown -- key-agnostic serialization read: the key array is mixed in value type, so unknown is the honest return\n getAny(key: AnyContextKey): unknown {\n return this.readByName<unknown>(key.name);\n }\n\n /** Clear one context key. Used by the api-tag seam's set → log → remove span (see LogApiCall). */\n removeKey(key: AnyContextKey): void {\n this.storage.getStore()?.delete(key.name);\n }\n\n hasKey(key: AnyContextKey): boolean {\n return this.storage.getStore()?.has(key.name) ?? false;\n }\n\n /**\n * Build the masked field map for LOGGING: every logged key in the global\n * {@link HeaderRegistry} read straight from this context, secured values\n * masked (via {@link ContextKey.maskForLogs}), keyed by each key's `name`.\n *\n * Callers: RecordingFilter + NodeProxyClient.recordCall, which snapshot the context into a\n * test FIXTURE. The @webpieces/winston and @webpieces/bunyan backends also stamp these fields\n * onto every record, and they own the \"log emitted outside RequestContext.run(...)\" complaint —\n * reporting it HERE would recurse (the error line itself re-enters buildLogFields).\n *\n * Returns an EMPTY map outside a `run(...)` block rather than throwing: a fixture snapshot or a\n * log line is never worth crashing a request over.\n */\n buildLogFields(): Map<string, string> {\n const fields = new Map<string, string>();\n if (!this.isActive()) {\n return fields;\n }\n // The registry owns WHICH keys log (getLoggedKeys); we read each straight from THIS context\n // and each ContextKey masks its own secured value. String-only — this map feeds wire/MDC +\n // recorder fixtures — so an object-valued key (API_CALL_INFO) is guarded out by the\n // typeof-string check; objects ride buildStructuredLogFields instead. (Was a HeaderRegistry\n // method taking a read callback; only the server ever called it, so the seam was dead weight.)\n for (const key of HeaderRegistry.get().getLoggedKeys()) {\n // getLoggedKeys() is AnyContextKey[] — mixed in BOTH value type and trust — so this reads\n // through getAny (serialization, not a trust decision) and narrows with the typeof-string\n // guard rather than asserting a value type per key.\n const value = this.getAny(key);\n if (typeof value === 'string' && value) {\n fields.set(key.name, key.maskForLogs(value));\n }\n }\n return fields;\n }\n\n /**\n * The STRUCTURED field map for the node logging backends: like {@link buildLogFields}, but values\n * may be OBJECTS, so an object-valued logged key ({@link WebpiecesCoreHeaders.API_CALL_INFO} holding\n * an {@link ApiCallInfo}) survives as an object and the winston/bunyan backends nest it into\n * `jsonPayload.api`. Reads values UNTYPED (not `<string>`) so the object comes through intact.\n *\n * Outside a `run(...)` block it returns just the `svcName` + `version` entries below (not a fully\n * empty map): a log line is never worth crashing over, and startup/background lines must still say\n * which service and build emitted them.\n *\n * PLUS this service's `svcName` and this build's `version` from {@link ServiceInfo}. Neither is a\n * {@link ContextKey} — they are process-global identity facts, added HERE (BEFORE the active-context\n * check) so EVERY log line of BOTH node backends (winston/bunyan read this one map) says which\n * service and build emitted it — request path, startup, and background jobs alike — with no\n * per-backend duplication. This is the SINGLE place both are stamped, keeping the two backends\n * symmetrical (jsonPayload.svcName + jsonPayload.version). Read via the non-throwing\n * {@link ServiceInfo.getName} / {@link ServiceInfo.getVersion}, so each is simply ABSENT until\n * `setInfo` has run — logging keeps working before the service is identified, then the fields start\n * appearing. Caller-set `svcName`/`version` headers (there are none by convention) would be\n * overwritten here; that is intentional — the ServiceInfo identity is authoritative.\n */\n buildStructuredLogFields(): Map<string, string | object> {\n const fields = new Map<string, string | object>();\n // This service's `svcName` + this build's `version` from ServiceInfo — NOT ContextKeys, they are\n // process-global identity facts. Added FIRST, BEFORE the active-context check, so they ride EVERY\n // line of both node backends (they read this one map) — including startup and background-job lines\n // emitted with NO active RequestContext. Treated identically and read per-record via the\n // non-throwing getters, so each is simply ABSENT until setInfo has run, then starts appearing —\n // even if setInfo runs after a backend was constructed. This is the ONE place both facts are\n // stamped, so winston and bunyan stay symmetrical (jsonPayload.svcName + jsonPayload.version).\n const svcName = ServiceInfo.getName();\n if (svcName) {\n fields.set('svcName', svcName);\n }\n const version = ServiceInfo.getVersion();\n if (version) {\n fields.set('version', version);\n }\n if (!this.isActive()) {\n return fields;\n }\n // Like buildLogFields, but values may be OBJECTS (API_CALL_INFO): read UNTYPED so the object\n // survives and winston/bunyan nest it into jsonPayload.<name>. Secured STRING values are still\n // masked per key; non-string primitives are ignored rather than String()-flattened. (Inlined\n // from HeaderRegistry for the same reason as buildLogFields — only the server called it.)\n for (const key of HeaderRegistry.get().getLoggedKeys()) {\n const value = this.getAny(key);\n if (value === undefined || value === null) {\n continue;\n }\n if (typeof value === 'string') {\n if (value) {\n fields.set(key.name, key.maskForLogs(value));\n }\n } else if (typeof value === 'object') {\n fields.set(key.name, value);\n }\n }\n return fields;\n }\n\n\n /**\n * Store the transport-neutral {@link HttpRequest} for this request. Called once, above the\n * api boundary, by whichever transport is driving the router (the express adapter, or the\n * in-process client). Filters/auth read it back via {@link getRequest} so they never touch\n * express — the same chain then runs over HTTP and in-process.\n */\n setRequest(request: HttpRequest): void {\n this.put(HTTP_REQUEST_KEY, request);\n }\n\n /** The current {@link HttpRequest}, or undefined if none was set for this context. */\n getRequest(): HttpRequest | undefined {\n return this.get<HttpRequest>(HTTP_REQUEST_KEY);\n }\n\n /**\n * Store a value under a RAW STRING key — the escape hatch for the framework's own reserved,\n * UNREGISTERED slots ('__webpieces_http_request__', the AuthFilter principal, the Cloud Tasks\n * schedule frame). Those are internal plumbing, not context keys, so they have no ContextKey and\n * no trust level.\n *\n * REJECTS any name that belongs to a registered {@link ContextKey}. Without that check this\n * method is a complete bypass of the trust system — `put('userId', req.body.userId)` would forge\n * a trusted value while never typing `putTrusted`, and an agent picks whatever compiles. The\n * check is necessarily a RUNTIME one: the registry is populated at `configure()` time, so \"is\n * this string a registered key name\" is not a fact a type can express.\n *\n * @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.\n */\n // webpieces-disable no-any-unknown -- reserved-slot values are heterogeneous (HttpRequest, principal, schedule frame)\n put(key: string, value: any): void {\n this.rejectRegisteredName(key, 'putTrusted / putUntrusted');\n this.writeByName(key, value);\n }\n\n /**\n * Retrieve a value stored under a RAW STRING key. Same reserved-slot purpose, and the same\n * rejection, as {@link put} — reading `get('userId')` would hand back a trusted value without the\n * call site ever saying `getTrusted`, which is exactly the ambiguity this whole change removes.\n *\n * @throws Error when `key` is a registered ContextKey name — naming the verb to use instead.\n */\n // webpieces-disable no-any-unknown -- reserved-slot values are heterogeneous; callers name the concrete type\n get<T = any>(key: string): T | undefined {\n this.rejectRegisteredName(key, 'getTrusted / getUntrusted / getAny');\n return this.readByName<T>(key);\n }\n\n /**\n * Remove a value stored under a RAW STRING key. Registered names are rejected here too: deleting\n * a trusted key out from under a reader is a trust decision, so it goes through {@link removeKey}\n * with the key in hand.\n *\n * @throws Error when `key` is a registered ContextKey name.\n */\n remove(key: string): void {\n this.rejectRegisteredName(key, 'removeKey(key)');\n this.storage.getStore()?.delete(key);\n }\n\n /**\n * The guard behind the three raw-string accessors above. Silent (a no-op) until\n * `HeaderRegistry.configure(...)` has run, which is correct rather than lax: with no registry\n * there are no registered keys, so there is no trusted value to launder.\n */\n private rejectRegisteredName(name: string, useInstead: string): void {\n if (!HeaderRegistry.isConfigured()) {\n return;\n }\n const key = HeaderRegistry.get().findByName(name);\n if (key) {\n throw new Error(\n `RequestContext string accessors cannot touch '${name}' — it is a registered ` +\n `ContextKey (trust: '${key.trust}'). The raw string form hides whether the value is ` +\n `a proven fact or something a caller asserted, so it is a bypass of the trust ` +\n `system. Use ${useInstead} with the ContextKey itself.`,\n );\n }\n }\n\n /** The type-erased read. Every typed verb above funnels here; nothing else reads the store. */\n private readByName<T>(name: string): T | undefined {\n return this.storage.getStore()?.get(name);\n }\n\n /** The type-erased write. Every typed verb above funnels here; nothing else writes the store. */\n // webpieces-disable no-any-unknown -- context values are heterogeneous (strings, recorder, meta objects)\n private writeByName(name: string, value: any): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error('No context available. Did you call Context.run() first?');\n }\n store.set(name, value);\n }\n\n /**\n * Clear all values from the current context.\n */\n clear(): void {\n const store = this.storage.getStore();\n store?.clear();\n }\n\n /**\n * Snapshot this scope so the work you are about to hand off keeps its request id, log fields and\n * proven identity. The ONLY producer of a {@link CapturedContext} — which is what makes the\n * restore side unforgeable, since there is no other way to obtain the payload it accepts.\n *\n * Outside a `run(...)` block this returns an EMPTY snapshot rather than throwing: capturing \"no\n * context\" is a legitimate thing for a background caller to do, and restoring it simply installs\n * nothing.\n *\n * The snapshot is a defensive COPY — writes to this context after capturing do not reach it.\n */\n copyContext(): CapturedContext {\n const store = this.storage.getStore();\n return CapturedContext.capture(ContextCaptureAuthority.INTERNAL, store ?? new Map());\n }\n\n /**\n * Overwrite the ACTIVE scope with a snapshot. The in-place twin of {@link runWithContext}, and the\n * one you almost never want: prefer `runWithContext`, which gives the restored work its OWN scope\n * and cannot disturb the caller's. Reach for this only when something else owns the scope and it\n * must be re-pointed in place.\n *\n * OVERWRITE, not merge — `clear()` runs first, so every entry the active scope holds and the\n * snapshot does not is DROPPED. That includes the empty case:\n * `restoreContext(copyContext().withTrusted())` taken outside a scope wipes the request id and\n * every proven identity from a live request, and says nothing. That is faithful (a snapshot\n * restores exactly what it captured) but it is the sharp edge of this method and the reason\n * `runWithContext` is the default.\n *\n * Takes only a {@link RestorableContext} for the reason spelled out there — the DELETED Map-taking\n * form let `new Map([['userId','victim']])` forge a proven identity in one line — and that type\n * exists only via `withTrusted()` / `withoutTrusted()`, so this call site states whether the proven\n * identity survives the re-point.\n *\n * @throws Error when no RequestContext is active.\n */\n restoreContext(captured: RestorableContext): void {\n const store = this.storage.getStore();\n if (!store) {\n throw new Error(\n 'No context available to restore into. Either open one with RequestContext.run(...) ' +\n 'first, or use RequestContext.runWithContext(captured.withTrusted(), fn) — or ' +\n '.withoutTrusted() — which opens its own.',\n );\n }\n captured.restoreInto(ContextCaptureAuthority.INTERNAL, store);\n }\n\n /**\n * Check if a key exists in the context.\n */\n /**\n * Presence of a value under a RAW STRING key. Guarded like its three siblings: `has('userId')`\n * alongside `hasKey(WebpiecesCoreHeaders.USER_ID)` would be a second spelling of one question,\n * and the string form is the one that says nothing about whether the value can be believed.\n *\n * @throws Error when `key` is a registered ContextKey name.\n */\n has(key: string): boolean {\n this.rejectRegisteredName(key, 'hasKey(key)');\n return this.storage.getStore()?.has(key) ?? false;\n }\n\n /**\n * Check if RequestContext is currently active.\n * Returns true if we're inside a RequestContext.run() block, false otherwise.\n *\n * Useful for tests to verify context is set up before making API calls.\n */\n isActive(): boolean {\n return this.storage.getStore() !== undefined;\n }\n\n}\n\n\n\n/**\n * Global singleton instance of RequestContext.\n * Use this throughout your application.\n */\nexport const RequestContext = new RequestContextImpl();\n"]}