@webpieces/core-util 0.4.389 → 0.4.391

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/core-util",
3
- "version": "0.4.389",
3
+ "version": "0.4.391",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -1,3 +1,4 @@
1
+ import { ApiMethodInfo } from './ApiMethodInfo';
1
2
  /**
2
3
  * ApiCallInfo - the structured tag stamped into RequestContext around every API call
3
4
  * (by {@link LogApiCall}), so ANY log line emitted during the call inherits a filterable
@@ -6,22 +7,26 @@
6
7
  * The node logging backends (winston/bunyan) read this struct out of context via
7
8
  * `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,
8
9
  * which unlocks GCP Cloud Logging filters like:
9
- * - `jsonPayload.api.side="client"` — every outbound call this process made
10
- * - `jsonPayload.api.side="server"` — every inbound call it handled
11
- * - `jsonPayload.api.result="failure"`— failed exchanges only
12
- * - `jsonPayload.api:*` "API traffic only" (tracing + the recorder)
10
+ * - `jsonPayload.api.method.side="client"` — every outbound call this process made
11
+ * - `jsonPayload.api.method.side="server"` — every inbound call it handled
12
+ * - `jsonPayload.api.method.apiClass="SaveApi"` one logical method, BOTH sides (client + server)
13
+ * - `jsonPayload.api.result="failure"` failed exchanges only
14
+ * - `jsonPayload.api:*` — "API traffic only" (tracing + the recorder)
13
15
  *
14
- * IMPORTANT: the field names on this class ARE the GCP field names (`api.side`, `api.type`,
15
- * `api.result`, `api.path`, `api.method`, `api.controller`) rename a field here and the filter
16
- * renames with it.
16
+ * IMPORTANT: the field names here (and on the nested {@link ApiMethodInfo}) ARE the GCP field names
17
+ * rename a field and the filter renames with it. The identity lives NESTED under `api.method`
18
+ * (`api.method.{side,apiClass,methodName,controllerName}`); `api.type` and `api.result` sit at the top.
19
+ *
20
+ * NOTE: the request `httpMethod`/`path` are NOT here — an inbound request stamps them as the separate
21
+ * top-level logged keys `jsonPayload.httpMethod` / `jsonPayload.requestPath` (see
22
+ * {@link WebpiecesCoreHeaders} + `RequestContextHeaders.fillFromRequest`). Outbound client calls have
23
+ * no inbound path, so they carry only the `api` identity.
17
24
  *
18
25
  * Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a
19
26
  * downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.
20
27
  *
21
28
  * Per CLAUDE.md: data-only structures are classes, not interfaces.
22
29
  */
23
- /** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */
24
- export type ApiSide = 'client' | 'server';
25
30
  /** Which half of the exchange this tag describes: the outgoing 'request' or the returning 'response'. */
26
31
  export type ApiType = 'request' | 'response';
27
32
  /**
@@ -29,18 +34,19 @@ export type ApiType = 'request' | 'response';
29
34
  * handled "you made a mistake"); 'failure' is a genuine server error. See {@link LogApiCall.isUserError}.
30
35
  */
31
36
  export type ApiResult = 'success' | 'failure';
37
+ /** Re-exported from {@link ApiMethodInfo} (its true home) so existing `ApiSide` imports keep working. */
38
+ export type { ApiSide } from './ApiMethodInfo';
32
39
  export declare class ApiCallInfo {
33
- readonly side: ApiSide;
40
+ /** The call identity (side, apiClass, methodName, controllerName) — surfaces nested under
41
+ * `jsonPayload.api.method`. */
42
+ readonly method: ApiMethodInfo;
34
43
  readonly type: ApiType;
35
44
  /** Response only — undefined on the 'request' tag. */
36
45
  readonly result?: ApiResult | undefined;
37
- readonly path?: string | undefined;
38
- readonly method?: string | undefined;
39
- /** The controller/API class name (e.g. 'SaveController') — filter with jsonPayload.api.controller. */
40
- readonly controller?: string | undefined;
41
- constructor(side: ApiSide, type: ApiType,
46
+ constructor(
47
+ /** The call identity (side, apiClass, methodName, controllerName) — surfaces nested under
48
+ * `jsonPayload.api.method`. */
49
+ method: ApiMethodInfo, type: ApiType,
42
50
  /** Response only — undefined on the 'request' tag. */
43
- result?: ApiResult | undefined, path?: string | undefined, method?: string | undefined,
44
- /** The controller/API class name (e.g. 'SaveController') — filter with jsonPayload.api.controller. */
45
- controller?: string | undefined);
51
+ result?: ApiResult | undefined);
46
52
  }
@@ -1,46 +1,19 @@
1
1
  "use strict";
2
- /**
3
- * ApiCallInfo - the structured tag stamped into RequestContext around every API call
4
- * (by {@link LogApiCall}), so ANY log line emitted during the call inherits a filterable
5
- * `api` object rather than only the req/resp text lines.
6
- *
7
- * The node logging backends (winston/bunyan) read this struct out of context via
8
- * `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,
9
- * which unlocks GCP Cloud Logging filters like:
10
- * - `jsonPayload.api.side="client"` — every outbound call this process made
11
- * - `jsonPayload.api.side="server"` — every inbound call it handled
12
- * - `jsonPayload.api.result="failure"`— failed exchanges only
13
- * - `jsonPayload.api:*` — "API traffic only" (tracing + the recorder)
14
- *
15
- * IMPORTANT: the field names on this class ARE the GCP field names (`api.side`, `api.type`,
16
- * `api.result`, `api.path`, `api.method`, `api.controller`) — rename a field here and the filter
17
- * renames with it.
18
- *
19
- * Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a
20
- * downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.
21
- *
22
- * Per CLAUDE.md: data-only structures are classes, not interfaces.
23
- */
24
2
  Object.defineProperty(exports, "__esModule", { value: true });
25
3
  exports.ApiCallInfo = void 0;
26
4
  class ApiCallInfo {
27
- side;
5
+ method;
28
6
  type;
29
7
  result;
30
- path;
31
- method;
32
- controller;
33
- constructor(side, type,
8
+ constructor(
9
+ /** The call identity (side, apiClass, methodName, controllerName) — surfaces nested under
10
+ * `jsonPayload.api.method`. */
11
+ method, type,
34
12
  /** Response only — undefined on the 'request' tag. */
35
- result, path, method,
36
- /** The controller/API class name (e.g. 'SaveController') — filter with jsonPayload.api.controller. */
37
- controller) {
38
- this.side = side;
13
+ result) {
14
+ this.method = method;
39
15
  this.type = type;
40
16
  this.result = result;
41
- this.path = path;
42
- this.method = method;
43
- this.controller = controller;
44
17
  }
45
18
  }
46
19
  exports.ApiCallInfo = ApiCallInfo;
@@ -1 +1 @@
1
- {"version":3,"file":"ApiCallInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiCallInfo.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAcH,MAAa,WAAW;IAEP;IACA;IAEA;IACA;IACA;IAEA;IARb,YACa,IAAa,EACb,IAAa;IACtB,sDAAsD;IAC7C,MAAkB,EAClB,IAAa,EACb,MAAe;IACxB,sGAAsG;IAC7F,UAAmB;QAPnB,SAAI,GAAJ,IAAI,CAAS;QACb,SAAI,GAAJ,IAAI,CAAS;QAEb,WAAM,GAAN,MAAM,CAAY;QAClB,SAAI,GAAJ,IAAI,CAAS;QACb,WAAM,GAAN,MAAM,CAAS;QAEf,eAAU,GAAV,UAAU,CAAS;IAC7B,CAAC;CACP;AAXD,kCAWC","sourcesContent":["/**\n * ApiCallInfo - the structured tag stamped into RequestContext around every API call\n * (by {@link LogApiCall}), so ANY log line emitted during the call inherits a filterable\n * `api` object rather than only the req/resp text lines.\n *\n * The node logging backends (winston/bunyan) read this struct out of context via\n * `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,\n * which unlocks GCP Cloud Logging filters like:\n * - `jsonPayload.api.side=\"client\"` — every outbound call this process made\n * - `jsonPayload.api.side=\"server\"` — every inbound call it handled\n * - `jsonPayload.api.result=\"failure\"`— failed exchanges only\n * - `jsonPayload.api:*` — \"API traffic only\" (tracing + the recorder)\n *\n * IMPORTANT: the field names on this class ARE the GCP field names (`api.side`, `api.type`,\n * `api.result`, `api.path`, `api.method`, `api.controller`) rename a field here and the filter\n * renames with it.\n *\n * Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a\n * downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\n/** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */\nexport type ApiSide = 'client' | 'server';\n\n/** Which half of the exchange this tag describes: the outgoing 'request' or the returning 'response'. */\nexport type ApiType = 'request' | 'response';\n\n/**\n * Response outcome. 'success' covers 2xx AND user errors (400/401/403/404/266 — a successfully\n * handled \"you made a mistake\"); 'failure' is a genuine server error. See {@link LogApiCall.isUserError}.\n */\nexport type ApiResult = 'success' | 'failure';\n\nexport class ApiCallInfo {\n constructor(\n readonly side: ApiSide,\n readonly type: ApiType,\n /** Response onlyundefined on the 'request' tag. */\n readonly result?: ApiResult,\n readonly path?: string,\n readonly method?: string,\n /** The controller/API class name (e.g. 'SaveController') — filter with jsonPayload.api.controller. */\n readonly controller?: string,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"ApiCallInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiCallInfo.ts"],"names":[],"mappings":";;;AA2CA,MAAa,WAAW;IAIP;IACA;IAEA;IANb;IACI;oCACgC;IACvB,MAAqB,EACrB,IAAa;IACtB,sDAAsD;IAC7C,MAAkB;QAHlB,WAAM,GAAN,MAAM,CAAe;QACrB,SAAI,GAAJ,IAAI,CAAS;QAEb,WAAM,GAAN,MAAM,CAAY;IAC5B,CAAC;CACP;AATD,kCASC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * ApiCallInfo - the structured tag stamped into RequestContext around every API call\n * (by {@link LogApiCall}), so ANY log line emitted during the call inherits a filterable\n * `api` object rather than only the req/resp text lines.\n *\n * The node logging backends (winston/bunyan) read this struct out of context via\n * `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,\n * which unlocks GCP Cloud Logging filters like:\n * - `jsonPayload.api.method.side=\"client\"` — every outbound call this process made\n * - `jsonPayload.api.method.side=\"server\"` — every inbound call it handled\n * - `jsonPayload.api.method.apiClass=\"SaveApi\"` — one logical method, BOTH sides (client + server)\n * - `jsonPayload.api.result=\"failure\"` — failed exchanges only\n * - `jsonPayload.api:*` — \"API traffic only\" (tracing + the recorder)\n *\n * IMPORTANT: the field names here (and on the nested {@link ApiMethodInfo}) ARE the GCP field names —\n * rename a field and the filter renames with it. The identity lives NESTED under `api.method`\n * (`api.method.{side,apiClass,methodName,controllerName}`); `api.type` and `api.result` sit at the top.\n *\n * NOTE: the request `httpMethod`/`path` are NOT here — an inbound request stamps them as the separate\n * top-level logged keys `jsonPayload.httpMethod` / `jsonPayload.requestPath` (see\n * {@link WebpiecesCoreHeaders} + `RequestContextHeaders.fillFromRequest`). Outbound client calls have\n * no inbound path, so they carry only the `api` identity.\n *\n * Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a\n * downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\n/** Which half of the exchange this tag describes: the outgoing 'request' or the returning 'response'. */\nexport type ApiType = 'request' | 'response';\n\n/**\n * Response outcome. 'success' covers 2xx AND user errors (400/401/403/404/266 — a successfully\n * handled \"you made a mistake\"); 'failure' is a genuine server error. See {@link LogApiCall.isUserError}.\n */\nexport type ApiResult = 'success' | 'failure';\n\n/** Re-exported from {@link ApiMethodInfo} (its true home) so existing `ApiSide` imports keep working. */\nexport type { ApiSide } from './ApiMethodInfo';\n\nexport class ApiCallInfo {\n constructor(\n /** The call identity (side, apiClass, methodName, controllerName) surfaces nested under\n * `jsonPayload.api.method`. */\n readonly method: ApiMethodInfo,\n readonly type: ApiType,\n /** Response only undefined on the 'request' tag. */\n readonly result?: ApiResult,\n ) {}\n}\n"]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * ApiMethodInfo - the transport-neutral identity of a single API method call, handed to
3
+ * {@link LogApiCall.execute} by every caller (server inbound, http/in-process clients, cloud tasks,
4
+ * and external wrapped clients like the firestore admin client).
5
+ *
6
+ * WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,
7
+ * pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.
8
+ * None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped
9
+ * `RouteMetadata`. This carries only what identifies the call.
10
+ *
11
+ * MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical
12
+ * method must log the SAME identity so `jsonPayload.api.method.apiClass="SaveApi"` filters both sides
13
+ * together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only
14
+ * ever knows it, and the server carries it alongside its impl name — so it, not the server's impl
15
+ * class, is the required key.
16
+ *
17
+ * Per CLAUDE.md: data-only structures are classes, not interfaces.
18
+ */
19
+ /** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */
20
+ export type ApiSide = 'client' | 'server';
21
+ export declare class ApiMethodInfo {
22
+ readonly side: ApiSide;
23
+ /** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both
24
+ * sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */
25
+ readonly apiClass: string;
26
+ /** REQUIRED — the method on the contract (e.g. 'save'). */
27
+ readonly methodName: string;
28
+ /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no
29
+ * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */
30
+ readonly controllerName?: string | undefined;
31
+ constructor(side: ApiSide,
32
+ /** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both
33
+ * sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */
34
+ apiClass: string,
35
+ /** REQUIRED — the method on the contract (e.g. 'save'). */
36
+ methodName: string,
37
+ /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no
38
+ * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */
39
+ controllerName?: string | undefined);
40
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * ApiMethodInfo - the transport-neutral identity of a single API method call, handed to
4
+ * {@link LogApiCall.execute} by every caller (server inbound, http/in-process clients, cloud tasks,
5
+ * and external wrapped clients like the firestore admin client).
6
+ *
7
+ * WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,
8
+ * pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.
9
+ * None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped
10
+ * `RouteMetadata`. This carries only what identifies the call.
11
+ *
12
+ * MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical
13
+ * method must log the SAME identity so `jsonPayload.api.method.apiClass="SaveApi"` filters both sides
14
+ * together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only
15
+ * ever knows it, and the server carries it alongside its impl name — so it, not the server's impl
16
+ * class, is the required key.
17
+ *
18
+ * Per CLAUDE.md: data-only structures are classes, not interfaces.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.ApiMethodInfo = void 0;
22
+ class ApiMethodInfo {
23
+ side;
24
+ apiClass;
25
+ methodName;
26
+ controllerName;
27
+ constructor(side,
28
+ /** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both
29
+ * sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */
30
+ apiClass,
31
+ /** REQUIRED — the method on the contract (e.g. 'save'). */
32
+ methodName,
33
+ /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no
34
+ * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */
35
+ controllerName) {
36
+ this.side = side;
37
+ this.apiClass = apiClass;
38
+ this.methodName = methodName;
39
+ this.controllerName = controllerName;
40
+ }
41
+ }
42
+ exports.ApiMethodInfo = ApiMethodInfo;
43
+ //# sourceMappingURL=ApiMethodInfo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiMethodInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiMethodInfo.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAKH,MAAa,aAAa;IAET;IAGA;IAEA;IAGA;IATb,YACa,IAAa;IACtB;2FACuF;IAC9E,QAAgB;IACzB,2DAA2D;IAClD,UAAkB;IAC3B;gGAC4F;IACnF,cAAuB;QARvB,SAAI,GAAJ,IAAI,CAAS;QAGb,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,eAAU,GAAV,UAAU,CAAQ;QAGlB,mBAAc,GAAd,cAAc,CAAS;IACjC,CAAC;CACP;AAZD,sCAYC","sourcesContent":["/**\n * ApiMethodInfo - the transport-neutral identity of a single API method call, handed to\n * {@link LogApiCall.execute} by every caller (server inbound, http/in-process clients, cloud tasks,\n * and external wrapped clients like the firestore admin client).\n *\n * WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,\n * pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.\n * None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped\n * `RouteMetadata`. This carries only what identifies the call.\n *\n * MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical\n * method must log the SAME identity so `jsonPayload.api.method.apiClass=\"SaveApi\"` filters both sides\n * together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only\n * ever knows it, and the server carries it alongside its impl name — so it, not the server's impl\n * class, is the required key.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\n/** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */\nexport type ApiSide = 'client' | 'server';\n\nexport class ApiMethodInfo {\n constructor(\n readonly side: ApiSide,\n /** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both\n * sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */\n readonly apiClass: string,\n /** REQUIRED — the method on the contract (e.g. 'save'). */\n readonly methodName: string,\n /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no\n * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */\n readonly controllerName?: string,\n ) {}\n}\n"]}
@@ -1,14 +1,4 @@
1
- import { RouteMetadata } from "./decorators";
2
- import { ApiSide } from "./ApiCallInfo";
3
- /**
4
- * Options for {@link LogApiCallImpl.execute}. `allowVoidResponse` opts OUT of the strict
5
- * falsy-response guard for callers that legitimately return void/undefined (e.g. a local firestore
6
- * wrapper). Defaults to strict so RPC callers keep the safety net.
7
- */
8
- export declare class LogApiCallOptions {
9
- allowVoidResponse?: boolean;
10
- constructor(allowVoidResponse?: boolean);
11
- }
1
+ import { ApiMethodInfo } from "./ApiMethodInfo";
12
2
  /**
13
3
  * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and
14
4
  * client-side (ProxyClient) for one consistent logging shape across the framework.
@@ -18,7 +8,7 @@ export declare class LogApiCallOptions {
18
8
  * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the
19
9
  * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the
20
10
  * req/resp lines) inherits a filterable `api` object — surfacing in GCP as
21
- * `jsonPayload.api.{side,type,result,path,method}`.
11
+ * `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.
22
12
  *
23
13
  * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →
24
14
  * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).
@@ -38,15 +28,10 @@ export declare class LogApiCallImpl {
38
28
  /**
39
29
  * Execute an API call with logging + `api` context-tagging around it.
40
30
  *
41
- * @param side - 'client' (outbound call this process made) or 'server' (inbound call it handled)
42
- * @param meta - Route metadata with controllerClassName and methodName
43
- * @param requestDto - The request DTO
31
+ * @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,
32
+ * controllerName?). `apiClass` is what matches a client call to its server handler in the logs.
33
+ * @param requestDto - The request DTO (external multi-param callers synthesize a small object)
44
34
  * @param method - The method to execute
45
- * @param options - `allowVoidResponse: true` opts OUT of the strict falsy-response guard, for
46
- * callers that legitimately return void/undefined (e.g. a local firestore wrapper whose
47
- * `setDocument` returns void, or a `getDocById` miss returning undefined). Defaults to strict,
48
- * so RPC callers (LogApiFilter, ProxyClient) keep the safety net: an HTTP endpoint returning
49
- * nothing is almost always a bug.
50
35
  *
51
36
  * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,
52
37
  * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only
@@ -55,7 +40,7 @@ export declare class LogApiCallImpl {
55
40
  * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is
56
41
  * exactly what the GCP filters (`jsonPayload.api.*`) want.
57
42
  */
58
- execute(side: ApiSide, meta: RouteMetadata, requestDto: any, method: (dto: any) => Promise<any>, options?: LogApiCallOptions): Promise<any>;
43
+ execute(methodInfo: ApiMethodInfo, requestDto: any, method: (dto: any) => Promise<any>): Promise<any>;
59
44
  /**
60
45
  * Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api
61
46
  * result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LogApiCall = exports.LogApiCallImpl = exports.LogApiCallOptions = void 0;
3
+ exports.LogApiCall = exports.LogApiCallImpl = void 0;
4
4
  const errors_1 = require("./errors");
5
5
  const errorUtils_1 = require("../lib/errorUtils");
6
6
  const LogManager_1 = require("../logging/LogManager");
@@ -8,18 +8,6 @@ const ApiCallInfo_1 = require("./ApiCallInfo");
8
8
  const ApiCallContext_1 = require("./ApiCallContext");
9
9
  const WebpiecesCoreHeaders_1 = require("./WebpiecesCoreHeaders");
10
10
  const log = LogManager_1.LogManager.getLogger('LogApiCall');
11
- /**
12
- * Options for {@link LogApiCallImpl.execute}. `allowVoidResponse` opts OUT of the strict
13
- * falsy-response guard for callers that legitimately return void/undefined (e.g. a local firestore
14
- * wrapper). Defaults to strict so RPC callers keep the safety net.
15
- */
16
- class LogApiCallOptions {
17
- allowVoidResponse;
18
- constructor(allowVoidResponse) {
19
- this.allowVoidResponse = allowVoidResponse;
20
- }
21
- }
22
- exports.LogApiCallOptions = LogApiCallOptions;
23
11
  /**
24
12
  * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and
25
13
  * client-side (ProxyClient) for one consistent logging shape across the framework.
@@ -29,7 +17,7 @@ exports.LogApiCallOptions = LogApiCallOptions;
29
17
  * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the
30
18
  * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the
31
19
  * req/resp lines) inherits a filterable `api` object — surfacing in GCP as
32
- * `jsonPayload.api.{side,type,result,path,method}`.
20
+ * `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.
33
21
  *
34
22
  * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →
35
23
  * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).
@@ -49,15 +37,10 @@ class LogApiCallImpl {
49
37
  /**
50
38
  * Execute an API call with logging + `api` context-tagging around it.
51
39
  *
52
- * @param side - 'client' (outbound call this process made) or 'server' (inbound call it handled)
53
- * @param meta - Route metadata with controllerClassName and methodName
54
- * @param requestDto - The request DTO
40
+ * @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,
41
+ * controllerName?). `apiClass` is what matches a client call to its server handler in the logs.
42
+ * @param requestDto - The request DTO (external multi-param callers synthesize a small object)
55
43
  * @param method - The method to execute
56
- * @param options - `allowVoidResponse: true` opts OUT of the strict falsy-response guard, for
57
- * callers that legitimately return void/undefined (e.g. a local firestore wrapper whose
58
- * `setDocument` returns void, or a `getDocById` miss returning undefined). Defaults to strict,
59
- * so RPC callers (LogApiFilter, ProxyClient) keep the safety net: an HTTP endpoint returning
60
- * nothing is almost always a bug.
61
44
  *
62
45
  * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,
63
46
  * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only
@@ -66,13 +49,11 @@ class LogApiCallImpl {
66
49
  * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is
67
50
  * exactly what the GCP filters (`jsonPayload.api.*`) want.
68
51
  */
69
- async execute(side, meta,
52
+ async execute(methodInfo,
70
53
  // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches ProxyClient)
71
54
  requestDto,
72
55
  // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary
73
- method, options
74
- // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary
75
- ) {
56
+ method) {
76
57
  // Throws if no ApiCallContext was installed at startup, or there is no active scope to stamp
77
58
  // into (loud misconfiguration — an api call with nowhere to tag is a bug).
78
59
  const ctx = ApiCallContext_1.ApiCallContextHolder.get();
@@ -81,8 +62,9 @@ class LogApiCallImpl {
81
62
  'RequestContext.run(...) a server filter opens; in a browser, build ClientHttpBrowserFactory first.');
82
63
  }
83
64
  const key = WebpiecesCoreHeaders_1.WebpiecesCoreHeaders.API_CALL_INFO;
65
+ const side = methodInfo.side;
84
66
  const server = side === 'server';
85
- const cls = meta.controllerClassName;
67
+ const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;
86
68
  // set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,
87
69
  // never across an await, so a single browser global slot can never be clobbered by a concurrent call.
88
70
  const stamp = (info, emit) => {
@@ -92,13 +74,11 @@ class LogApiCallImpl {
92
74
  };
93
75
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller
94
76
  try {
95
- stamp(new ApiCallInfo_1.ApiCallInfo(side, 'request', undefined, meta.path, meta.methodName, cls), () => log.info(`[API-${side}-req] ${cls}.${meta.methodName} ${meta.path} request=${JSON.stringify(requestDto)}`));
77
+ stamp(new ApiCallInfo_1.ApiCallInfo(methodInfo, 'request', undefined), () => log.info(`[API-${side}-req] ${id} request=${JSON.stringify(requestDto)}`));
96
78
  if (!requestDto)
97
- throw new Error(`Request cannot be null and was from ${cls}.${meta.methodName}`);
79
+ throw new Error(`Request cannot be null and was from ${id}`);
98
80
  const response = await method(requestDto);
99
- if (!options?.allowVoidResponse && !response)
100
- throw new Error(`Response cannot be null and was from ${cls}.${meta.methodName}`);
101
- stamp(new ApiCallInfo_1.ApiCallInfo(side, 'response', 'success', meta.path, meta.methodName, cls), () => log.info(`[API-${side}-resp-SUCCESS] ${cls}.${meta.methodName} response=${JSON.stringify(response)}`));
81
+ stamp(new ApiCallInfo_1.ApiCallInfo(methodInfo, 'response', 'success'), () => log.info(`[API-${side}-resp-SUCCESS] ${id} response=${JSON.stringify(response)}`));
102
82
  return response;
103
83
  }
104
84
  catch (err) {
@@ -108,9 +88,9 @@ class LogApiCallImpl {
108
88
  // Side-dependent: a 4xx the SERVER raised is a handled non-failure (OTHER); the same 4xx a
109
89
  // CLIENT receives means its call FAILED. HttpUserError (266) is never a failure, either side.
110
90
  const isUser = this.isUserError(error, server);
111
- stamp(new ApiCallInfo_1.ApiCallInfo(side, 'response', isUser ? 'success' : 'failure', meta.path, meta.methodName, cls), () => isUser
112
- ? log.warn(`[API-${side}-resp-OTHER] ${cls}.${meta.methodName} errorType=${errorType}`)
113
- : log.error(`[API-${side}-resp-FAIL] ${cls}.${meta.methodName} errorType=${errorType} error=${errorMessage}`));
91
+ stamp(new ApiCallInfo_1.ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure'), () => isUser
92
+ ? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)
93
+ : log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${errorMessage}`));
114
94
  throw error;
115
95
  }
116
96
  }
@@ -1 +1 @@
1
- {"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":"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","sourcesContent":["import {RouteMetadata} from \"./decorators\";\nimport {\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpNotFoundError,\n HttpUserError,\n} from './errors';\nimport {toError} from \"../lib/errorUtils\";\nimport {LogManager} from \"../logging/LogManager\";\nimport {ApiCallInfo, ApiSide} from \"./ApiCallInfo\";\nimport {ApiCallContextHolder} from \"./ApiCallContext\";\nimport {WebpiecesCoreHeaders} from \"./WebpiecesCoreHeaders\";\n\nconst log = LogManager.getLogger('LogApiCall');\n\n/**\n * Options for {@link LogApiCallImpl.execute}. `allowVoidResponse` opts OUT of the strict\n * falsy-response guard for callers that legitimately return void/undefined (e.g. a local firestore\n * wrapper). Defaults to strict so RPC callers keep the safety net.\n */\nexport class LogApiCallOptions {\n allowVoidResponse?: boolean;\n\n constructor(allowVoidResponse?: boolean) {\n this.allowVoidResponse = allowVoidResponse;\n }\n}\n\n/**\n * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and\n * client-side (ProxyClient) for one consistent logging shape across the framework.\n *\n * TWO things happen around each call:\n * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).\n * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the\n * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the\n * req/resp lines) inherits a filterable `api` object — surfacing in GCP as\n * `jsonPayload.api.{side,type,result,path,method}`.\n *\n * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →\n * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).\n * It stamps through the {@link ApiCallContext} seam instead: `setupRuntime` installs a\n * RequestContext-backed impl on a Node server, and `ClientHttpBrowserFactory` a module-global impl in a\n * browser. If neither ran, {@link ApiCallContextHolder.get} throws (loud misconfiguration).\n *\n * Singleton, mirroring `RequestContext`: use the exported {@link LogApiCall} constant, not `new`.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} (user errors)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (server errors)\n */\nexport class LogApiCallImpl {\n\n /**\n * Execute an API call with logging + `api` context-tagging around it.\n *\n * @param side - 'client' (outbound call this process made) or 'server' (inbound call it handled)\n * @param meta - Route metadata with controllerClassName and methodName\n * @param requestDto - The request DTO\n * @param method - The method to execute\n * @param options - `allowVoidResponse: true` opts OUT of the strict falsy-response guard, for\n * callers that legitimately return void/undefined (e.g. a local firestore wrapper whose\n * `setDocument` returns void, or a `getDocById` miss returning undefined). Defaults to strict,\n * so RPC callers (LogApiFilter, ProxyClient) keep the safety net: an HTTP endpoint returning\n * nothing is almost always a bug.\n *\n * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,\n * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only\n * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across\n * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber\n * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is\n * exactly what the GCP filters (`jsonPayload.api.*`) want.\n */\n public async execute(\n side: ApiSide,\n meta: RouteMetadata,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches ProxyClient)\n requestDto: any,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n method: (dto: any) => Promise<any>,\n options?: LogApiCallOptions\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n ): Promise<any> {\n // Throws if no ApiCallContext was installed at startup, or there is no active scope to stamp\n // into (loud misconfiguration — an api call with nowhere to tag is a bug).\n const ctx = ApiCallContextHolder.get();\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside the ' +\n 'RequestContext.run(...) a server filter opens; in a browser, build ClientHttpBrowserFactory first.',\n );\n }\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n const server = side === 'server';\n const cls = meta.controllerClassName;\n // set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,\n // never across an await, so a single browser global slot can never be clobbered by a concurrent call.\n const stamp = (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n emit();\n ctx.remove(key);\n };\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller\n try {\n stamp(new ApiCallInfo(side, 'request', undefined, meta.path, meta.methodName, cls), () =>\n log.info(`[API-${side}-req] ${cls}.${meta.methodName} ${meta.path} request=${JSON.stringify(requestDto)}`));\n\n if(!requestDto)\n throw new Error(`Request cannot be null and was from ${cls}.${meta.methodName}`);\n\n const response = await method(requestDto);\n\n if(!options?.allowVoidResponse && !response)\n throw new Error(`Response cannot be null and was from ${cls}.${meta.methodName}`);\n\n stamp(new ApiCallInfo(side, 'response', 'success', meta.path, meta.methodName, cls), () =>\n log.info(`[API-${side}-resp-SUCCESS] ${cls}.${meta.methodName} response=${JSON.stringify(response)}`));\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n const errorType = error.constructor.name;\n const errorMessage = error.message;\n // Side-dependent: a 4xx the SERVER raised is a handled non-failure (OTHER); the same 4xx a\n // CLIENT receives means its call FAILED. HttpUserError (266) is never a failure, either side.\n const isUser = this.isUserError(error, server);\n\n stamp(new ApiCallInfo(side, 'response', isUser ? 'success' : 'failure', meta.path, meta.methodName, cls), () =>\n isUser\n ? log.warn(`[API-${side}-resp-OTHER] ${cls}.${meta.methodName} errorType=${errorType}`)\n : log.error(`[API-${side}-resp-FAIL] ${cls}.${meta.methodName} errorType=${errorType} error=${errorMessage}`));\n throw error;\n }\n }\n\n /**\n * Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api\n * result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?\n *\n * The question is \"are things WORKING?\", NOT \"was it an HTTP 4xx vs 5xx\" — and the two are not the\n * same (see 408 below). Classification is by portable Error TYPE, never by transport: LogApiCall runs\n * deep in the stack (in-process calls, pubsub/queue handlers, HTTP — one code path) and only ever\n * sees a thrown Error. The Http* classes are poorly named for that (the `Http` prefix is historical),\n * but they ARE portable error types that travel with the throw anywhere, so matching the TYPE works\n * with or without any HTTP in the picture. Do NOT swap this for an `error.code` 4xx range check — that\n * both re-couples to HTTP AND would bury 408.\n *\n * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:\n * - HttpBadRequestError (400) \"your request is malformed\"\n * - HttpUnauthorizedError (401) \"you're not authenticated\"\n * - HttpForbiddenError (403) \"authenticated, but not allowed\"\n * - HttpNotFoundError (404) \"wrong url / no such entity\" (EndpointNotFoundError is a subclass)\n * → the server is fine, the caller erred → result:'success', logged OTHER.\n *\n * SERVER — something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):\n * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately\n * absent from the list below so it counts as a failure.\n * - 500 / 502 / 504 / 598, and any non-Http Error: real failures.\n *\n * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected \"user made a mistake\" signal.\n * CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.\n *\n * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)\n * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call\n * @returns true if this should be treated as a non-failure (OTHER / result:'success')\n */\n isUserError(error: Error, server: boolean): boolean {\n // 266 is the one error that is never a failure, on either side.\n if (error instanceof HttpUserError) {\n return true;\n }\n // A client that RECEIVED any error (except the 266 above) made a failed call.\n if (!server) {\n return false;\n }\n // SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408\n // (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.\n return (\n error instanceof HttpBadRequestError ||\n error instanceof HttpUnauthorizedError ||\n error instanceof HttpForbiddenError ||\n error instanceof HttpNotFoundError\n );\n }\n}\n\n/**\n * The process-wide {@link LogApiCallImpl} singleton — mirrors the `RequestContext` export pattern.\n * Callers use `LogApiCall.execute(...)`, never `new`.\n */\nexport const LogApiCall = new LogApiCallImpl();\n"]}
1
+ {"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpNotFoundError,\n HttpUserError,\n} from './errors';\nimport {toError} from \"../lib/errorUtils\";\nimport {LogManager} from \"../logging/LogManager\";\nimport {ApiCallInfo} from \"./ApiCallInfo\";\nimport {ApiMethodInfo} from \"./ApiMethodInfo\";\nimport {ApiCallContextHolder} from \"./ApiCallContext\";\nimport {WebpiecesCoreHeaders} from \"./WebpiecesCoreHeaders\";\n\nconst log = LogManager.getLogger('LogApiCall');\n\n/**\n * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and\n * client-side (ProxyClient) for one consistent logging shape across the framework.\n *\n * TWO things happen around each call:\n * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).\n * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the\n * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the\n * req/resp lines) inherits a filterable `api` object — surfacing in GCP as\n * `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.\n *\n * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →\n * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).\n * It stamps through the {@link ApiCallContext} seam instead: `setupRuntime` installs a\n * RequestContext-backed impl on a Node server, and `ClientHttpBrowserFactory` a module-global impl in a\n * browser. If neither ran, {@link ApiCallContextHolder.get} throws (loud misconfiguration).\n *\n * Singleton, mirroring `RequestContext`: use the exported {@link LogApiCall} constant, not `new`.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} (user errors)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (server errors)\n */\nexport class LogApiCallImpl {\n\n /**\n * Execute an API call with logging + `api` context-tagging around it.\n *\n * @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,\n * controllerName?). `apiClass` is what matches a client call to its server handler in the logs.\n * @param requestDto - The request DTO (external multi-param callers synthesize a small object)\n * @param method - The method to execute\n *\n * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,\n * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only\n * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across\n * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber\n * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is\n * exactly what the GCP filters (`jsonPayload.api.*`) want.\n */\n public async execute(\n methodInfo: ApiMethodInfo,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches ProxyClient)\n requestDto: any,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n method: (dto: any) => Promise<any>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n ): Promise<any> {\n // Throws if no ApiCallContext was installed at startup, or there is no active scope to stamp\n // into (loud misconfiguration — an api call with nowhere to tag is a bug).\n const ctx = ApiCallContextHolder.get();\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside the ' +\n 'RequestContext.run(...) a server filter opens; in a browser, build ClientHttpBrowserFactory first.',\n );\n }\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n const side = methodInfo.side;\n const server = side === 'server';\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n // set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,\n // never across an await, so a single browser global slot can never be clobbered by a concurrent call.\n const stamp = (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n emit();\n ctx.remove(key);\n };\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller\n try {\n stamp(new ApiCallInfo(methodInfo, 'request', undefined), () =>\n log.info(`[API-${side}-req] ${id} request=${JSON.stringify(requestDto)}`));\n\n if(!requestDto)\n throw new Error(`Request cannot be null and was from ${id}`);\n\n const response = await method(requestDto);\n\n stamp(new ApiCallInfo(methodInfo, 'response', 'success'), () =>\n log.info(`[API-${side}-resp-SUCCESS] ${id} response=${JSON.stringify(response)}`));\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n const errorType = error.constructor.name;\n const errorMessage = error.message;\n // Side-dependent: a 4xx the SERVER raised is a handled non-failure (OTHER); the same 4xx a\n // CLIENT receives means its call FAILED. HttpUserError (266) is never a failure, either side.\n const isUser = this.isUserError(error, server);\n\n stamp(new ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure'), () =>\n isUser\n ? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)\n : log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${errorMessage}`));\n throw error;\n }\n }\n\n /**\n * Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api\n * result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?\n *\n * The question is \"are things WORKING?\", NOT \"was it an HTTP 4xx vs 5xx\" — and the two are not the\n * same (see 408 below). Classification is by portable Error TYPE, never by transport: LogApiCall runs\n * deep in the stack (in-process calls, pubsub/queue handlers, HTTP — one code path) and only ever\n * sees a thrown Error. The Http* classes are poorly named for that (the `Http` prefix is historical),\n * but they ARE portable error types that travel with the throw anywhere, so matching the TYPE works\n * with or without any HTTP in the picture. Do NOT swap this for an `error.code` 4xx range check — that\n * both re-couples to HTTP AND would bury 408.\n *\n * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:\n * - HttpBadRequestError (400) \"your request is malformed\"\n * - HttpUnauthorizedError (401) \"you're not authenticated\"\n * - HttpForbiddenError (403) \"authenticated, but not allowed\"\n * - HttpNotFoundError (404) \"wrong url / no such entity\" (EndpointNotFoundError is a subclass)\n * → the server is fine, the caller erred → result:'success', logged OTHER.\n *\n * SERVER — something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):\n * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately\n * absent from the list below so it counts as a failure.\n * - 500 / 502 / 504 / 598, and any non-Http Error: real failures.\n *\n * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected \"user made a mistake\" signal.\n * CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.\n *\n * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)\n * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call\n * @returns true if this should be treated as a non-failure (OTHER / result:'success')\n */\n isUserError(error: Error, server: boolean): boolean {\n // 266 is the one error that is never a failure, on either side.\n if (error instanceof HttpUserError) {\n return true;\n }\n // A client that RECEIVED any error (except the 266 above) made a failed call.\n if (!server) {\n return false;\n }\n // SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408\n // (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.\n return (\n error instanceof HttpBadRequestError ||\n error instanceof HttpUnauthorizedError ||\n error instanceof HttpForbiddenError ||\n error instanceof HttpNotFoundError\n );\n }\n}\n\n/**\n * The process-wide {@link LogApiCallImpl} singleton — mirrors the `RequestContext` export pattern.\n * Callers use `LogApiCall.execute(...)`, never `new`.\n */\nexport const LogApiCall = new LogApiCallImpl();\n"]}
@@ -14,3 +14,9 @@ export declare class Secrets {
14
14
  /** The secret this client sends for the given `@AuthSharedSecret(key)`, or undefined if unset. */
15
15
  get(key: string): string | undefined;
16
16
  }
17
+ /**
18
+ * DI identifier for the optional {@link Secrets} binding. It is a Symbol (not the class) so the app
19
+ * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(SECRETS)`
20
+ * correct — undefined when unbound. The Secrets class stays the TYPE.
21
+ */
22
+ export declare const SECRETS: unique symbol;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Secrets = void 0;
3
+ exports.SECRETS = exports.Secrets = void 0;
4
4
  /**
5
5
  * Secrets - the CLIENT-side shared-secret store: the ONE value THIS service currently SENDS per
6
6
  * `@AuthSharedSecret(key)` name. Bound once (from config; tests pass literals) and used by every
@@ -24,4 +24,10 @@ class Secrets {
24
24
  }
25
25
  }
26
26
  exports.Secrets = Secrets;
27
+ /**
28
+ * DI identifier for the optional {@link Secrets} binding. It is a Symbol (not the class) so the app
29
+ * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(SECRETS)`
30
+ * correct — undefined when unbound. The Secrets class stays the TYPE.
31
+ */
32
+ exports.SECRETS = Symbol.for('Secrets');
27
33
  //# sourceMappingURL=Secrets.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Secrets.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/Secrets.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,MAAa,OAAO;IAGa;IAF7B,iGAAiG;IACjG,wGAAwG;IACxG,YAA6B,SAA6C,EAAE;QAA/C,WAAM,GAAN,MAAM,CAAyC;IAAG,CAAC;IAEhF,kGAAkG;IAClG,GAAG,CAAC,GAAW;QACX,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;CACJ;AATD,0BASC","sourcesContent":["/**\n * Secrets - the CLIENT-side shared-secret store: the ONE value THIS service currently SENDS per\n * `@AuthSharedSecret(key)` name. Bound once (from config; tests pass literals) and used by every\n * outbound client (the RPC http-client + the Cloud Tasks invokers) — NEVER read from process.env in\n * the send path, so tests stay parallel-safe.\n *\n * The SERVER counterpart accepts TWO values per key ({@link SharedSecrets}) for zero-downtime\n * rotation; a client sends ONE and is migrated by changing its value here. Data-only structure (a\n * class, per the webpieces guidelines).\n */\nexport class Secrets {\n // Values are `string | undefined` so a process.env slot drops straight in with no `?? ''` noise:\n // new Secrets({ INTERNAL_API_SECRET: process.env['INTERNAL_API_SECRET'], KEY2: process.env['KEY2'] })\n constructor(private readonly values: Record<string, string | undefined> = {}) {}\n\n /** The secret this client sends for the given `@AuthSharedSecret(key)`, or undefined if unset. */\n get(key: string): string | undefined {\n return this.values[key];\n }\n}\n"]}
1
+ {"version":3,"file":"Secrets.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/Secrets.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,MAAa,OAAO;IAGa;IAF7B,iGAAiG;IACjG,wGAAwG;IACxG,YAA6B,SAA6C,EAAE;QAA/C,WAAM,GAAN,MAAM,CAAyC;IAAG,CAAC;IAEhF,kGAAkG;IAClG,GAAG,CAAC,GAAW;QACX,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;CACJ;AATD,0BASC;AAED;;;;GAIG;AACU,QAAA,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC","sourcesContent":["/**\n * Secrets - the CLIENT-side shared-secret store: the ONE value THIS service currently SENDS per\n * `@AuthSharedSecret(key)` name. Bound once (from config; tests pass literals) and used by every\n * outbound client (the RPC http-client + the Cloud Tasks invokers) — NEVER read from process.env in\n * the send path, so tests stay parallel-safe.\n *\n * The SERVER counterpart accepts TWO values per key ({@link SharedSecrets}) for zero-downtime\n * rotation; a client sends ONE and is migrated by changing its value here. Data-only structure (a\n * class, per the webpieces guidelines).\n */\nexport class Secrets {\n // Values are `string | undefined` so a process.env slot drops straight in with no `?? ''` noise:\n // new Secrets({ INTERNAL_API_SECRET: process.env['INTERNAL_API_SECRET'], KEY2: process.env['KEY2'] })\n constructor(private readonly values: Record<string, string | undefined> = {}) {}\n\n /** The secret this client sends for the given `@AuthSharedSecret(key)`, or undefined if unset. */\n get(key: string): string | undefined {\n return this.values[key];\n }\n}\n\n/**\n * DI identifier for the optional {@link Secrets} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(SECRETS)`\n * correct — undefined when unbound. The Secrets class stays the TYPE.\n */\nexport const SECRETS = Symbol.for('Secrets');\n"]}
@@ -42,6 +42,19 @@ export declare class WebpiecesCoreHeaders {
42
42
  * `buildLogFields()` string map deliberately skips it (typeof-string guard).
43
43
  */
44
44
  static readonly API_CALL_INFO: ContextKey;
45
+ /**
46
+ * The inbound request's HTTP method and path, stamped ONCE from the {@link HttpRequest} by
47
+ * `RequestContextHeaders.fillFromRequest` (the atomic inbound choke point every transport funnels
48
+ * through). They surface as top-level `jsonPayload.httpMethod` / `jsonPayload.requestPath` so every
49
+ * log line of the request carries them — they used to ride inside {@link ApiCallInfo} (`api.path` /
50
+ * `api.method`) but that coupled a per-CALL logger to the per-REQUEST transport shape.
51
+ *
52
+ * - `httpHeader` UNDEFINED → NOT transferred over the wire: a downstream hop stamps its OWN inbound
53
+ * method/path, never the caller's. Outbound client calls never set these (no inbound path).
54
+ * - `isLogged` TRUE → emitted by the logging backends as plain strings.
55
+ */
56
+ static readonly HTTP_METHOD: ContextKey;
57
+ static readonly REQUEST_PATH: ContextKey;
45
58
  /**
46
59
  * NO CREDENTIAL KEYS LIVE HERE.
47
60
  *
@@ -45,6 +45,19 @@ class WebpiecesCoreHeaders {
45
45
  * `buildLogFields()` string map deliberately skips it (typeof-string guard).
46
46
  */
47
47
  static API_CALL_INFO = new ContextKey_1.ContextKey('api', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);
48
+ /**
49
+ * The inbound request's HTTP method and path, stamped ONCE from the {@link HttpRequest} by
50
+ * `RequestContextHeaders.fillFromRequest` (the atomic inbound choke point every transport funnels
51
+ * through). They surface as top-level `jsonPayload.httpMethod` / `jsonPayload.requestPath` so every
52
+ * log line of the request carries them — they used to ride inside {@link ApiCallInfo} (`api.path` /
53
+ * `api.method`) but that coupled a per-CALL logger to the per-REQUEST transport shape.
54
+ *
55
+ * - `httpHeader` UNDEFINED → NOT transferred over the wire: a downstream hop stamps its OWN inbound
56
+ * method/path, never the caller's. Outbound client calls never set these (no inbound path).
57
+ * - `isLogged` TRUE → emitted by the logging backends as plain strings.
58
+ */
59
+ static HTTP_METHOD = new ContextKey_1.ContextKey('httpMethod', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);
60
+ static REQUEST_PATH = new ContextKey_1.ContextKey('requestPath', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);
48
61
  /**
49
62
  * NO CREDENTIAL KEYS LIVE HERE.
50
63
  *
@@ -72,6 +85,8 @@ class WebpiecesCoreHeaders {
72
85
  WebpiecesCoreHeaders.USER_ROLES,
73
86
  WebpiecesCoreHeaders.RECORDING,
74
87
  WebpiecesCoreHeaders.API_CALL_INFO,
88
+ WebpiecesCoreHeaders.HTTP_METHOD,
89
+ WebpiecesCoreHeaders.REQUEST_PATH,
75
90
  ];
76
91
  }
77
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"WebpiecesCoreHeaders.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesCoreHeaders.ts"],"names":[],"mappings":";;;AAAA,8CAA2C;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,MAAa,oBAAoB;IAC7B;;;OAGG;IACH,MAAM,CAAU,UAAU,GAAG,IAAI,uBAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAEzE,MAAM,CAAU,MAAM,GAAG,IAAI,uBAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAE7D,MAAM,CAAU,OAAO,GAAG,IAAI,uBAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IAEhE,MAAM,CAAU,UAAU,GAAG,IAAI,uBAAU,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IAE1E;;;OAGG;IACH,MAAM,CAAU,SAAS,GAAG,IAAI,uBAAU,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC;IAEjF;;;;;;;;;;;OAWG;IACH,MAAM,CAAU,aAAa,GAAG,IAAI,uBAAU,CAAC,KAAK,EAAE,cAAc,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IAExH;;;;;;;;;;;;;;;OAeG;IAEH;;OAEG;IACH,MAAM,CAAC,aAAa;QAChB,OAAO;YACH,oBAAoB,CAAC,UAAU;YAC/B,oBAAoB,CAAC,OAAO;YAC5B,oBAAoB,CAAC,MAAM;YAC3B,oBAAoB,CAAC,UAAU;YAC/B,oBAAoB,CAAC,SAAS;YAC9B,oBAAoB,CAAC,aAAa;SACrC,CAAC;IACN,CAAC;;AA9DL,oDA+DC","sourcesContent":["import { ContextKey } from '../ContextKey';\n\n/**\n * Core framework context keys — the minimum the WebPieces framework needs to correlate one\n * request across every service it touches, and across every log line each of them writes.\n *\n * ONE id, propagated unchanged. The first service to see a request without an `x-request-id`\n * generates one (RequestContextHeaders.fillFromRequest); every hop copies it onward verbatim. Grep that id and you\n * have the whole call tree. There is no per-hop id and no parent pointer: a chain of ids you must\n * stitch back together buys nothing a single shared id does not already give you.\n *\n * Lives in core-util (browser-safe) so both the http clients and http-server can reference it.\n *\n * Exposed as {@link HeaderRegistry.DEFAULT_HEADERS} — a service opts into these by\n * passing `platformHeaders=true` to `HeaderRegistry.configure(...)`.\n *\n * Each key's `name` is the logical/log name; `httpHeader` is the wire name.\n */\nexport class WebpiecesCoreHeaders {\n /**\n * The id that correlates every hop of one request, and every log line of every hop.\n * Generated by the first service to see a request without one; propagated unchanged after that.\n */\n static readonly REQUEST_ID = new ContextKey('requestId', 'x-request-id');\n\n static readonly ORG_ID = new ContextKey('orgId', 'x-org-id');\n\n static readonly USER_ID = new ContextKey('userId', 'x-user-id');\n\n static readonly USER_ROLES = new ContextKey('roles', 'x-webpieces-roles');\n\n /**\n * Turns on test-case recording for this request (Java: x-webpieces-recording).\n * Transferred so recording follows the request across service hops.\n */\n static readonly RECORDING = new ContextKey('recording', 'x-webpieces-recording');\n\n /**\n * The structured API-call tag ({@link ApiCallInfo}) stamped by {@link LogApiCall} around every\n * outbound (client) / inbound (server) call. It rides the magic context so EVERY log line emitted\n * during the call inherits a filterable `api` object, surfacing in GCP as nested\n * `jsonPayload.api.{side,type,result,path,method}`.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire. Per-hop only: each server/client hop\n * stamps its own tag, so a downstream server records `side:'server'`, never the caller's `side:'client'`.\n * - `isLogged` TRUE → emitted by the logging backends. It carries an OBJECT value, so the backends\n * read it via {@link HeaderRegistry.buildStructuredLogFields} (object-aware); the flat\n * `buildLogFields()` string map deliberately skips it (typeof-string guard).\n */\n static readonly API_CALL_INFO = new ContextKey('api', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);\n\n /**\n * NO CREDENTIAL KEYS LIVE HERE.\n *\n * `authorization` and `x-webpieces-shared-secret` used to be ContextKeys. That made them\n * TRANSFERRED keys, so the inbound transfer copied them off the request into the\n * RequestContext, and every outbound RPC call and enqueued Cloud Task then carried the\n * caller's credential onward — to services that had no business seeing it.\n *\n * A credential belongs to ONE request hop. It is read straight off the {@link HttpRequest}\n * by the framework AuthFilter, and written straight onto the outbound request by the client\n * that mints it (NodeProxyClient, GcpTaskInvoker, InMemoryTaskInvoker). It never enters the\n * magic context, so nothing can propagate it by accident.\n *\n * An app that genuinely wants a credential to travel can still register its own ContextKey for\n * it — but that is now an explicit, visible decision rather than the default.\n */\n\n /**\n * Get all core context keys as an array (the platform DEFAULT_HEADERS set).\n */\n static getAllHeaders(): ContextKey[] {\n return [\n WebpiecesCoreHeaders.REQUEST_ID,\n WebpiecesCoreHeaders.USER_ID,\n WebpiecesCoreHeaders.ORG_ID,\n WebpiecesCoreHeaders.USER_ROLES,\n WebpiecesCoreHeaders.RECORDING,\n WebpiecesCoreHeaders.API_CALL_INFO,\n ];\n }\n}\n"]}
1
+ {"version":3,"file":"WebpiecesCoreHeaders.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesCoreHeaders.ts"],"names":[],"mappings":";;;AAAA,8CAA2C;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,MAAa,oBAAoB;IAC7B;;;OAGG;IACH,MAAM,CAAU,UAAU,GAAG,IAAI,uBAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAEzE,MAAM,CAAU,MAAM,GAAG,IAAI,uBAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAE7D,MAAM,CAAU,OAAO,GAAG,IAAI,uBAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IAEhE,MAAM,CAAU,UAAU,GAAG,IAAI,uBAAU,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IAE1E;;;OAGG;IACH,MAAM,CAAU,SAAS,GAAG,IAAI,uBAAU,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC;IAEjF;;;;;;;;;;;OAWG;IACH,MAAM,CAAU,aAAa,GAAG,IAAI,uBAAU,CAAC,KAAK,EAAE,cAAc,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IAExH;;;;;;;;;;OAUG;IACH,MAAM,CAAU,WAAW,GAAG,IAAI,uBAAU,CAAC,YAAY,EAAE,cAAc,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IAE7H,MAAM,CAAU,YAAY,GAAG,IAAI,uBAAU,CAAC,aAAa,EAAE,cAAc,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IAE/H;;;;;;;;;;;;;;;OAeG;IAEH;;OAEG;IACH,MAAM,CAAC,aAAa;QAChB,OAAO;YACH,oBAAoB,CAAC,UAAU;YAC/B,oBAAoB,CAAC,OAAO;YAC5B,oBAAoB,CAAC,MAAM;YAC3B,oBAAoB,CAAC,UAAU;YAC/B,oBAAoB,CAAC,SAAS;YAC9B,oBAAoB,CAAC,aAAa;YAClC,oBAAoB,CAAC,WAAW;YAChC,oBAAoB,CAAC,YAAY;SACpC,CAAC;IACN,CAAC;;AA/EL,oDAgFC","sourcesContent":["import { ContextKey } from '../ContextKey';\n\n/**\n * Core framework context keys — the minimum the WebPieces framework needs to correlate one\n * request across every service it touches, and across every log line each of them writes.\n *\n * ONE id, propagated unchanged. The first service to see a request without an `x-request-id`\n * generates one (RequestContextHeaders.fillFromRequest); every hop copies it onward verbatim. Grep that id and you\n * have the whole call tree. There is no per-hop id and no parent pointer: a chain of ids you must\n * stitch back together buys nothing a single shared id does not already give you.\n *\n * Lives in core-util (browser-safe) so both the http clients and http-server can reference it.\n *\n * Exposed as {@link HeaderRegistry.DEFAULT_HEADERS} — a service opts into these by\n * passing `platformHeaders=true` to `HeaderRegistry.configure(...)`.\n *\n * Each key's `name` is the logical/log name; `httpHeader` is the wire name.\n */\nexport class WebpiecesCoreHeaders {\n /**\n * The id that correlates every hop of one request, and every log line of every hop.\n * Generated by the first service to see a request without one; propagated unchanged after that.\n */\n static readonly REQUEST_ID = new ContextKey('requestId', 'x-request-id');\n\n static readonly ORG_ID = new ContextKey('orgId', 'x-org-id');\n\n static readonly USER_ID = new ContextKey('userId', 'x-user-id');\n\n static readonly USER_ROLES = new ContextKey('roles', 'x-webpieces-roles');\n\n /**\n * Turns on test-case recording for this request (Java: x-webpieces-recording).\n * Transferred so recording follows the request across service hops.\n */\n static readonly RECORDING = new ContextKey('recording', 'x-webpieces-recording');\n\n /**\n * The structured API-call tag ({@link ApiCallInfo}) stamped by {@link LogApiCall} around every\n * outbound (client) / inbound (server) call. It rides the magic context so EVERY log line emitted\n * during the call inherits a filterable `api` object, surfacing in GCP as nested\n * `jsonPayload.api.{side,type,result,path,method}`.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire. Per-hop only: each server/client hop\n * stamps its own tag, so a downstream server records `side:'server'`, never the caller's `side:'client'`.\n * - `isLogged` TRUE → emitted by the logging backends. It carries an OBJECT value, so the backends\n * read it via {@link HeaderRegistry.buildStructuredLogFields} (object-aware); the flat\n * `buildLogFields()` string map deliberately skips it (typeof-string guard).\n */\n static readonly API_CALL_INFO = new ContextKey('api', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);\n\n /**\n * The inbound request's HTTP method and path, stamped ONCE from the {@link HttpRequest} by\n * `RequestContextHeaders.fillFromRequest` (the atomic inbound choke point every transport funnels\n * through). They surface as top-level `jsonPayload.httpMethod` / `jsonPayload.requestPath` so every\n * log line of the request carries them — they used to ride inside {@link ApiCallInfo} (`api.path` /\n * `api.method`) but that coupled a per-CALL logger to the per-REQUEST transport shape.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire: a downstream hop stamps its OWN inbound\n * method/path, never the caller's. Outbound client calls never set these (no inbound path).\n * - `isLogged` TRUE → emitted by the logging backends as plain strings.\n */\n static readonly HTTP_METHOD = new ContextKey('httpMethod', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);\n\n static readonly REQUEST_PATH = new ContextKey('requestPath', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);\n\n /**\n * NO CREDENTIAL KEYS LIVE HERE.\n *\n * `authorization` and `x-webpieces-shared-secret` used to be ContextKeys. That made them\n * TRANSFERRED keys, so the inbound transfer copied them off the request into the\n * RequestContext, and every outbound RPC call and enqueued Cloud Task then carried the\n * caller's credential onward — to services that had no business seeing it.\n *\n * A credential belongs to ONE request hop. It is read straight off the {@link HttpRequest}\n * by the framework AuthFilter, and written straight onto the outbound request by the client\n * that mints it (NodeProxyClient, GcpTaskInvoker, InMemoryTaskInvoker). It never enters the\n * magic context, so nothing can propagate it by accident.\n *\n * An app that genuinely wants a credential to travel can still register its own ContextKey for\n * it — but that is now an explicit, visible decision rather than the default.\n */\n\n /**\n * Get all core context keys as an array (the platform DEFAULT_HEADERS set).\n */\n static getAllHeaders(): ContextKey[] {\n return [\n WebpiecesCoreHeaders.REQUEST_ID,\n WebpiecesCoreHeaders.USER_ID,\n WebpiecesCoreHeaders.ORG_ID,\n WebpiecesCoreHeaders.USER_ROLES,\n WebpiecesCoreHeaders.RECORDING,\n WebpiecesCoreHeaders.API_CALL_INFO,\n WebpiecesCoreHeaders.HTTP_METHOD,\n WebpiecesCoreHeaders.REQUEST_PATH,\n ];\n }\n}\n"]}
package/src/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export { ConsoleLoggerFactory } from './logging/ConsoleLoggerFactory';
17
17
  export { LogManager } from './logging/LogManager';
18
18
  export { ApiPath, Endpoint, Authentication, AuthenticationConfig, Public, AuthJwt, Auth, AuthOidc, AuthSharedSecret, Rpc, PubSub, Queue, getApiPath, getEndpoints, getEndpointOptions, isFormPost, isApiPath, getAuthMeta, getAuthMode, assertEveryEndpointHasAuthMode, getApiKind, assertApiKind, assertPubSubConventions, getQueueName, validateNoConflictingDecorators, AuthMeta, RouteMetadata, METADATA_KEYS, } from './http/decorators';
19
19
  export type { AuthMode, ApiKind, JwtRequirement, EndpointOptions } from './http/decorators';
20
- export { Secrets } from './http/Secrets';
20
+ export { Secrets, SECRETS } from './http/Secrets';
21
21
  export { ValidateImplementation } from './http/validators';
22
22
  export { ProtocolError, HttpError, HttpNotFoundError, EndpointNotFoundError, HttpBadRequestError, HttpUnauthorizedError, HttpForbiddenError, HttpTimeoutError, HttpBadGatewayError, HttpGatewayTimeoutError, HttpInternalServerError, HttpVendorError, HttpUserError, ENTITY_NOT_FOUND, WRONG_LOGIN_TYPE, WRONG_LOGIN, NOT_APPROVED, EMAIL_NOT_CONFIRMED, WRONG_DOMAIN, WRONG_COMPANY, NO_REG_CODE, } from './http/errors';
23
23
  export { InstantDto, DateDto, TimeDto, DateTimeDto, InstantUtil, DateUtil, TimeUtil, DateTimeUtil, } from './http/datetime';
@@ -31,9 +31,11 @@ export { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';
31
31
  export { ContextReader } from './http/ContextReader';
32
32
  export type { ContextRead, StructuredContextRead } from './http/ContextReader';
33
33
  export { ContextMgr } from './http/ContextMgr';
34
- export { LogApiCall, LogApiCallImpl, LogApiCallOptions } from './http/LogApiCall';
34
+ export { LogApiCall, LogApiCallImpl } from './http/LogApiCall';
35
35
  export { ApiCallInfo } from './http/ApiCallInfo';
36
- export type { ApiSide, ApiType, ApiResult } from './http/ApiCallInfo';
36
+ export type { ApiType, ApiResult } from './http/ApiCallInfo';
37
+ export { ApiMethodInfo } from './http/ApiMethodInfo';
38
+ export type { ApiSide } from './http/ApiMethodInfo';
37
39
  export { ApiCallContextHolder } from './http/ApiCallContext';
38
40
  export type { ApiCallContext } from './http/ApiCallContext';
39
41
  export { TestCaseRecorder, RecorderKeys } from './http/recorder/TestCaseRecorder';
package/src/index.js CHANGED
@@ -8,8 +8,8 @@
8
8
  * @packageDocumentation
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.HttpVendorError = exports.HttpInternalServerError = exports.HttpGatewayTimeoutError = exports.HttpBadGatewayError = exports.HttpTimeoutError = exports.HttpForbiddenError = exports.HttpUnauthorizedError = exports.HttpBadRequestError = exports.EndpointNotFoundError = exports.HttpNotFoundError = exports.HttpError = exports.ProtocolError = exports.Secrets = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.validateNoConflictingDecorators = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.isFormPost = exports.getEndpointOptions = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.AuthSharedSecret = exports.AuthOidc = exports.Auth = exports.AuthJwt = exports.Public = exports.AuthenticationConfig = exports.Authentication = exports.Endpoint = exports.ApiPath = exports.LogManager = exports.ConsoleLoggerFactory = exports.ConsoleLogger = exports.DESIGN_METADATA_KEYS = exports.isDocumentDesign = exports.DocumentDesign = exports.ContextTuple = exports.ContextKey = exports.toError = void 0;
12
- exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.ApiCallInfo = exports.LogApiCallOptions = exports.LogApiCallImpl = exports.LogApiCall = exports.ContextMgr = exports.WebpiecesCoreHeaders = exports.templateDeriver = exports.ErrorWireForm = exports.ClientRegistry = exports.HeaderRegistry = exports.DateTimeUtil = exports.TimeUtil = exports.DateUtil = exports.InstantUtil = exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.HttpUserError = void 0;
11
+ exports.HttpInternalServerError = exports.HttpGatewayTimeoutError = exports.HttpBadGatewayError = exports.HttpTimeoutError = exports.HttpForbiddenError = exports.HttpUnauthorizedError = exports.HttpBadRequestError = exports.EndpointNotFoundError = exports.HttpNotFoundError = exports.HttpError = exports.ProtocolError = exports.SECRETS = exports.Secrets = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.validateNoConflictingDecorators = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.isFormPost = exports.getEndpointOptions = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.AuthSharedSecret = exports.AuthOidc = exports.Auth = exports.AuthJwt = exports.Public = exports.AuthenticationConfig = exports.Authentication = exports.Endpoint = exports.ApiPath = exports.LogManager = exports.ConsoleLoggerFactory = exports.ConsoleLogger = exports.DESIGN_METADATA_KEYS = exports.isDocumentDesign = exports.DocumentDesign = exports.ContextTuple = exports.ContextKey = exports.toError = void 0;
12
+ exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.ApiMethodInfo = exports.ApiCallInfo = exports.LogApiCallImpl = exports.LogApiCall = exports.ContextMgr = exports.WebpiecesCoreHeaders = exports.templateDeriver = exports.ErrorWireForm = exports.ClientRegistry = exports.HeaderRegistry = exports.DateTimeUtil = exports.TimeUtil = exports.DateUtil = exports.InstantUtil = exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.HttpUserError = exports.HttpVendorError = void 0;
13
13
  var errorUtils_1 = require("./lib/errorUtils");
14
14
  Object.defineProperty(exports, "toError", { enumerable: true, get: function () { return errorUtils_1.toError; } });
15
15
  var ContextKey_1 = require("./ContextKey");
@@ -69,6 +69,7 @@ Object.defineProperty(exports, "METADATA_KEYS", { enumerable: true, get: functio
69
69
  // Client-side shared-secret store (the value THIS service sends per @AuthSharedSecret key).
70
70
  var Secrets_1 = require("./http/Secrets");
71
71
  Object.defineProperty(exports, "Secrets", { enumerable: true, get: function () { return Secrets_1.Secrets; } });
72
+ Object.defineProperty(exports, "SECRETS", { enumerable: true, get: function () { return Secrets_1.SECRETS; } });
72
73
  // HTTP errors
73
74
  var errors_1 = require("./http/errors");
74
75
  Object.defineProperty(exports, "ProtocolError", { enumerable: true, get: function () { return errors_1.ProtocolError; } });
@@ -121,11 +122,12 @@ Object.defineProperty(exports, "ContextMgr", { enumerable: true, get: function (
121
122
  var LogApiCall_1 = require("./http/LogApiCall");
122
123
  Object.defineProperty(exports, "LogApiCall", { enumerable: true, get: function () { return LogApiCall_1.LogApiCall; } });
123
124
  Object.defineProperty(exports, "LogApiCallImpl", { enumerable: true, get: function () { return LogApiCall_1.LogApiCallImpl; } });
124
- Object.defineProperty(exports, "LogApiCallOptions", { enumerable: true, get: function () { return LogApiCall_1.LogApiCallOptions; } });
125
125
  // The structured `api` tag + the context-writer seam LogApiCall stamps through. The Node
126
126
  // RequestContext-backed impl is installed by @webpieces/core-context; the browser gets the no-op.
127
127
  var ApiCallInfo_1 = require("./http/ApiCallInfo");
128
128
  Object.defineProperty(exports, "ApiCallInfo", { enumerable: true, get: function () { return ApiCallInfo_1.ApiCallInfo; } });
129
+ var ApiMethodInfo_1 = require("./http/ApiMethodInfo");
130
+ Object.defineProperty(exports, "ApiMethodInfo", { enumerable: true, get: function () { return ApiMethodInfo_1.ApiMethodInfo; } });
129
131
  var ApiCallContext_1 = require("./http/ApiCallContext");
130
132
  Object.defineProperty(exports, "ApiCallContextHolder", { enumerable: true, get: function () { return ApiCallContext_1.ApiCallContextHolder; } });
131
133
  // Test-case recording contract (impl lives in http-server; hooks in http-client)
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @webpieces/core-util\n *\n * Utility functions for WebPieces applications.\n * This package works in both browser and Node.js environments.\n *\n * @packageDocumentation\n */\n\nexport { toError } from './lib/errorUtils';\nexport { ContextKey } from './ContextKey';\nexport { ContextTuple } from './ContextTuple';\n\n// @DocumentDesign — DI-design-root marker. Applies to ANY project kind (server\n// controllers AND library impl classes), so it lives here (browser + Node) rather\n// than in a server-only routing package.\nexport { DocumentDesign, isDocumentDesign, DESIGN_METADATA_KEYS } from './DocumentDesign';\n\n// Logging (merged from former @webpieces/wp-logging).\n// Pluggable logging interface + a browser-safe console default; apps plug in\n// bunyan/winston/pino/etc. via LogManager.setFactory(...). Browser + Node.\nexport type { Logger, LogLevel } from './logging/Logger';\nexport type { LoggerFactory } from './logging/LoggerFactory';\nexport { ConsoleLogger } from './logging/ConsoleLogger';\nexport { ConsoleLoggerFactory } from './logging/ConsoleLoggerFactory';\nexport { LogManager } from './logging/LogManager';\n\n// HTTP API contract (merged from former @webpieces/http-api).\n// Shared HTTP API definition consumed by both client and server: REST\n// decorators, the HttpError hierarchy, datetime DTOs, platform-header\n// registry/readers, ValidateImplementation, and the test-case recorder\n// contract. Pure definitions — express-free, browser + Node safe.\n\n// API definition decorators\nexport {\n ApiPath,\n Endpoint,\n Authentication,\n AuthenticationConfig,\n // Auth mode decorators (clean service-to-service + user JWT model)\n Public,\n AuthJwt,\n Auth,\n AuthOidc,\n AuthSharedSecret,\n // API kind (RPC vs PubSub/Cloud Tasks) + queue naming\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n isFormPost,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n validateNoConflictingDecorators,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n} from './http/decorators';\nexport type { AuthMode, ApiKind, JwtRequirement, EndpointOptions } from './http/decorators';\n// Client-side shared-secret store (the value THIS service sends per @AuthSharedSecret key).\nexport { Secrets } from './http/Secrets';\n\n// Type validators\nexport { ValidateImplementation } from './http/validators';\n\n// HTTP errors\nexport {\n ProtocolError,\n HttpError,\n HttpNotFoundError,\n EndpointNotFoundError,\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpTimeoutError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n HttpInternalServerError,\n HttpVendorError,\n HttpUserError,\n // Error subtype constants\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './http/errors';\n\n// Date/Time DTOs and Utilities (inspired by Java Time / JSR-310)\nexport {\n InstantDto,\n DateDto,\n TimeDto,\n DateTimeDto,\n InstantUtil,\n DateUtil,\n TimeUtil,\n DateTimeUtil,\n} from './http/datetime';\n\n// Context keys + registry (the global magic-context header system)\nexport { HeaderRegistry } from './http/HeaderRegistry';\nexport { ClientRegistry } from './http/ClientRegistry';\nexport type { ServiceUrlDeriver } from './http/ClientRegistry';\n// Pluggable, bidirectional error translation (app exception <-> wire form). Registered on\n// ClientRegistry at startup; consulted before the built-in webpieces mapping on BOTH sides.\nexport { ErrorWireForm } from './http/ErrorTranslation';\nexport type { ErrorTranslation } from './http/ErrorTranslation';\nexport { templateDeriver } from './http/templateDeriver';\nexport { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';\nexport { ContextReader } from './http/ContextReader';\nexport type { ContextRead, StructuredContextRead } from './http/ContextReader';\n\n// BROWSER-ONLY outbound-header propagation (app-held store + registry -> outbound HTTP headers).\n// Only @webpieces/http-client-browser may name it; the server reads RequestContext directly via\n// RequestContextHeaders in the Node-only @webpieces/core-context.\nexport { ContextMgr } from './http/ContextMgr';\n\n// API-call logging helper (uses LogManager above). Singleton: use the LogApiCall constant, not `new`.\nexport { LogApiCall, LogApiCallImpl, LogApiCallOptions } from './http/LogApiCall';\n\n// The structured `api` tag + the context-writer seam LogApiCall stamps through. The Node\n// RequestContext-backed impl is installed by @webpieces/core-context; the browser gets the no-op.\nexport { ApiCallInfo } from './http/ApiCallInfo';\nexport type { ApiSide, ApiType, ApiResult } from './http/ApiCallInfo';\nexport { ApiCallContextHolder } from './http/ApiCallContext';\nexport type { ApiCallContext } from './http/ApiCallContext';\n\n// Test-case recording contract (impl lives in http-server; hooks in http-client)\nexport { TestCaseRecorder, RecorderKeys } from './http/recorder/TestCaseRecorder';\nexport { RecordedEndpoint, RecordedError, RecordedTestCase } from './http/recorder/RecordedEndpoint';\nexport { DoNotRecord, getDoNotRecordFields } from './http/recorder/DoNotRecord';\nexport { RecordSerializer, SerializedMap, SerializedError } from './http/recorder/RecordSerializer';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @webpieces/core-util\n *\n * Utility functions for WebPieces applications.\n * This package works in both browser and Node.js environments.\n *\n * @packageDocumentation\n */\n\nexport { toError } from './lib/errorUtils';\nexport { ContextKey } from './ContextKey';\nexport { ContextTuple } from './ContextTuple';\n\n// @DocumentDesign — DI-design-root marker. Applies to ANY project kind (server\n// controllers AND library impl classes), so it lives here (browser + Node) rather\n// than in a server-only routing package.\nexport { DocumentDesign, isDocumentDesign, DESIGN_METADATA_KEYS } from './DocumentDesign';\n\n// Logging (merged from former @webpieces/wp-logging).\n// Pluggable logging interface + a browser-safe console default; apps plug in\n// bunyan/winston/pino/etc. via LogManager.setFactory(...). Browser + Node.\nexport type { Logger, LogLevel } from './logging/Logger';\nexport type { LoggerFactory } from './logging/LoggerFactory';\nexport { ConsoleLogger } from './logging/ConsoleLogger';\nexport { ConsoleLoggerFactory } from './logging/ConsoleLoggerFactory';\nexport { LogManager } from './logging/LogManager';\n\n// HTTP API contract (merged from former @webpieces/http-api).\n// Shared HTTP API definition consumed by both client and server: REST\n// decorators, the HttpError hierarchy, datetime DTOs, platform-header\n// registry/readers, ValidateImplementation, and the test-case recorder\n// contract. Pure definitions — express-free, browser + Node safe.\n\n// API definition decorators\nexport {\n ApiPath,\n Endpoint,\n Authentication,\n AuthenticationConfig,\n // Auth mode decorators (clean service-to-service + user JWT model)\n Public,\n AuthJwt,\n Auth,\n AuthOidc,\n AuthSharedSecret,\n // API kind (RPC vs PubSub/Cloud Tasks) + queue naming\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n isFormPost,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n validateNoConflictingDecorators,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n} from './http/decorators';\nexport type { AuthMode, ApiKind, JwtRequirement, EndpointOptions } from './http/decorators';\n// Client-side shared-secret store (the value THIS service sends per @AuthSharedSecret key).\nexport { Secrets, SECRETS } from './http/Secrets';\n\n// Type validators\nexport { ValidateImplementation } from './http/validators';\n\n// HTTP errors\nexport {\n ProtocolError,\n HttpError,\n HttpNotFoundError,\n EndpointNotFoundError,\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpTimeoutError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n HttpInternalServerError,\n HttpVendorError,\n HttpUserError,\n // Error subtype constants\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './http/errors';\n\n// Date/Time DTOs and Utilities (inspired by Java Time / JSR-310)\nexport {\n InstantDto,\n DateDto,\n TimeDto,\n DateTimeDto,\n InstantUtil,\n DateUtil,\n TimeUtil,\n DateTimeUtil,\n} from './http/datetime';\n\n// Context keys + registry (the global magic-context header system)\nexport { HeaderRegistry } from './http/HeaderRegistry';\nexport { ClientRegistry } from './http/ClientRegistry';\nexport type { ServiceUrlDeriver } from './http/ClientRegistry';\n// Pluggable, bidirectional error translation (app exception <-> wire form). Registered on\n// ClientRegistry at startup; consulted before the built-in webpieces mapping on BOTH sides.\nexport { ErrorWireForm } from './http/ErrorTranslation';\nexport type { ErrorTranslation } from './http/ErrorTranslation';\nexport { templateDeriver } from './http/templateDeriver';\nexport { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';\nexport { ContextReader } from './http/ContextReader';\nexport type { ContextRead, StructuredContextRead } from './http/ContextReader';\n\n// BROWSER-ONLY outbound-header propagation (app-held store + registry -> outbound HTTP headers).\n// Only @webpieces/http-client-browser may name it; the server reads RequestContext directly via\n// RequestContextHeaders in the Node-only @webpieces/core-context.\nexport { ContextMgr } from './http/ContextMgr';\n\n// API-call logging helper (uses LogManager above). Singleton: use the LogApiCall constant, not `new`.\nexport { LogApiCall, LogApiCallImpl } from './http/LogApiCall';\n\n// The structured `api` tag + the context-writer seam LogApiCall stamps through. The Node\n// RequestContext-backed impl is installed by @webpieces/core-context; the browser gets the no-op.\nexport { ApiCallInfo } from './http/ApiCallInfo';\nexport type { ApiType, ApiResult } from './http/ApiCallInfo';\nexport { ApiMethodInfo } from './http/ApiMethodInfo';\nexport type { ApiSide } from './http/ApiMethodInfo';\nexport { ApiCallContextHolder } from './http/ApiCallContext';\nexport type { ApiCallContext } from './http/ApiCallContext';\n\n// Test-case recording contract (impl lives in http-server; hooks in http-client)\nexport { TestCaseRecorder, RecorderKeys } from './http/recorder/TestCaseRecorder';\nexport { RecordedEndpoint, RecordedError, RecordedTestCase } from './http/recorder/RecordedEndpoint';\nexport { DoNotRecord, getDoNotRecordFields } from './http/recorder/DoNotRecord';\nexport { RecordSerializer, SerializedMap, SerializedError } from './http/recorder/RecordSerializer';\n"]}