@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.
@@ -67,9 +67,12 @@ export declare class RequestContextHeaders {
67
67
  * the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`
68
68
  * and therefore the reason service-to-service identity propagation works at all.
69
69
  *
70
- * The cast is the one place trust is narrowed from the registry's mixed `AnyContextKey`: the
71
- * runtime `isTrusted()` check IS the evidence for it, and it is confined to this single line
72
- * rather than spread across every caller.
70
+ * There is NO cast here. `isTrusted()` / `isUntrusted()` are type predicates, so each branch holds
71
+ * the narrowed key already the runtime check produces the type it proves. Two positive `if`s
72
+ * rather than an `if/else` because TypeScript cannot narrow the NEGATIVE of a predicate over
73
+ * `AnyContextKey` (it is `ContextKey<unknown, Trust>`, one type rather than a union, so there is
74
+ * nothing to `Exclude`); see {@link ContextKey.isUntrusted}. There is no third trust level, so the
75
+ * fall-through is unreachable, not a silent drop.
73
76
  */
74
77
  private acceptInbound;
75
78
  /**
@@ -121,16 +121,21 @@ let RequestContextHeaders = class RequestContextHeaders {
121
121
  * the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`
122
122
  * and therefore the reason service-to-service identity propagation works at all.
123
123
  *
124
- * The cast is the one place trust is narrowed from the registry's mixed `AnyContextKey`: the
125
- * runtime `isTrusted()` check IS the evidence for it, and it is confined to this single line
126
- * rather than spread across every caller.
124
+ * There is NO cast here. `isTrusted()` / `isUntrusted()` are type predicates, so each branch holds
125
+ * the narrowed key already the runtime check produces the type it proves. Two positive `if`s
126
+ * rather than an `if/else` because TypeScript cannot narrow the NEGATIVE of a predicate over
127
+ * `AnyContextKey` (it is `ContextKey<unknown, Trust>`, one type rather than a union, so there is
128
+ * nothing to `Exclude`); see {@link ContextKey.isUntrusted}. There is no third trust level, so the
129
+ * fall-through is unreachable, not a silent drop.
127
130
  */
128
131
  acceptInbound(key, value) {
129
132
  if (key.isTrusted()) {
130
133
  PendingWireTrust_1.PendingWireTrust.stash(key, value);
131
134
  return;
132
135
  }
133
- RequestContext_1.RequestContext.putUntrusted(key, value);
136
+ if (key.isUntrusted()) {
137
+ RequestContext_1.RequestContext.putUntrusted(key, value);
138
+ }
134
139
  }
135
140
  /**
136
141
  * Record that WE minted the id — only ever called from the generate branch above, so the key is
@@ -1 +1 @@
1
- {"version":3,"file":"RequestContextHeaders.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextHeaders.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AnyContextKey,\n AnyTrustedContextKey,\n AnyUntrustedContextKey,\n DestinationTrust,\n HeaderRegistry,\n RecorderKeys,\n ServiceInfo,\n TestCaseRecorder,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton } from './frameworkProvide';\nimport { PendingWireTrust } from './PendingWireTrust';\nimport { HttpRequest } from './HttpRequest';\nimport { RequestContext } from './RequestContext';\n\n/**\n * RequestContextHeaders - the magic context ↔ the wire, for a SERVER. Both directions live here:\n *\n * inbound {@link fillFromRequest} the published HttpRequest's headers -> the context\n * outbound {@link buildOutboundHeaders} the context -> the next hop's headers\n *\n * Reads the AsyncLocalStorage-backed {@link RequestContext} straight through — no ContextReader,\n * no ContextMgr, no abstract base. A server has exactly one place its context lives, and the\n * indirection only hid the failure below. (The browser's answer is `ContextMgr` in\n * @webpieces/core-util, which reads an app-held store because a browser has no ambient scope.)\n *\n * FAILS FAST outside a RequestContext. Silently sending an outbound call with NO request id or\n * tenant is far worse than a loud error — the trace just disappears and you find out in production. Every server-side client (RPC and Cloud Tasks) therefore only works\n * inside `RequestContext.run(...)`, which a top-level server filter normally establishes for you.\n *\n * Stateless once built, so it binds as a framework singleton every server-side client shares.\n */\n@provideFrameworkSingleton()\nexport class RequestContextHeaders {\n /**\n * Every transferred key with a non-empty value THAT THIS DESTINATION MAY RECEIVE, under its wire\n * name. Nothing is rewritten.\n *\n * That includes `x-request-id`, which propagates unchanged: one id correlates the whole call\n * tree, so the callee keeps ours rather than minting its own. ({@link fillFromRequest} only\n * generates an id when the inbound request carries none.)\n *\n * TRUSTED keys are the exception, and `destination` is why this method takes an argument at all.\n * The callee's `AuthFilter` admits an inbound `x-user-id` only on a route that authenticated its\n * CALLER, so shipping one to a `@Public` / `@AuthJwt` endpoint builds a request the callee is\n * obliged to 401. {@link DestinationTrust} answers that from the destination endpoint's own\n * AuthMode — there is no \"send everything\" default to fall into. Untrusted keys always travel.\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n *\n * @throws Error when called outside `RequestContext.run(...)` — see the class doc.\n */\n buildOutboundHeaders(destination: DestinationTrust): Map<string, string> {\n this.requireActiveContext();\n\n const headers = new Map<string, string>();\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n if (!destination.allows(key)) {\n continue;\n }\n // getTransferredKeys() is AnyContextKey[] — mixed in both value type and trust — so this\n // reads through getAny (serialization to the wire, not a trust decision) and narrows with\n // the typeof-string guard; every transferred value is a wire string.\n const value = RequestContext.getAny(key);\n if (typeof value === 'string' && value !== '') {\n headers.set(key.httpHeader!, value);\n }\n }\n\n // CLIENT_VERSION is transferred, but each hop sends ITS OWN build version (not the inherited\n // one) so a downstream server logs which build actually called it. Overwrite whatever the loop\n // copied from an inbound clientVersion with ours; if THIS service has no version, drop it\n // rather than forward the caller's as if it were ours. Non-throwing read — absent before setInfo.\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n headers.set(clientVersionHeader, myVersion);\n } else {\n headers.delete(clientVersionHeader);\n }\n\n return headers;\n }\n\n /**\n * INBOUND — the exact inverse of {@link buildOutboundHeaders}. Publish the request, move every\n * transferrable header off it into the context (read by wire name, stored under the key's\n * `name`), and mint an `x-request-id` if the caller sent none.\n *\n * The request is a PARAMETER, not something we fish back out of the context. Publishing and\n * filling are therefore one atomic step that cannot be half-done or done out of order — the\n * older `setRequest()` + `fillContext()` pair could silently skip the transfer entirely when a\n * caller forgot the first half.\n *\n * This is a PRECONDITION of calling into http-routing, and it belongs ABOVE the api boundary.\n * `WebpiecesMiddleware` does it for every HTTP request; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. The api proxy only checks that a\n * request scope exists — it never builds one.\n *\n * @throws Error when called outside `RequestContext.run(...)`.\n */\n fillFromRequest(request: HttpRequest): void {\n this.requireActiveContext();\n\n RequestContext.setRequest(request);\n\n // Stamp the inbound method+path as top-level logged keys (jsonPayload.httpMethod / requestPath)\n // so EVERY log line of this request carries them. Sourced from the just-published HttpRequest;\n // NOT transferred over the wire, so a downstream hop stamps its own inbound values.\n RequestContext.putUntrusted(WebpiecesCoreHeaders.HTTP_METHOD, request.method);\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_PATH, request.path);\n\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n const values = request.getHeaderValues(key);\n if (values && values.length > 0) {\n this.acceptInbound(key, values[0]);\n }\n }\n\n if (!RequestContext.hasKey(WebpiecesCoreHeaders.REQUEST_ID)) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID, this.generateRequestId());\n this.stampRequestIdSource();\n }\n }\n\n /**\n * ONE inbound header -> the context, routed by the key's TRUST.\n *\n * An untrusted key goes straight in — nobody was ever going to make a security decision on it.\n *\n * A TRUSTED key does NOT. This transport-level fill runs BEFORE any filter, so at this instant\n * nothing has verified who the caller is; writing the value now would mean `getTrusted` could\n * return a header a stranger typed. It is stashed in {@link PendingWireTrust} instead and\n * admitted (or rejected) by `AuthFilter`, which knows the route's auth mode. See that class for\n * the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`\n * and therefore the reason service-to-service identity propagation works at all.\n *\n * The cast is the one place trust is narrowed from the registry's mixed `AnyContextKey`: the\n * runtime `isTrusted()` check IS the evidence for it, and it is confined to this single line\n * rather than spread across every caller.\n */\n private acceptInbound(key: AnyContextKey, value: string): void {\n if (key.isTrusted()) {\n PendingWireTrust.stash(key as AnyTrustedContextKey, value);\n return;\n }\n RequestContext.putUntrusted(key as AnyUntrustedContextKey, value);\n }\n\n /**\n * Record that WE minted the id — only ever called from the generate branch above, so the key is\n * ABSENT on a hop that inherited the caller's id. Present == this service is the trace's origin.\n *\n * Uses the non-throwing `getName()`: this runs PER REQUEST, and a missing log field must not 500\n * live traffic. A server that booted already ran `setupRuntime`, which calls `ServiceInfo.setInfo`\n * with its required name+version, so the name is always there in practice; only a test driving the\n * context directly sees undefined.\n */\n private stampRequestIdSource(): void {\n const svcName = ServiceInfo.getName();\n if (svcName) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID_SOURCE, svcName);\n }\n }\n\n /** The id every log line of this request, and every downstream hop, will carry. */\n private generateRequestId(): string {\n return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;\n }\n\n /**\n * The recorder travelling in the context, when a test is recording this call. Absent in normal\n * operation, and ALWAYS absent in a browser — which is why recording lives on the server-side\n * client and never in the isomorphic core.\n */\n findRecorder(): TestCaseRecorder | undefined {\n if (!RequestContext.isActive()) {\n return undefined;\n }\n return RequestContext.getUntrusted<TestCaseRecorder>(RecorderKeys.RECORDER);\n }\n\n /** Guard both directions: no ambient request scope means there is no context to fill or read. */\n private requireActiveContext(): void {\n if (!RequestContext.isActive()) {\n throw new Error(\n 'No active RequestContext. A webpieces server-side client only works inside ' +\n 'RequestContext.run(...), which a top-level server filter normally establishes. ' +\n 'In a test, wrap the call: await RequestContext.run(async () => client.foo(req));',\n );\n }\n }\n}\n"]}
1
+ {"version":3,"file":"RequestContextHeaders.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/RequestContextHeaders.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AnyContextKey,\n DestinationTrust,\n HeaderRegistry,\n RecorderKeys,\n ServiceInfo,\n TestCaseRecorder,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton } from './frameworkProvide';\nimport { PendingWireTrust } from './PendingWireTrust';\nimport { HttpRequest } from './HttpRequest';\nimport { RequestContext } from './RequestContext';\n\n/**\n * RequestContextHeaders - the magic context ↔ the wire, for a SERVER. Both directions live here:\n *\n * inbound {@link fillFromRequest} the published HttpRequest's headers -> the context\n * outbound {@link buildOutboundHeaders} the context -> the next hop's headers\n *\n * Reads the AsyncLocalStorage-backed {@link RequestContext} straight through — no ContextReader,\n * no ContextMgr, no abstract base. A server has exactly one place its context lives, and the\n * indirection only hid the failure below. (The browser's answer is `ContextMgr` in\n * @webpieces/core-util, which reads an app-held store because a browser has no ambient scope.)\n *\n * FAILS FAST outside a RequestContext. Silently sending an outbound call with NO request id or\n * tenant is far worse than a loud error — the trace just disappears and you find out in production. Every server-side client (RPC and Cloud Tasks) therefore only works\n * inside `RequestContext.run(...)`, which a top-level server filter normally establishes for you.\n *\n * Stateless once built, so it binds as a framework singleton every server-side client shares.\n */\n@provideFrameworkSingleton()\nexport class RequestContextHeaders {\n /**\n * Every transferred key with a non-empty value THAT THIS DESTINATION MAY RECEIVE, under its wire\n * name. Nothing is rewritten.\n *\n * That includes `x-request-id`, which propagates unchanged: one id correlates the whole call\n * tree, so the callee keeps ours rather than minting its own. ({@link fillFromRequest} only\n * generates an id when the inbound request carries none.)\n *\n * TRUSTED keys are the exception, and `destination` is why this method takes an argument at all.\n * The callee's `AuthFilter` admits an inbound `x-user-id` only on a route that authenticated its\n * CALLER, so shipping one to a `@Public` / `@AuthJwt` endpoint builds a request the callee is\n * obliged to 401. {@link DestinationTrust} answers that from the destination endpoint's own\n * AuthMode — there is no \"send everything\" default to fall into. Untrusted keys always travel.\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n *\n * @throws Error when called outside `RequestContext.run(...)` — see the class doc.\n */\n buildOutboundHeaders(destination: DestinationTrust): Map<string, string> {\n this.requireActiveContext();\n\n const headers = new Map<string, string>();\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n if (!destination.allows(key)) {\n continue;\n }\n // getTransferredKeys() is AnyContextKey[] — mixed in both value type and trust — so this\n // reads through getAny (serialization to the wire, not a trust decision) and narrows with\n // the typeof-string guard; every transferred value is a wire string.\n const value = RequestContext.getAny(key);\n if (typeof value === 'string' && value !== '') {\n headers.set(key.httpHeader!, value);\n }\n }\n\n // CLIENT_VERSION is transferred, but each hop sends ITS OWN build version (not the inherited\n // one) so a downstream server logs which build actually called it. Overwrite whatever the loop\n // copied from an inbound clientVersion with ours; if THIS service has no version, drop it\n // rather than forward the caller's as if it were ours. Non-throwing read — absent before setInfo.\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n headers.set(clientVersionHeader, myVersion);\n } else {\n headers.delete(clientVersionHeader);\n }\n\n return headers;\n }\n\n /**\n * INBOUND — the exact inverse of {@link buildOutboundHeaders}. Publish the request, move every\n * transferrable header off it into the context (read by wire name, stored under the key's\n * `name`), and mint an `x-request-id` if the caller sent none.\n *\n * The request is a PARAMETER, not something we fish back out of the context. Publishing and\n * filling are therefore one atomic step that cannot be half-done or done out of order — the\n * older `setRequest()` + `fillContext()` pair could silently skip the transfer entirely when a\n * caller forgot the first half.\n *\n * This is a PRECONDITION of calling into http-routing, and it belongs ABOVE the api boundary.\n * `WebpiecesMiddleware` does it for every HTTP request; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. The api proxy only checks that a\n * request scope exists — it never builds one.\n *\n * @throws Error when called outside `RequestContext.run(...)`.\n */\n fillFromRequest(request: HttpRequest): void {\n this.requireActiveContext();\n\n RequestContext.setRequest(request);\n\n // Stamp the inbound method+path as top-level logged keys (jsonPayload.httpMethod / requestPath)\n // so EVERY log line of this request carries them. Sourced from the just-published HttpRequest;\n // NOT transferred over the wire, so a downstream hop stamps its own inbound values.\n RequestContext.putUntrusted(WebpiecesCoreHeaders.HTTP_METHOD, request.method);\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_PATH, request.path);\n\n // getTransferredKeys() is precomputed at configure() time.\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n const values = request.getHeaderValues(key);\n if (values && values.length > 0) {\n this.acceptInbound(key, values[0]);\n }\n }\n\n if (!RequestContext.hasKey(WebpiecesCoreHeaders.REQUEST_ID)) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID, this.generateRequestId());\n this.stampRequestIdSource();\n }\n }\n\n /**\n * ONE inbound header -> the context, routed by the key's TRUST.\n *\n * An untrusted key goes straight in — nobody was ever going to make a security decision on it.\n *\n * A TRUSTED key does NOT. This transport-level fill runs BEFORE any filter, so at this instant\n * nothing has verified who the caller is; writing the value now would mean `getTrusted` could\n * return a header a stranger typed. It is stashed in {@link PendingWireTrust} instead and\n * admitted (or rejected) by `AuthFilter`, which knows the route's auth mode. See that class for\n * the full rationale — this two-step is the reason trusted keys can safely keep an `httpHeader`\n * and therefore the reason service-to-service identity propagation works at all.\n *\n * There is NO cast here. `isTrusted()` / `isUntrusted()` are type predicates, so each branch holds\n * the narrowed key already — the runtime check produces the type it proves. Two positive `if`s\n * rather than an `if/else` because TypeScript cannot narrow the NEGATIVE of a predicate over\n * `AnyContextKey` (it is `ContextKey<unknown, Trust>`, one type rather than a union, so there is\n * nothing to `Exclude`); see {@link ContextKey.isUntrusted}. There is no third trust level, so the\n * fall-through is unreachable, not a silent drop.\n */\n private acceptInbound(key: AnyContextKey, value: string): void {\n if (key.isTrusted()) {\n PendingWireTrust.stash(key, value);\n return;\n }\n if (key.isUntrusted()) {\n RequestContext.putUntrusted(key, value);\n }\n }\n\n /**\n * Record that WE minted the id — only ever called from the generate branch above, so the key is\n * ABSENT on a hop that inherited the caller's id. Present == this service is the trace's origin.\n *\n * Uses the non-throwing `getName()`: this runs PER REQUEST, and a missing log field must not 500\n * live traffic. A server that booted already ran `setupRuntime`, which calls `ServiceInfo.setInfo`\n * with its required name+version, so the name is always there in practice; only a test driving the\n * context directly sees undefined.\n */\n private stampRequestIdSource(): void {\n const svcName = ServiceInfo.getName();\n if (svcName) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID_SOURCE, svcName);\n }\n }\n\n /** The id every log line of this request, and every downstream hop, will carry. */\n private generateRequestId(): string {\n return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;\n }\n\n /**\n * The recorder travelling in the context, when a test is recording this call. Absent in normal\n * operation, and ALWAYS absent in a browser — which is why recording lives on the server-side\n * client and never in the isomorphic core.\n */\n findRecorder(): TestCaseRecorder | undefined {\n if (!RequestContext.isActive()) {\n return undefined;\n }\n return RequestContext.getUntrusted<TestCaseRecorder>(RecorderKeys.RECORDER);\n }\n\n /** Guard both directions: no ambient request scope means there is no context to fill or read. */\n private requireActiveContext(): void {\n if (!RequestContext.isActive()) {\n throw new Error(\n 'No active RequestContext. A webpieces server-side client only works inside ' +\n 'RequestContext.run(...), which a top-level server filter normally establishes. ' +\n 'In a test, wrap the call: await RequestContext.run(async () => client.foo(req));',\n );\n }\n }\n}\n"]}
package/src/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { RequestContext } from './RequestContext';
2
2
  export type { CapturedContext } from './CapturedContext';
3
+ export type { RestorableContext } from './CapturedContext';
3
4
  export { RequestContextApiCallContext } from './RequestContextApiCallContext';
4
5
  export { HttpRequest } from './HttpRequest';
5
6
  export { provideSingletonDefaultForApi } from './provide';
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/index.ts"],"names":[],"mappings":";;;AAAA,4CAA4C;AAC5C,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAWvB,oGAAoG;AACpG,uGAAuG;AACvG,4FAA4F;AAC5F,+EAA8E;AAArE,4IAAA,4BAA4B,OAAA;AACrC,mGAAmG;AACnG,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,mFAAmF;AACnF,qCAA0D;AAAjD,wHAAA,6BAA6B,OAAA;AACtC,2FAA2F;AAC3F,qCAAqC;AAA5B,mGAAA,QAAQ,OAAA;AACjB,sFAAsF;AACtF,wEAAwE;AACxE,uDAM4B;AALxB,6HAAA,yBAAyB,OAAA;AACzB,0IAAA,sCAAsC,OAAA;AACtC,6HAAA,yBAAyB,OAAA;AACzB,yHAAA,qBAAqB,OAAA;AACrB,wHAAA,oBAAoB,OAAA;AAIxB,mFAAmF;AACnF,yFAAyF;AACzF,yBAAyB;AACzB,EAAE;AACF,8FAA8F;AAC9F,2FAA2F;AAC3F,uDAAuD;AACvD,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,oGAAoG;AACpG,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,uDAA2E;AAAlE,oHAAA,gBAAgB,OAAA;AAAE,uHAAA,mBAAmB,OAAA","sourcesContent":["// Context management with AsyncLocalStorage\nexport { RequestContext } from './RequestContext';\n// The OPAQUE snapshot type that copyContext() produces and restoreContext()/runWithContext() accept.\n//\n// `export type`, NOT `export` — deliberately. Consumers need to NAME it (a field, a queue entry, a\n// parameter) and nothing more. A VALUE export would hand them the class object, and with it the static\n// `capture(...)`, whose capability token a cast can supply even though this barrel never exports the\n// token's type: `CapturedContext.capture(null as never, new Map([['userId','victim']]))` would compile\n// and forge a proven identity — the exact hole this whole change closes. A type-only export removes the\n// class object from the package surface, so there is no factory to reach and copyContext() really is\n// the only producer. (ContextCaptureAuthority is not exported here in any form.)\nexport type { CapturedContext } from './CapturedContext';\n// SERVER impl of the core-util ApiCallContext seam, bound to RequestContext. Importing it here runs\n// its install() side effect, so LogApiCall (core-util, browser-safe) stamps the real RequestContext on\n// a Node server without importing it. A browser never loads core-context → keeps the no-op.\nexport { RequestContextApiCallContext } from './RequestContextApiCallContext';\n// Transport-neutral request stored in the context (http-routing's request type; re-exported there)\nexport { HttpRequest } from './HttpRequest';\n\n// DI provider decorators (shared DI seam; http-routing re-exports for back-compat)\nexport { provideSingletonDefaultForApi } from './provide';\n// Guice-style Provider<T> — lazy singleton OR fresh-per-get, decided by T's binding scope.\nexport { Provider } from './provide';\n// Framework-only DI registry (packages/** use these; keeps framework classes out of a\n// client's buildProviderModule() global scan). See frameworkProvide.ts.\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n provideFrameworkTransient,\n bindFrameworkProvider,\n buildFrameworkModule,\n} from './frameworkProvide';\nexport type { FrameworkScope } from './frameworkProvide';\n\n// Outbound headers for a SERVER: reads RequestContext directly, fails fast outside\n// RequestContext.run(...). Server-side clients (http-client-node, cloudtasks-client) and\n// http-routing use THIS.\n//\n// ContextMgr is deliberately NOT re-exported. It is the browser's answer (an app-held store),\n// and only @webpieces/http-client-browser may name it — importing it here would let a node\n// package reach for a ContextReader it has no use for.\nexport { RequestContextHeaders } from './RequestContextHeaders';\n// The browser store's server counterpart, still used by the logging packages + http-server filters.\nexport { RequestContextReader } from './RequestContextReader';\nexport { PendingWireTrust, PendingTrustedValue } from './PendingWireTrust';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-context/src/index.ts"],"names":[],"mappings":";;;AAAA,4CAA4C;AAC5C,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAiBvB,oGAAoG;AACpG,uGAAuG;AACvG,4FAA4F;AAC5F,+EAA8E;AAArE,4IAAA,4BAA4B,OAAA;AACrC,mGAAmG;AACnG,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,mFAAmF;AACnF,qCAA0D;AAAjD,wHAAA,6BAA6B,OAAA;AACtC,2FAA2F;AAC3F,qCAAqC;AAA5B,mGAAA,QAAQ,OAAA;AACjB,sFAAsF;AACtF,wEAAwE;AACxE,uDAM4B;AALxB,6HAAA,yBAAyB,OAAA;AACzB,0IAAA,sCAAsC,OAAA;AACtC,6HAAA,yBAAyB,OAAA;AACzB,yHAAA,qBAAqB,OAAA;AACrB,wHAAA,oBAAoB,OAAA;AAIxB,mFAAmF;AACnF,yFAAyF;AACzF,yBAAyB;AACzB,EAAE;AACF,8FAA8F;AAC9F,2FAA2F;AAC3F,uDAAuD;AACvD,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,oGAAoG;AACpG,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,uDAA2E;AAAlE,oHAAA,gBAAgB,OAAA;AAAE,uHAAA,mBAAmB,OAAA","sourcesContent":["// Context management with AsyncLocalStorage\nexport { RequestContext } from './RequestContext';\n// The OPAQUE snapshot type that copyContext() produces and restoreContext()/runWithContext() accept.\n//\n// `export type`, NOT `export` — deliberately. Consumers need to NAME it (a field, a queue entry, a\n// parameter) and nothing more. A VALUE export would hand them the class object, and with it the static\n// `capture(...)`, whose capability token a cast can supply even though this barrel never exports the\n// token's type: `CapturedContext.capture(null as never, new Map([['userId','victim']]))` would compile\n// and forge a proven identity — the exact hole this whole change closes. A type-only export removes the\n// class object from the package surface, so there is no factory to reach and copyContext() really is\n// the only producer. (ContextCaptureAuthority is not exported here in any form.)\nexport type { CapturedContext } from './CapturedContext';\n// The narrowed snapshot — what withTrusted() / withoutTrusted() produce and what\n// runWithContext()/restoreContext() accept. A bare CapturedContext is NOT accepted by either, so every\n// call site states whether the proven identity travels with the work (`grep -rn withTrusted`) or is\n// deliberately dropped (`grep -rn withoutTrusted`). Type-only for the same reason as above: with no\n// class object on the surface there is no `of(...)` factory to reach, cast or not.\nexport type { RestorableContext } from './CapturedContext';\n// SERVER impl of the core-util ApiCallContext seam, bound to RequestContext. Importing it here runs\n// its install() side effect, so LogApiCall (core-util, browser-safe) stamps the real RequestContext on\n// a Node server without importing it. A browser never loads core-context → keeps the no-op.\nexport { RequestContextApiCallContext } from './RequestContextApiCallContext';\n// Transport-neutral request stored in the context (http-routing's request type; re-exported there)\nexport { HttpRequest } from './HttpRequest';\n\n// DI provider decorators (shared DI seam; http-routing re-exports for back-compat)\nexport { provideSingletonDefaultForApi } from './provide';\n// Guice-style Provider<T> — lazy singleton OR fresh-per-get, decided by T's binding scope.\nexport { Provider } from './provide';\n// Framework-only DI registry (packages/** use these; keeps framework classes out of a\n// client's buildProviderModule() global scan). See frameworkProvide.ts.\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n provideFrameworkTransient,\n bindFrameworkProvider,\n buildFrameworkModule,\n} from './frameworkProvide';\nexport type { FrameworkScope } from './frameworkProvide';\n\n// Outbound headers for a SERVER: reads RequestContext directly, fails fast outside\n// RequestContext.run(...). Server-side clients (http-client-node, cloudtasks-client) and\n// http-routing use THIS.\n//\n// ContextMgr is deliberately NOT re-exported. It is the browser's answer (an app-held store),\n// and only @webpieces/http-client-browser may name it — importing it here would let a node\n// package reach for a ContextReader it has no use for.\nexport { RequestContextHeaders } from './RequestContextHeaders';\n// The browser store's server counterpart, still used by the logging packages + http-server filters.\nexport { RequestContextReader } from './RequestContextReader';\nexport { PendingWireTrust, PendingTrustedValue } from './PendingWireTrust';\n"]}