@webpieces/core-util 0.3.383 → 0.3.385

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.3.383",
3
+ "version": "0.3.385",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -0,0 +1,56 @@
1
+ import { ContextKey } from '../ContextKey';
2
+ /**
3
+ * ApiCallContext - the tiny seam that lets {@link LogApiCall} (browser-safe, core-util) stamp a
4
+ * ContextKey (the `api` tag) into the ambient request context WITHOUT importing it.
5
+ *
6
+ * WHY a seam instead of a direct call: the ambient context is `RequestContext` in
7
+ * `@webpieces/core-context`, which is built on Node `async_hooks` (AsyncLocalStorage). core-util —
8
+ * and `ProxyClient`, which runs in a BROWSER bundle — must never import that (it would be a circular
9
+ * dependency, and it would drag Node vocabulary into a browser build). So core-util owns only this
10
+ * interface + a global holder; each environment installs its own impl at startup:
11
+ * - Node server: `setupRuntime` installs a RequestContext-backed impl.
12
+ * - Browser: `ClientHttpBrowserFactory` installs a module-global impl.
13
+ *
14
+ * If NEITHER installs one, {@link ApiCallContextHolder.get} THROWS (loud misconfiguration, in the
15
+ * webpieces spirit) — see its message. There is deliberately no silent no-op default.
16
+ *
17
+ * This mirrors the existing global-singleton-configured-at-startup pattern (LogManager,
18
+ * HeaderRegistry): behavior is an interface (per CLAUDE.md), the holder is the global seam.
19
+ */
20
+ export interface ApiCallContext {
21
+ /**
22
+ * True when there is a context to stamp into (a live Node RequestContext scope; a browser is always
23
+ * active). {@link LogApiCall} throws if this is false — an api call with nowhere to tag is a bug.
24
+ */
25
+ isActive(): boolean;
26
+ /**
27
+ * Stamp one ContextKey → value into the ambient context. The logger reads it back off the context
28
+ * (server: RequestContext.buildStructuredLogFields; browser: its own store) during the log emit.
29
+ */
30
+ set(contextKey: ContextKey, value: unknown): void;
31
+ /**
32
+ * Clear one ContextKey. {@link LogApiCall} calls set → log → remove as one SYNCHRONOUS span, so the
33
+ * tag is never held across `await`. That is what makes a single browser global safe: single-threaded,
34
+ * nothing can interleave between set and remove, so a concurrent call can never clobber the slot.
35
+ */
36
+ remove(contextKey: ContextKey): void;
37
+ }
38
+ /**
39
+ * ApiCallContextHolder - the process-wide holder for the active {@link ApiCallContext}.
40
+ *
41
+ * Configured exactly like {@link LogManager}: the environment calls {@link ApiCallContextHolder.install}
42
+ * at startup. Until then {@link get} THROWS, so a forgotten setup fails loudly rather than silently
43
+ * dropping the `api` tag off every log line.
44
+ */
45
+ export declare class ApiCallContextHolder {
46
+ private static current;
47
+ /** Install the environment's ApiCallContext (Node: RequestContext-backed; browser: module-global). */
48
+ static install(ctx: ApiCallContext): void;
49
+ /** True once an ApiCallContext has been installed (used by tests to probe the unset state). */
50
+ static isInstalled(): boolean;
51
+ /**
52
+ * The active ApiCallContext. Throws if nothing was installed — a one-time setup call is required:
53
+ * `setupRuntime()` does it on a Node server; building `ClientHttpBrowserFactory` does it in a browser.
54
+ */
55
+ static get(): ApiCallContext;
56
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiCallContextHolder = void 0;
4
+ /**
5
+ * ApiCallContextHolder - the process-wide holder for the active {@link ApiCallContext}.
6
+ *
7
+ * Configured exactly like {@link LogManager}: the environment calls {@link ApiCallContextHolder.install}
8
+ * at startup. Until then {@link get} THROWS, so a forgotten setup fails loudly rather than silently
9
+ * dropping the `api` tag off every log line.
10
+ */
11
+ class ApiCallContextHolder {
12
+ static current;
13
+ /** Install the environment's ApiCallContext (Node: RequestContext-backed; browser: module-global). */
14
+ // webpieces-disable no-function-outside-class -- static global seam, configured once at startup (like LogManager.setFactory)
15
+ static install(ctx) {
16
+ ApiCallContextHolder.current = ctx;
17
+ }
18
+ /** True once an ApiCallContext has been installed (used by tests to probe the unset state). */
19
+ // webpieces-disable no-function-outside-class -- static global seam accessor (like HeaderRegistry.isConfigured)
20
+ static isInstalled() {
21
+ return ApiCallContextHolder.current !== undefined;
22
+ }
23
+ /**
24
+ * The active ApiCallContext. Throws if nothing was installed — a one-time setup call is required:
25
+ * `setupRuntime()` does it on a Node server; building `ClientHttpBrowserFactory` does it in a browser.
26
+ */
27
+ // webpieces-disable no-function-outside-class -- static global seam accessor (like LogManager/HeaderRegistry.get), not DI-injected
28
+ static get() {
29
+ if (!ApiCallContextHolder.current) {
30
+ throw new Error('ApiCallContext is not installed — LogApiCall cannot tag API-call logs. Set it up ONCE ' +
31
+ 'at startup: on a Node server, setupRuntime() installs it for you; in a browser, construct ' +
32
+ 'ClientHttpBrowserFactory once at startup. (This is the same one-time setup as ' +
33
+ 'HeaderRegistry.configure / LogManager.setFactory.)');
34
+ }
35
+ return ApiCallContextHolder.current;
36
+ }
37
+ }
38
+ exports.ApiCallContextHolder = ApiCallContextHolder;
39
+ //# sourceMappingURL=ApiCallContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiCallContext.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiCallContext.ts"],"names":[],"mappings":";;;AA0CA;;;;;;GAMG;AACH,MAAa,oBAAoB;IACrB,MAAM,CAAC,OAAO,CAA6B;IAEnD,sGAAsG;IACtG,6HAA6H;IAC7H,MAAM,CAAC,OAAO,CAAC,GAAmB;QAC9B,oBAAoB,CAAC,OAAO,GAAG,GAAG,CAAC;IACvC,CAAC;IAED,+FAA+F;IAC/F,gHAAgH;IAChH,MAAM,CAAC,WAAW;QACd,OAAO,oBAAoB,CAAC,OAAO,KAAK,SAAS,CAAC;IACtD,CAAC;IAED;;;OAGG;IACH,mIAAmI;IACnI,MAAM,CAAC,GAAG;QACN,IAAI,CAAC,oBAAoB,CAAC,OAAO,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CACX,wFAAwF;gBACxF,4FAA4F;gBAC5F,gFAAgF;gBAChF,oDAAoD,CACvD,CAAC;QACN,CAAC;QACD,OAAO,oBAAoB,CAAC,OAAO,CAAC;IACxC,CAAC;CACJ;AA/BD,oDA+BC","sourcesContent":["import { ContextKey } from '../ContextKey';\n\n/**\n * ApiCallContext - the tiny seam that lets {@link LogApiCall} (browser-safe, core-util) stamp a\n * ContextKey (the `api` tag) into the ambient request context WITHOUT importing it.\n *\n * WHY a seam instead of a direct call: the ambient context is `RequestContext` in\n * `@webpieces/core-context`, which is built on Node `async_hooks` (AsyncLocalStorage). core-util —\n * and `ProxyClient`, which runs in a BROWSER bundle — must never import that (it would be a circular\n * dependency, and it would drag Node vocabulary into a browser build). So core-util owns only this\n * interface + a global holder; each environment installs its own impl at startup:\n * - Node server: `setupRuntime` installs a RequestContext-backed impl.\n * - Browser: `ClientHttpBrowserFactory` installs a module-global impl.\n *\n * If NEITHER installs one, {@link ApiCallContextHolder.get} THROWS (loud misconfiguration, in the\n * webpieces spirit) — see its message. There is deliberately no silent no-op default.\n *\n * This mirrors the existing global-singleton-configured-at-startup pattern (LogManager,\n * HeaderRegistry): behavior is an interface (per CLAUDE.md), the holder is the global seam.\n */\nexport interface ApiCallContext {\n /**\n * True when there is a context to stamp into (a live Node RequestContext scope; a browser is always\n * active). {@link LogApiCall} throws if this is false — an api call with nowhere to tag is a bug.\n */\n isActive(): boolean;\n\n /**\n * Stamp one ContextKey → value into the ambient context. The logger reads it back off the context\n * (server: RequestContext.buildStructuredLogFields; browser: its own store) during the log emit.\n */\n // webpieces-disable no-any-unknown -- a context value is heterogeneous (the api struct here; strings elsewhere)\n set(contextKey: ContextKey, value: unknown): void;\n\n /**\n * Clear one ContextKey. {@link LogApiCall} calls set → log → remove as one SYNCHRONOUS span, so the\n * tag is never held across `await`. That is what makes a single browser global safe: single-threaded,\n * nothing can interleave between set and remove, so a concurrent call can never clobber the slot.\n */\n remove(contextKey: ContextKey): void;\n}\n\n/**\n * ApiCallContextHolder - the process-wide holder for the active {@link ApiCallContext}.\n *\n * Configured exactly like {@link LogManager}: the environment calls {@link ApiCallContextHolder.install}\n * at startup. Until then {@link get} THROWS, so a forgotten setup fails loudly rather than silently\n * dropping the `api` tag off every log line.\n */\nexport class ApiCallContextHolder {\n private static current: ApiCallContext | undefined;\n\n /** Install the environment's ApiCallContext (Node: RequestContext-backed; browser: module-global). */\n // webpieces-disable no-function-outside-class -- static global seam, configured once at startup (like LogManager.setFactory)\n static install(ctx: ApiCallContext): void {\n ApiCallContextHolder.current = ctx;\n }\n\n /** True once an ApiCallContext has been installed (used by tests to probe the unset state). */\n // webpieces-disable no-function-outside-class -- static global seam accessor (like HeaderRegistry.isConfigured)\n static isInstalled(): boolean {\n return ApiCallContextHolder.current !== undefined;\n }\n\n /**\n * The active ApiCallContext. Throws if nothing was installed — a one-time setup call is required:\n * `setupRuntime()` does it on a Node server; building `ClientHttpBrowserFactory` does it in a browser.\n */\n // webpieces-disable no-function-outside-class -- static global seam accessor (like LogManager/HeaderRegistry.get), not DI-injected\n static get(): ApiCallContext {\n if (!ApiCallContextHolder.current) {\n throw new Error(\n 'ApiCallContext is not installed — LogApiCall cannot tag API-call logs. Set it up ONCE ' +\n 'at startup: on a Node server, setupRuntime() installs it for you; in a browser, construct ' +\n 'ClientHttpBrowserFactory once at startup. (This is the same one-time setup as ' +\n 'HeaderRegistry.configure / LogManager.setFactory.)',\n );\n }\n return ApiCallContextHolder.current;\n }\n}\n"]}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * ApiCallInfo - the structured tag stamped into RequestContext around every API call
3
+ * (by {@link LogApiCall}), so ANY log line emitted during the call inherits a filterable
4
+ * `api` object rather than only the req/resp text lines.
5
+ *
6
+ * The node logging backends (winston/bunyan) read this struct out of context via
7
+ * `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,
8
+ * 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)
13
+ *
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.
17
+ *
18
+ * Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a
19
+ * downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.
20
+ *
21
+ * Per CLAUDE.md: data-only structures are classes, not interfaces.
22
+ */
23
+ /** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */
24
+ export type ApiSide = 'client' | 'server';
25
+ /** Which half of the exchange this tag describes: the outgoing 'request' or the returning 'response'. */
26
+ export type ApiType = 'request' | 'response';
27
+ /**
28
+ * Response outcome. 'success' covers 2xx AND user errors (400/401/403/404/266 — a successfully
29
+ * handled "you made a mistake"); 'failure' is a genuine server error. See {@link LogApiCall.isUserError}.
30
+ */
31
+ export type ApiResult = 'success' | 'failure';
32
+ export declare class ApiCallInfo {
33
+ readonly side: ApiSide;
34
+ readonly type: ApiType;
35
+ /** Response only — undefined on the 'request' tag. */
36
+ 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,
42
+ /** 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);
46
+ }
@@ -0,0 +1,47 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.ApiCallInfo = void 0;
26
+ class ApiCallInfo {
27
+ side;
28
+ type;
29
+ result;
30
+ path;
31
+ method;
32
+ controller;
33
+ constructor(side, type,
34
+ /** 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;
39
+ this.type = type;
40
+ this.result = result;
41
+ this.path = path;
42
+ this.method = method;
43
+ this.controller = controller;
44
+ }
45
+ }
46
+ exports.ApiCallInfo = ApiCallInfo;
47
+ //# sourceMappingURL=ApiCallInfo.js.map
@@ -0,0 +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 only — undefined 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"]}
@@ -6,6 +6,13 @@ import { ContextKey } from '../ContextKey';
6
6
  * A lambda, not an object — nothing here needs an implementation to hold.
7
7
  */
8
8
  export type ContextRead = (key: ContextKey) => string | undefined;
9
+ /**
10
+ * Reads one context key's value WITHOUT narrowing to string — a key may hold an object
11
+ * (e.g. {@link ApiCallInfo} under API_CALL_INFO). Used only by
12
+ * {@link HeaderRegistry.buildStructuredLogFields} for the node logging backends, which can emit
13
+ * object values as nested `jsonPayload.<name>`. Server passes `RequestContext.getHeader`.
14
+ */
15
+ export type StructuredContextRead = (key: ContextKey) => unknown;
9
16
  /**
10
17
  * ContextReader - reads context-key values from an app-held store.
11
18
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ContextReader.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ContextReader.ts"],"names":[],"mappings":"","sourcesContent":["import { ContextKey } from '../ContextKey';\n\n/**\n * Reads one context key's string value. The ONE seam between the two environments:\n * the server passes `RequestContext.getHeader`, a browser passes its store's read.\n *\n * A lambda, not an object — nothing here needs an implementation to hold.\n */\nexport type ContextRead = (key: ContextKey) => string | undefined;\n\n/**\n * ContextReader - reads context-key values from an app-held store.\n *\n * BROWSER-ONLY. Browsers have no AsyncLocalStorage and therefore no ambient request scope, so the\n * app holds a `MutableContextStore` (in @webpieces/http-client-browser) and sets values as they\n * become known (login token, tenant, ...).\n *\n * The server has no use for this: there is exactly one right answer there, so `RequestContextHeaders`\n * (in @webpieces/core-context) reads `RequestContext` directly rather than through a reader object.\n *\n * Note this is only about where VALUES live. The key SCHEMA — which keys exist, which transfer, which\n * are secured — is the global {@link HeaderRegistry}, and that is browser-safe and shared by both.\n *\n * This is a business-logic interface (per CLAUDE.md: behavior = interface).\n */\nexport interface ContextReader {\n /**\n * Read the string value of a context key. Returns undefined if not present.\n */\n read(key: ContextKey): string | undefined;\n\n /**\n * OPTIONAL: read a non-string context value (e.g. the active TestCaseRecorder\n * under RecorderKeys.RECORDER). Server-side readers implement this over the\n * RequestContext; browser readers may omit it (no server-side recording in\n * browsers — same as Java).\n */\n // webpieces-disable no-any-unknown -- context values are heterogeneous (recorder, meta objects)\n readValue?(key: ContextKey): unknown;\n}\n"]}
1
+ {"version":3,"file":"ContextReader.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ContextReader.ts"],"names":[],"mappings":"","sourcesContent":["import { ContextKey } from '../ContextKey';\n\n/**\n * Reads one context key's string value. The ONE seam between the two environments:\n * the server passes `RequestContext.getHeader`, a browser passes its store's read.\n *\n * A lambda, not an object — nothing here needs an implementation to hold.\n */\nexport type ContextRead = (key: ContextKey) => string | undefined;\n\n/**\n * Reads one context key's value WITHOUT narrowing to string — a key may hold an object\n * (e.g. {@link ApiCallInfo} under API_CALL_INFO). Used only by\n * {@link HeaderRegistry.buildStructuredLogFields} for the node logging backends, which can emit\n * object values as nested `jsonPayload.<name>`. Server passes `RequestContext.getHeader`.\n */\n// webpieces-disable no-any-unknown -- structured log values are heterogeneous (strings + api struct)\nexport type StructuredContextRead = (key: ContextKey) => unknown;\n\n/**\n * ContextReader - reads context-key values from an app-held store.\n *\n * BROWSER-ONLY. Browsers have no AsyncLocalStorage and therefore no ambient request scope, so the\n * app holds a `MutableContextStore` (in @webpieces/http-client-browser) and sets values as they\n * become known (login token, tenant, ...).\n *\n * The server has no use for this: there is exactly one right answer there, so `RequestContextHeaders`\n * (in @webpieces/core-context) reads `RequestContext` directly rather than through a reader object.\n *\n * Note this is only about where VALUES live. The key SCHEMA — which keys exist, which transfer, which\n * are secured — is the global {@link HeaderRegistry}, and that is browser-safe and shared by both.\n *\n * This is a business-logic interface (per CLAUDE.md: behavior = interface).\n */\nexport interface ContextReader {\n /**\n * Read the string value of a context key. Returns undefined if not present.\n */\n read(key: ContextKey): string | undefined;\n\n /**\n * OPTIONAL: read a non-string context value (e.g. the active TestCaseRecorder\n * under RecorderKeys.RECORDER). Server-side readers implement this over the\n * RequestContext; browser readers may omit it (no server-side recording in\n * browsers — same as Java).\n */\n // webpieces-disable no-any-unknown -- context values are heterogeneous (recorder, meta objects)\n readValue?(key: ContextKey): unknown;\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { ContextKey } from '../ContextKey';
2
- import { ContextRead } from './ContextReader';
2
+ import { ContextRead, StructuredContextRead } from './ContextReader';
3
3
  /**
4
4
  * HeaderRegistry - the single, GLOBAL source of truth for every ContextKey the
5
5
  * platform knows about. Port of Java webpieces' HeaderTranslation.
@@ -65,6 +65,18 @@ export declare class HeaderRegistry {
65
65
  * getLoggedKeys() is precomputed at configure() time, so this is hot-path safe.
66
66
  */
67
67
  buildLogFields(read: ContextRead): Map<string, string>;
68
+ /**
69
+ * The STRUCTURED log-field map: like {@link buildLogFields}, but values may be objects, so an
70
+ * object-valued logged key (e.g. {@link ApiCallInfo} under API_CALL_INFO) survives as an object
71
+ * instead of being dropped. The node logging backends (winston/bunyan) call THIS — they
72
+ * JSON-serialize the whole record, so an object value nests into `jsonPayload.<name>` (→
73
+ * filterable `jsonPayload.api.side`, `.result`, ...). String values are still masked per key.
74
+ *
75
+ * The flat {@link buildLogFields} stays string-only for wire/MDC/recorder-fixture callers, which
76
+ * must not carry heterogeneous objects. This is the deliberate second builder (registry stays the
77
+ * single source of truth for WHICH keys log; the two builders differ only in value SHAPE).
78
+ */
79
+ buildStructuredLogFields(read: StructuredContextRead): Map<string, string | object>;
68
80
  /** Look up a key by its HTTP header name (case-insensitive). O(1) via the precomputed map. */
69
81
  findByHttpHeader(httpHeader: string): ContextKey | undefined;
70
82
  /**
@@ -106,12 +106,47 @@ class HeaderRegistry {
106
106
  const fields = new Map();
107
107
  for (const key of this.getLoggedKeys()) {
108
108
  const value = read(key);
109
- if (value) {
109
+ // typeof-string guard: an object-valued logged key (API_CALL_INFO) must NOT leak into the
110
+ // flat string map — this map feeds wire/MDC + recorder fixtures, which are string-only.
111
+ // `read` is typed string-returning, but a key like API_CALL_INFO actually holds an object at
112
+ // runtime; the guard keeps it out. Object-valued keys ride buildStructuredLogFields instead.
113
+ if (typeof value === 'string' && value) {
110
114
  fields.set(key.name, key.maskIfSecured(value));
111
115
  }
112
116
  }
113
117
  return fields;
114
118
  }
119
+ /**
120
+ * The STRUCTURED log-field map: like {@link buildLogFields}, but values may be objects, so an
121
+ * object-valued logged key (e.g. {@link ApiCallInfo} under API_CALL_INFO) survives as an object
122
+ * instead of being dropped. The node logging backends (winston/bunyan) call THIS — they
123
+ * JSON-serialize the whole record, so an object value nests into `jsonPayload.<name>` (→
124
+ * filterable `jsonPayload.api.side`, `.result`, ...). String values are still masked per key.
125
+ *
126
+ * The flat {@link buildLogFields} stays string-only for wire/MDC/recorder-fixture callers, which
127
+ * must not carry heterogeneous objects. This is the deliberate second builder (registry stays the
128
+ * single source of truth for WHICH keys log; the two builders differ only in value SHAPE).
129
+ */
130
+ buildStructuredLogFields(read) {
131
+ const fields = new Map();
132
+ for (const key of this.getLoggedKeys()) {
133
+ const value = read(key);
134
+ if (value === undefined || value === null) {
135
+ continue;
136
+ }
137
+ if (typeof value === 'string') {
138
+ if (value) {
139
+ fields.set(key.name, key.maskIfSecured(value));
140
+ }
141
+ }
142
+ else if (typeof value === 'object') {
143
+ fields.set(key.name, value);
144
+ }
145
+ // Non-string primitives (number/boolean/bigint) are not expected for logged context keys;
146
+ // ignore them here rather than String()-flattening, keeping the shape honest.
147
+ }
148
+ return fields;
149
+ }
115
150
  /** Look up a key by its HTTP header name (case-insensitive). O(1) via the precomputed map. */
116
151
  findByHttpHeader(httpHeader) {
117
152
  return this.byHttpHeader.get(httpHeader.toLowerCase());
@@ -1 +1 @@
1
- {"version":3,"file":"HeaderRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HeaderRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ContextKey } from '../ContextKey';\nimport { ContextRead } from './ContextReader';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\n\n/**\n * HeaderRegistry - the single, GLOBAL source of truth for every ContextKey the\n * platform knows about. Port of Java webpieces' HeaderTranslation.\n *\n * Configured exactly like {@link LogManager} — once, at process startup — and then\n * globally accessible. There is NO DI wiring: filters/clients call\n * `HeaderRegistry.get()` instead of injecting it.\n *\n * ```ts\n * // startup (server AND browser), BEFORE LogManager.setFactory(...):\n * HeaderRegistry.configure(CompanyHeaders.getAllHeaders(), true);\n * ```\n *\n * - `svrHeaders` the context keys this process registers — by convention the whole\n * company-wide set (all keys across all servers), e.g. CompanyHeaders.\n * - `platformHeaders` when true, also include {@link HeaderRegistry.DEFAULT_HEADERS}\n * (the webpieces common keys: request-id, correlation-id, ...).\n *\n * Duplicate validation (port of Java checkForDuplicates) runs at configure() time,\n * so conflicting definitions fail fast at startup:\n * - Two keys with the same `name` must agree on httpHeader/isSecured/isLogged.\n * - Two keys with the same `httpHeader` must agree on `name`.\n * - Exact duplicates collapse to one entry.\n */\nexport class HeaderRegistry {\n /** The webpieces-supplied common keys; included when platformHeaders=true. */\n static readonly DEFAULT_HEADERS: ContextKey[] = WebpiecesCoreHeaders.getAllHeaders();\n\n private static instance: HeaderRegistry | undefined;\n\n private readonly keys: ContextKey[];\n\n // Derived collections precomputed ONCE, here in the constructor (i.e. at\n // configure() time). The hot path — every log line calls getLoggedKeys(), every\n // outbound request calls getTransferredKeys() — then returns the cached array\n // instead of re-filtering the full key list on each call. These are reachable\n // only through HeaderRegistry.get(), which throws until configure() has run.\n private readonly transferredKeys: ContextKey[];\n private readonly securedNames: string[];\n private readonly loggedKeys: ContextKey[];\n private readonly byHttpHeader: Map<string, ContextKey>;\n\n private constructor(keys: ContextKey[]) {\n this.keys = this.checkForDuplicates(keys);\n this.transferredKeys = this.keys.filter((k: ContextKey) => k.httpHeader !== undefined);\n this.securedNames = this.keys\n .filter((k: ContextKey) => k.isSecured)\n .map((k: ContextKey) => k.name);\n this.loggedKeys = this.keys.filter((k: ContextKey) => k.isLogged);\n this.byHttpHeader = new Map(\n this.transferredKeys.map((k: ContextKey): [string, ContextKey] => [k.httpHeader!.toLowerCase(), k]),\n );\n }\n\n /**\n * Install the process-wide registry. Call once at startup, BEFORE\n * LogManager.setFactory(...) (logging masks/keys off this registry).\n */\n // webpieces-disable no-function-outside-class -- HeaderRegistry is a deliberately static global singleton (like LogManager); configured once at startup, never DI-injected\n static configure(svrHeaders: ContextKey[], platformHeaders: boolean): void {\n const all: ContextKey[] = [\n ...(platformHeaders ? HeaderRegistry.DEFAULT_HEADERS : []),\n ...svrHeaders,\n ];\n HeaderRegistry.instance = new HeaderRegistry(all);\n }\n\n /** The configured registry. Throws if configure() has not been called. */\n static get(): HeaderRegistry {\n if (!HeaderRegistry.instance) {\n throw new Error(\n 'HeaderRegistry.configure(...) has not been called. Configure the registry ' +\n 'at startup (before LogManager.setFactory) so filters/logging know the context keys.',\n );\n }\n return HeaderRegistry.instance;\n }\n\n /** True once configure() has run. Used by LogManager.setFactory to fail fast. */\n static isConfigured(): boolean {\n return HeaderRegistry.instance !== undefined;\n }\n\n /** All registered keys (deduplicated). */\n getKeys(): ContextKey[] {\n return this.keys;\n }\n\n /**\n * Keys that transfer over the wire (inbound request -> context, and context ->\n * outbound request): those with an httpHeader set.\n */\n getTransferredKeys(): ContextKey[] {\n return this.transferredKeys;\n }\n\n /** Names (log keys) whose values must be masked in logs. isSecured=true. */\n getSecuredNames(): string[] {\n return this.securedNames;\n }\n\n /** Keys that appear in logs. isLogged=true. */\n getLoggedKeys(): ContextKey[] {\n return this.loggedKeys;\n }\n\n /**\n * The log/MDC field map: every logged key with a value, under its `name`, secured values masked.\n *\n * The ONE implementation, because the registry owns the keys and each {@link ContextKey} knows\n * how to mask its own value. Callers differ only in WHERE a value is read from:\n * `RequestContext.buildLogFields()` passes its own getHeader (server), and `ContextMgr` passes\n * the app-held store's read (browser).\n *\n * getLoggedKeys() is precomputed at configure() time, so this is hot-path safe.\n */\n buildLogFields(read: ContextRead): Map<string, string> {\n const fields = new Map<string, string>();\n for (const key of this.getLoggedKeys()) {\n const value = read(key);\n if (value) {\n fields.set(key.name, key.maskIfSecured(value));\n }\n }\n return fields;\n }\n\n /** Look up a key by its HTTP header name (case-insensitive). O(1) via the precomputed map. */\n findByHttpHeader(httpHeader: string): ContextKey | undefined {\n return this.byHttpHeader.get(httpHeader.toLowerCase());\n }\n\n /**\n * Collapse exact duplicates, throw on conflicting definitions sharing a `name`\n * or an `httpHeader`.\n */\n private checkForDuplicates(allKeys: ContextKey[]): ContextKey[] {\n const byName = new Map<string, ContextKey>();\n const byHttpHeader = new Map<string, ContextKey>();\n\n for (const key of allKeys) {\n const nameKey = key.name.toLowerCase();\n const existing = byName.get(nameKey);\n if (existing) {\n this.assertSameDefinition(existing, key);\n continue; // exact duplicate - collapse\n }\n byName.set(nameKey, key);\n\n if (key.httpHeader !== undefined) {\n const headerKey = key.httpHeader.toLowerCase();\n const clash = byHttpHeader.get(headerKey);\n if (clash) {\n throw new Error(\n `Duplicate ContextKey httpHeader '${key.httpHeader}': ` +\n `defined by key '${clash.name}' AND key '${key.name}'. ` +\n `Each HTTP header must map to exactly one context key.`,\n );\n }\n byHttpHeader.set(headerKey, key);\n }\n }\n\n return Array.from(byName.values());\n }\n\n /**\n * Two keys sharing a `name` must agree on httpHeader/isSecured/isLogged,\n * otherwise the platform would behave differently depending on which module's\n * definition happened to load first.\n */\n private assertSameDefinition(existing: ContextKey, duplicate: ContextKey): void {\n const conflicts: string[] = [];\n if (existing.httpHeader !== duplicate.httpHeader) {\n conflicts.push(`httpHeader ('${existing.httpHeader}' vs '${duplicate.httpHeader}')`);\n }\n if (existing.isSecured !== duplicate.isSecured) {\n conflicts.push(`isSecured (${existing.isSecured} vs ${duplicate.isSecured})`);\n }\n if (existing.isLogged !== duplicate.isLogged) {\n conflicts.push(`isLogged (${existing.isLogged} vs ${duplicate.isLogged})`);\n }\n if (conflicts.length > 0) {\n throw new Error(\n `Conflicting ContextKey definitions for '${existing.name}': ` +\n `two modules registered it with different ${conflicts.join(', ')}. ` +\n `Keys sharing a name must agree on all flags.`,\n );\n }\n }\n}\n"]}
1
+ {"version":3,"file":"HeaderRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HeaderRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ContextKey } from '../ContextKey';\nimport { ContextRead, StructuredContextRead } from './ContextReader';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\n\n/**\n * HeaderRegistry - the single, GLOBAL source of truth for every ContextKey the\n * platform knows about. Port of Java webpieces' HeaderTranslation.\n *\n * Configured exactly like {@link LogManager} — once, at process startup — and then\n * globally accessible. There is NO DI wiring: filters/clients call\n * `HeaderRegistry.get()` instead of injecting it.\n *\n * ```ts\n * // startup (server AND browser), BEFORE LogManager.setFactory(...):\n * HeaderRegistry.configure(CompanyHeaders.getAllHeaders(), true);\n * ```\n *\n * - `svrHeaders` the context keys this process registers — by convention the whole\n * company-wide set (all keys across all servers), e.g. CompanyHeaders.\n * - `platformHeaders` when true, also include {@link HeaderRegistry.DEFAULT_HEADERS}\n * (the webpieces common keys: request-id, correlation-id, ...).\n *\n * Duplicate validation (port of Java checkForDuplicates) runs at configure() time,\n * so conflicting definitions fail fast at startup:\n * - Two keys with the same `name` must agree on httpHeader/isSecured/isLogged.\n * - Two keys with the same `httpHeader` must agree on `name`.\n * - Exact duplicates collapse to one entry.\n */\nexport class HeaderRegistry {\n /** The webpieces-supplied common keys; included when platformHeaders=true. */\n static readonly DEFAULT_HEADERS: ContextKey[] = WebpiecesCoreHeaders.getAllHeaders();\n\n private static instance: HeaderRegistry | undefined;\n\n private readonly keys: ContextKey[];\n\n // Derived collections precomputed ONCE, here in the constructor (i.e. at\n // configure() time). The hot path — every log line calls getLoggedKeys(), every\n // outbound request calls getTransferredKeys() — then returns the cached array\n // instead of re-filtering the full key list on each call. These are reachable\n // only through HeaderRegistry.get(), which throws until configure() has run.\n private readonly transferredKeys: ContextKey[];\n private readonly securedNames: string[];\n private readonly loggedKeys: ContextKey[];\n private readonly byHttpHeader: Map<string, ContextKey>;\n\n private constructor(keys: ContextKey[]) {\n this.keys = this.checkForDuplicates(keys);\n this.transferredKeys = this.keys.filter((k: ContextKey) => k.httpHeader !== undefined);\n this.securedNames = this.keys\n .filter((k: ContextKey) => k.isSecured)\n .map((k: ContextKey) => k.name);\n this.loggedKeys = this.keys.filter((k: ContextKey) => k.isLogged);\n this.byHttpHeader = new Map(\n this.transferredKeys.map((k: ContextKey): [string, ContextKey] => [k.httpHeader!.toLowerCase(), k]),\n );\n }\n\n /**\n * Install the process-wide registry. Call once at startup, BEFORE\n * LogManager.setFactory(...) (logging masks/keys off this registry).\n */\n // webpieces-disable no-function-outside-class -- HeaderRegistry is a deliberately static global singleton (like LogManager); configured once at startup, never DI-injected\n static configure(svrHeaders: ContextKey[], platformHeaders: boolean): void {\n const all: ContextKey[] = [\n ...(platformHeaders ? HeaderRegistry.DEFAULT_HEADERS : []),\n ...svrHeaders,\n ];\n HeaderRegistry.instance = new HeaderRegistry(all);\n }\n\n /** The configured registry. Throws if configure() has not been called. */\n static get(): HeaderRegistry {\n if (!HeaderRegistry.instance) {\n throw new Error(\n 'HeaderRegistry.configure(...) has not been called. Configure the registry ' +\n 'at startup (before LogManager.setFactory) so filters/logging know the context keys.',\n );\n }\n return HeaderRegistry.instance;\n }\n\n /** True once configure() has run. Used by LogManager.setFactory to fail fast. */\n static isConfigured(): boolean {\n return HeaderRegistry.instance !== undefined;\n }\n\n /** All registered keys (deduplicated). */\n getKeys(): ContextKey[] {\n return this.keys;\n }\n\n /**\n * Keys that transfer over the wire (inbound request -> context, and context ->\n * outbound request): those with an httpHeader set.\n */\n getTransferredKeys(): ContextKey[] {\n return this.transferredKeys;\n }\n\n /** Names (log keys) whose values must be masked in logs. isSecured=true. */\n getSecuredNames(): string[] {\n return this.securedNames;\n }\n\n /** Keys that appear in logs. isLogged=true. */\n getLoggedKeys(): ContextKey[] {\n return this.loggedKeys;\n }\n\n /**\n * The log/MDC field map: every logged key with a value, under its `name`, secured values masked.\n *\n * The ONE implementation, because the registry owns the keys and each {@link ContextKey} knows\n * how to mask its own value. Callers differ only in WHERE a value is read from:\n * `RequestContext.buildLogFields()` passes its own getHeader (server), and `ContextMgr` passes\n * the app-held store's read (browser).\n *\n * getLoggedKeys() is precomputed at configure() time, so this is hot-path safe.\n */\n buildLogFields(read: ContextRead): Map<string, string> {\n const fields = new Map<string, string>();\n for (const key of this.getLoggedKeys()) {\n const value = read(key);\n // typeof-string guard: an object-valued logged key (API_CALL_INFO) must NOT leak into the\n // flat string map — this map feeds wire/MDC + recorder fixtures, which are string-only.\n // `read` is typed string-returning, but a key like API_CALL_INFO actually holds an object at\n // runtime; the guard keeps it out. Object-valued keys ride buildStructuredLogFields instead.\n if (typeof value === 'string' && value) {\n fields.set(key.name, key.maskIfSecured(value));\n }\n }\n return fields;\n }\n\n /**\n * The STRUCTURED log-field map: like {@link buildLogFields}, but values may be objects, so an\n * object-valued logged key (e.g. {@link ApiCallInfo} under API_CALL_INFO) survives as an object\n * instead of being dropped. The node logging backends (winston/bunyan) call THIS — they\n * JSON-serialize the whole record, so an object value nests into `jsonPayload.<name>` (→\n * filterable `jsonPayload.api.side`, `.result`, ...). String values are still masked per key.\n *\n * The flat {@link buildLogFields} stays string-only for wire/MDC/recorder-fixture callers, which\n * must not carry heterogeneous objects. This is the deliberate second builder (registry stays the\n * single source of truth for WHICH keys log; the two builders differ only in value SHAPE).\n */\n buildStructuredLogFields(read: StructuredContextRead): Map<string, string | object> {\n const fields = new Map<string, string | object>();\n for (const key of this.getLoggedKeys()) {\n const value = read(key);\n if (value === undefined || value === null) {\n continue;\n }\n if (typeof value === 'string') {\n if (value) {\n fields.set(key.name, key.maskIfSecured(value));\n }\n } else if (typeof value === 'object') {\n fields.set(key.name, value);\n }\n // Non-string primitives (number/boolean/bigint) are not expected for logged context keys;\n // ignore them here rather than String()-flattening, keeping the shape honest.\n }\n return fields;\n }\n\n /** Look up a key by its HTTP header name (case-insensitive). O(1) via the precomputed map. */\n findByHttpHeader(httpHeader: string): ContextKey | undefined {\n return this.byHttpHeader.get(httpHeader.toLowerCase());\n }\n\n /**\n * Collapse exact duplicates, throw on conflicting definitions sharing a `name`\n * or an `httpHeader`.\n */\n private checkForDuplicates(allKeys: ContextKey[]): ContextKey[] {\n const byName = new Map<string, ContextKey>();\n const byHttpHeader = new Map<string, ContextKey>();\n\n for (const key of allKeys) {\n const nameKey = key.name.toLowerCase();\n const existing = byName.get(nameKey);\n if (existing) {\n this.assertSameDefinition(existing, key);\n continue; // exact duplicate - collapse\n }\n byName.set(nameKey, key);\n\n if (key.httpHeader !== undefined) {\n const headerKey = key.httpHeader.toLowerCase();\n const clash = byHttpHeader.get(headerKey);\n if (clash) {\n throw new Error(\n `Duplicate ContextKey httpHeader '${key.httpHeader}': ` +\n `defined by key '${clash.name}' AND key '${key.name}'. ` +\n `Each HTTP header must map to exactly one context key.`,\n );\n }\n byHttpHeader.set(headerKey, key);\n }\n }\n\n return Array.from(byName.values());\n }\n\n /**\n * Two keys sharing a `name` must agree on httpHeader/isSecured/isLogged,\n * otherwise the platform would behave differently depending on which module's\n * definition happened to load first.\n */\n private assertSameDefinition(existing: ContextKey, duplicate: ContextKey): void {\n const conflicts: string[] = [];\n if (existing.httpHeader !== duplicate.httpHeader) {\n conflicts.push(`httpHeader ('${existing.httpHeader}' vs '${duplicate.httpHeader}')`);\n }\n if (existing.isSecured !== duplicate.isSecured) {\n conflicts.push(`isSecured (${existing.isSecured} vs ${duplicate.isSecured})`);\n }\n if (existing.isLogged !== duplicate.isLogged) {\n conflicts.push(`isLogged (${existing.isLogged} vs ${duplicate.isLogged})`);\n }\n if (conflicts.length > 0) {\n throw new Error(\n `Conflicting ContextKey definitions for '${existing.name}': ` +\n `two modules registered it with different ${conflicts.join(', ')}. ` +\n `Keys sharing a name must agree on all flags.`,\n );\n }\n }\n}\n"]}
@@ -1,43 +1,82 @@
1
1
  import { RouteMetadata } from "./decorators";
2
+ import { ApiSide } from "./ApiCallInfo";
2
3
  /**
3
- * LogApiCall - Generic API call logging utility.
4
+ * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and
5
+ * client-side (ProxyClient) for one consistent logging shape across the framework.
4
6
  *
5
- * Used by both server-side (LogApiFilter) and client-side (ClientFactory) for
6
- * consistent logging patterns across the framework.
7
+ * TWO things happen around each call:
8
+ * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).
9
+ * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the
10
+ * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the
11
+ * req/resp lines) inherits a filterable `api` object — surfacing in GCP as
12
+ * `jsonPayload.api.{side,type,result,path,method}`.
13
+ *
14
+ * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →
15
+ * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).
16
+ * It stamps through the {@link ApiCallContext} seam instead: `setupRuntime` installs a
17
+ * RequestContext-backed impl on a Node server, and `ClientHttpBrowserFactory` a module-global impl in a
18
+ * browser. If neither ran, {@link ApiCallContextHolder.get} throws (loud misconfiguration).
19
+ *
20
+ * Singleton, mirroring `RequestContext`: use the exported {@link LogApiCall} constant, not `new`.
7
21
  *
8
22
  * Logging format patterns:
9
- * - [API-{type}-req] ClassName.methodName request={...} headers={...}
10
- * - [API-{type}-resp-SUCCESS] ClassName.methodName response={...}
11
- * - [API-{type}-resp-OTHER] ClassName.methodName errorType={...} (user errors)
12
- * - [API-{type}-resp-FAIL] ClassName.methodName error={...} (server errors)
23
+ * - [API-{side}-req] ClassName.methodName request={...}
24
+ * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}
25
+ * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} (user errors)
26
+ * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (server errors)
13
27
  */
14
- export declare class LogApiCall {
28
+ export declare class LogApiCallImpl {
15
29
  /**
16
- * Execute an API call with logging around it.
30
+ * Execute an API call with logging + `api` context-tagging around it.
17
31
  *
18
- * @param type - 'SVR' or 'CLIENT'
32
+ * @param side - 'client' (outbound call this process made) or 'server' (inbound call it handled)
19
33
  * @param meta - Route metadata with controllerClassName and methodName
20
34
  * @param requestDto - The request DTO
21
35
  * @param method - The method to execute
22
36
  *
23
- * Context fields (requestId, tenantId, ...) are NOT stamped here. A logging BACKEND owns that:
24
- * bunyan/winston read RequestContext.buildLogFields() on every record. The bootstrap
25
- * ConsoleLogger deliberately carries no context.
37
+ * Correlation fields (requestId, tenantId, ...) are NOT stamped here a logging BACKEND owns that,
38
+ * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only
39
+ * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across
40
+ * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber
41
+ * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is
42
+ * exactly what the GCP filters (`jsonPayload.api.*`) want.
26
43
  */
27
- execute(type: string, meta: RouteMetadata, requestDto: any, method: (dto: any) => Promise<any>): Promise<any>;
44
+ execute(side: ApiSide, meta: RouteMetadata, requestDto: any, method: (dto: any) => Promise<any>): Promise<any>;
28
45
  /**
29
- * Check if an error is a user error (expected behavior from server perspective).
30
- * User errors are NOT failures - just users making mistakes or validation issues.
46
+ * Is this error a NON-failure for HEALTH/METRICS the process working CORRECTLY (log OTHER, api
47
+ * result:'success') rather than a real failure to surface (log FAIL, result:'failure')?
48
+ *
49
+ * The question is "are things WORKING?", NOT "was it an HTTP 4xx vs 5xx" — and the two are not the
50
+ * same (see 408 below). Classification is by portable Error TYPE, never by transport: LogApiCall runs
51
+ * deep in the stack (in-process calls, pubsub/queue handlers, HTTP — one code path) and only ever
52
+ * sees a thrown Error. The Http* classes are poorly named for that (the `Http` prefix is historical),
53
+ * but they ARE portable error types that travel with the throw anywhere, so matching the TYPE works
54
+ * with or without any HTTP in the picture. Do NOT swap this for an `error.code` 4xx range check — that
55
+ * both re-couples to HTTP AND would bury 408.
56
+ *
57
+ * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
58
+ * - HttpBadRequestError (400) "your request is malformed"
59
+ * - HttpUnauthorizedError (401) "you're not authenticated"
60
+ * - HttpForbiddenError (403) "authenticated, but not allowed"
61
+ * - HttpNotFoundError (404) "wrong url / no such entity" (EndpointNotFoundError is a subclass)
62
+ * → the server is fine, the caller erred → result:'success', logged OTHER.
31
63
  *
32
- * User errors (logged as OTHER, no stack trace):
33
- * - HttpBadRequestError (400)
34
- * - HttpUnauthorizedError (401)
35
- * - HttpForbiddenError (403)
36
- * - HttpNotFoundError (404)
37
- * - HttpUserError (266)
64
+ * SERVER something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):
65
+ * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
66
+ * absent from the list below so it counts as a failure.
67
+ * - 500 / 502 / 504 / 598, and any non-Http Error: real failures.
38
68
  *
39
- * @param error - The error to check
40
- * @returns true if this is a user error, false for server errors
69
+ * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake" signal.
70
+ * CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.
71
+ *
72
+ * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)
73
+ * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call
74
+ * @returns true if this should be treated as a non-failure (OTHER / result:'success')
41
75
  */
42
- static isUserError(error: unknown): boolean;
76
+ isUserError(error: Error, server: boolean): boolean;
43
77
  }
78
+ /**
79
+ * The process-wide {@link LogApiCallImpl} singleton — mirrors the `RequestContext` export pattern.
80
+ * Callers use `LogApiCall.execute(...)`, never `new`.
81
+ */
82
+ export declare const LogApiCall: LogApiCallImpl;
@@ -1,83 +1,154 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LogApiCall = 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");
7
+ const ApiCallInfo_1 = require("./ApiCallInfo");
8
+ const ApiCallContext_1 = require("./ApiCallContext");
9
+ const WebpiecesCoreHeaders_1 = require("./WebpiecesCoreHeaders");
7
10
  const log = LogManager_1.LogManager.getLogger('LogApiCall');
8
11
  /**
9
- * LogApiCall - Generic API call logging utility.
12
+ * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and
13
+ * client-side (ProxyClient) for one consistent logging shape across the framework.
10
14
  *
11
- * Used by both server-side (LogApiFilter) and client-side (ClientFactory) for
12
- * consistent logging patterns across the framework.
15
+ * TWO things happen around each call:
16
+ * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).
17
+ * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the
18
+ * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the
19
+ * req/resp lines) inherits a filterable `api` object — surfacing in GCP as
20
+ * `jsonPayload.api.{side,type,result,path,method}`.
21
+ *
22
+ * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →
23
+ * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).
24
+ * It stamps through the {@link ApiCallContext} seam instead: `setupRuntime` installs a
25
+ * RequestContext-backed impl on a Node server, and `ClientHttpBrowserFactory` a module-global impl in a
26
+ * browser. If neither ran, {@link ApiCallContextHolder.get} throws (loud misconfiguration).
27
+ *
28
+ * Singleton, mirroring `RequestContext`: use the exported {@link LogApiCall} constant, not `new`.
13
29
  *
14
30
  * Logging format patterns:
15
- * - [API-{type}-req] ClassName.methodName request={...} headers={...}
16
- * - [API-{type}-resp-SUCCESS] ClassName.methodName response={...}
17
- * - [API-{type}-resp-OTHER] ClassName.methodName errorType={...} (user errors)
18
- * - [API-{type}-resp-FAIL] ClassName.methodName error={...} (server errors)
31
+ * - [API-{side}-req] ClassName.methodName request={...}
32
+ * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}
33
+ * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} (user errors)
34
+ * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (server errors)
19
35
  */
20
- class LogApiCall {
36
+ class LogApiCallImpl {
21
37
  /**
22
- * Execute an API call with logging around it.
38
+ * Execute an API call with logging + `api` context-tagging around it.
23
39
  *
24
- * @param type - 'SVR' or 'CLIENT'
40
+ * @param side - 'client' (outbound call this process made) or 'server' (inbound call it handled)
25
41
  * @param meta - Route metadata with controllerClassName and methodName
26
42
  * @param requestDto - The request DTO
27
43
  * @param method - The method to execute
28
44
  *
29
- * Context fields (requestId, tenantId, ...) are NOT stamped here. A logging BACKEND owns that:
30
- * bunyan/winston read RequestContext.buildLogFields() on every record. The bootstrap
31
- * ConsoleLogger deliberately carries no context.
45
+ * Correlation fields (requestId, tenantId, ...) are NOT stamped here a logging BACKEND owns that,
46
+ * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only
47
+ * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across
48
+ * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber
49
+ * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is
50
+ * exactly what the GCP filters (`jsonPayload.api.*`) want.
32
51
  */
33
- async execute(type, meta, requestDto, method) {
34
- log.info(`[API-${type}-req] ${meta.controllerClassName}.${meta.methodName} ${meta.path} request=${JSON.stringify(requestDto)}`);
52
+ async execute(side, meta,
53
+ // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches ProxyClient)
54
+ requestDto,
55
+ // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary
56
+ method
57
+ // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary
58
+ ) {
59
+ // Throws if no ApiCallContext was installed at startup, or there is no active scope to stamp
60
+ // into (loud misconfiguration — an api call with nowhere to tag is a bug).
61
+ const ctx = ApiCallContext_1.ApiCallContextHolder.get();
62
+ if (!ctx.isActive()) {
63
+ throw new Error('LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside the ' +
64
+ 'RequestContext.run(...) a server filter opens; in a browser, build ClientHttpBrowserFactory first.');
65
+ }
66
+ const key = WebpiecesCoreHeaders_1.WebpiecesCoreHeaders.API_CALL_INFO;
67
+ const server = side === 'server';
68
+ const cls = meta.controllerClassName;
69
+ // set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,
70
+ // never across an await, so a single browser global slot can never be clobbered by a concurrent call.
71
+ const stamp = (info, emit) => {
72
+ ctx.set(key, info);
73
+ emit();
74
+ ctx.remove(key);
75
+ };
35
76
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller
36
77
  try {
78
+ 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)}`));
37
79
  if (!requestDto)
38
- throw new Error(`Request cannot be null and was from ${meta.controllerClassName}.${meta.methodName}`);
80
+ throw new Error(`Request cannot be null and was from ${cls}.${meta.methodName}`);
39
81
  const response = await method(requestDto);
40
82
  if (!response)
41
- throw new Error(`Response cannot be null and was from ${meta.controllerClassName}.${meta.methodName}`);
42
- // Log success response
43
- log.info(`[API-${type}-resp-SUCCESS] ${meta.controllerClassName}.${meta.methodName} response=${JSON.stringify(response)}`);
83
+ throw new Error(`Response cannot be null and was from ${cls}.${meta.methodName}`);
84
+ 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)}`));
44
85
  return response;
45
86
  }
46
87
  catch (err) {
47
88
  const error = (0, errorUtils_1.toError)(err);
48
89
  const errorType = error.constructor.name;
49
90
  const errorMessage = error.message;
50
- // Log error based on type and re-throw
51
- if (LogApiCall.isUserError(error)) {
52
- log.warn(`[API-${type}-resp-OTHER] ${meta.controllerClassName}.${meta.methodName} errorType=${errorType}`);
53
- }
54
- else {
55
- log.error(`[API-${type}-resp-FAIL] ${meta.controllerClassName}.${meta.methodName} errorType=${errorType} error=${errorMessage}`);
56
- }
91
+ // Side-dependent: a 4xx the SERVER raised is a handled non-failure (OTHER); the same 4xx a
92
+ // CLIENT receives means its call FAILED. HttpUserError (266) is never a failure, either side.
93
+ const isUser = this.isUserError(error, server);
94
+ stamp(new ApiCallInfo_1.ApiCallInfo(side, 'response', isUser ? 'success' : 'failure', meta.path, meta.methodName, cls), () => isUser
95
+ ? log.warn(`[API-${side}-resp-OTHER] ${cls}.${meta.methodName} errorType=${errorType}`)
96
+ : log.error(`[API-${side}-resp-FAIL] ${cls}.${meta.methodName} errorType=${errorType} error=${errorMessage}`));
57
97
  throw error;
58
98
  }
59
99
  }
60
100
  /**
61
- * Check if an error is a user error (expected behavior from server perspective).
62
- * User errors are NOT failures - just users making mistakes or validation issues.
101
+ * Is this error a NON-failure for HEALTH/METRICS the process working CORRECTLY (log OTHER, api
102
+ * result:'success') rather than a real failure to surface (log FAIL, result:'failure')?
103
+ *
104
+ * The question is "are things WORKING?", NOT "was it an HTTP 4xx vs 5xx" — and the two are not the
105
+ * same (see 408 below). Classification is by portable Error TYPE, never by transport: LogApiCall runs
106
+ * deep in the stack (in-process calls, pubsub/queue handlers, HTTP — one code path) and only ever
107
+ * sees a thrown Error. The Http* classes are poorly named for that (the `Http` prefix is historical),
108
+ * but they ARE portable error types that travel with the throw anywhere, so matching the TYPE works
109
+ * with or without any HTTP in the picture. Do NOT swap this for an `error.code` 4xx range check — that
110
+ * both re-couples to HTTP AND would bury 408.
111
+ *
112
+ * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
113
+ * - HttpBadRequestError (400) "your request is malformed"
114
+ * - HttpUnauthorizedError (401) "you're not authenticated"
115
+ * - HttpForbiddenError (403) "authenticated, but not allowed"
116
+ * - HttpNotFoundError (404) "wrong url / no such entity" (EndpointNotFoundError is a subclass)
117
+ * → the server is fine, the caller erred → result:'success', logged OTHER.
63
118
  *
64
- * User errors (logged as OTHER, no stack trace):
65
- * - HttpBadRequestError (400)
66
- * - HttpUnauthorizedError (401)
67
- * - HttpForbiddenError (403)
68
- * - HttpNotFoundError (404)
69
- * - HttpUserError (266)
119
+ * SERVER something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):
120
+ * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
121
+ * absent from the list below so it counts as a failure.
122
+ * - 500 / 502 / 504 / 598, and any non-Http Error: real failures.
70
123
  *
71
- * @param error - The error to check
72
- * @returns true if this is a user error, false for server errors
124
+ * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake" signal.
125
+ * CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.
126
+ *
127
+ * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)
128
+ * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call
129
+ * @returns true if this should be treated as a non-failure (OTHER / result:'success')
73
130
  */
74
- static isUserError(error) {
131
+ isUserError(error, server) {
132
+ // 266 is the one error that is never a failure, on either side.
133
+ if (error instanceof errors_1.HttpUserError) {
134
+ return true;
135
+ }
136
+ // A client that RECEIVED any error (except the 266 above) made a failed call.
137
+ if (!server) {
138
+ return false;
139
+ }
140
+ // SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408
141
+ // (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.
75
142
  return (error instanceof errors_1.HttpBadRequestError ||
76
143
  error instanceof errors_1.HttpUnauthorizedError ||
77
144
  error instanceof errors_1.HttpForbiddenError ||
78
- error instanceof errors_1.HttpNotFoundError ||
79
- error instanceof errors_1.HttpUserError);
145
+ error instanceof errors_1.HttpNotFoundError);
80
146
  }
81
147
  }
82
- exports.LogApiCall = LogApiCall;
148
+ exports.LogApiCallImpl = LogApiCallImpl;
149
+ /**
150
+ * The process-wide {@link LogApiCallImpl} singleton — mirrors the `RequestContext` export pattern.
151
+ * Callers use `LogApiCall.execute(...)`, never `new`.
152
+ */
153
+ exports.LogApiCall = new LogApiCallImpl();
83
154
  //# sourceMappingURL=LogApiCall.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":";;;AACA,qCAMkB;AAClB,kDAA0C;AAC1C,sDAAiD;AAEjD,MAAM,GAAG,GAAG,uBAAU,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;AAG/C;;;;;;;;;;;GAWG;AACH,MAAa,UAAU;IAEnB;;;;;;;;;;;OAWG;IACI,KAAK,CAAC,OAAO,CAChB,IAAY,EACZ,IAAmB,EACnB,UAAe,EACf,MAAkC;QAElC,GAAG,CAAC,IAAI,CACJ,QAAQ,IAAI,SAAS,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,YAAY,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CACxH,CAAC;QAEF,qHAAqH;QACrH,IAAI,CAAC;YACD,IAAG,CAAC,UAAU;gBACV,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YAE1G,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;YAE1C,IAAG,CAAC,QAAQ;gBACR,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YAE3G,uBAAuB;YACvB,GAAG,CAAC,IAAI,CACJ,QAAQ,IAAI,kBAAkB,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,UAAU,aAAa,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CACnH,CAAC;YAEF,OAAO,QAAQ,CAAC;QACpB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,oBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC;YACzC,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC;YAEnC,uCAAuC;YACvC,IAAI,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChC,GAAG,CAAC,IAAI,CACJ,QAAQ,IAAI,gBAAgB,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,UAAU,cAAc,SAAS,EAAE,CACnG,CAAC;YACN,CAAC;iBAAM,CAAC;gBACJ,GAAG,CAAC,KAAK,CACL,QAAQ,IAAI,eAAe,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,UAAU,cAAc,SAAS,UAAU,YAAY,EAAE,CACxH,CAAC;YACN,CAAC;YACD,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,WAAW,CAAC,KAAc;QAC7B,OAAO,CACH,KAAK,YAAY,4BAAmB;YACpC,KAAK,YAAY,8BAAqB;YACtC,KAAK,YAAY,2BAAkB;YACnC,KAAK,YAAY,0BAAiB;YAClC,KAAK,YAAY,sBAAa,CACjC,CAAC;IACN,CAAC;CACJ;AAlFD,gCAkFC","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\";\n\nconst log = LogManager.getLogger('LogApiCall');\n\n\n/**\n * LogApiCall - Generic API call logging utility.\n *\n * Used by both server-side (LogApiFilter) and client-side (ClientFactory) for\n * consistent logging patterns across the framework.\n *\n * Logging format patterns:\n * - [API-{type}-req] ClassName.methodName request={...} headers={...}\n * - [API-{type}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{type}-resp-OTHER] ClassName.methodName errorType={...} (user errors)\n * - [API-{type}-resp-FAIL] ClassName.methodName error={...} (server errors)\n */\nexport class LogApiCall {\n\n /**\n * Execute an API call with logging around it.\n *\n * @param type - 'SVR' or 'CLIENT'\n * @param meta - Route metadata with controllerClassName and methodName\n * @param requestDto - The request DTO\n * @param method - The method to execute\n *\n * Context fields (requestId, tenantId, ...) are NOT stamped here. A logging BACKEND owns that:\n * bunyan/winston read RequestContext.buildLogFields() on every record. The bootstrap\n * ConsoleLogger deliberately carries no context.\n */\n public async execute(\n type: string,\n meta: RouteMetadata,\n requestDto: any,\n method: (dto: any) => Promise<any>\n ): Promise<any> {\n log.info(\n `[API-${type}-req] ${meta.controllerClassName}.${meta.methodName} ${meta.path} request=${JSON.stringify(requestDto)}`\n );\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller\n try {\n if(!requestDto)\n throw new Error(`Request cannot be null and was from ${meta.controllerClassName}.${meta.methodName}`);\n \n const response = await method(requestDto);\n\n if(!response)\n throw new Error(`Response cannot be null and was from ${meta.controllerClassName}.${meta.methodName}`);\n\n // Log success response\n log.info(\n `[API-${type}-resp-SUCCESS] ${meta.controllerClassName}.${meta.methodName} response=${JSON.stringify(response)}`\n );\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\n // Log error based on type and re-throw\n if (LogApiCall.isUserError(error)) {\n log.warn(\n `[API-${type}-resp-OTHER] ${meta.controllerClassName}.${meta.methodName} errorType=${errorType}`\n );\n } else {\n log.error(\n `[API-${type}-resp-FAIL] ${meta.controllerClassName}.${meta.methodName} errorType=${errorType} error=${errorMessage}`\n );\n }\n throw error;\n }\n }\n\n /**\n * Check if an error is a user error (expected behavior from server perspective).\n * User errors are NOT failures - just users making mistakes or validation issues.\n *\n * User errors (logged as OTHER, no stack trace):\n * - HttpBadRequestError (400)\n * - HttpUnauthorizedError (401)\n * - HttpForbiddenError (403)\n * - HttpNotFoundError (404)\n * - HttpUserError (266)\n *\n * @param error - The error to check\n * @returns true if this is a user error, false for server errors\n */\n static isUserError(error: unknown): boolean {\n return (\n error instanceof HttpBadRequestError ||\n error instanceof HttpUnauthorizedError ||\n error instanceof HttpForbiddenError ||\n error instanceof HttpNotFoundError ||\n error instanceof HttpUserError\n );\n }\n}\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 {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/**\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 *\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 // 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(!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"]}
@@ -29,6 +29,19 @@ export declare class WebpiecesCoreHeaders {
29
29
  * Transferred so recording follows the request across service hops.
30
30
  */
31
31
  static readonly RECORDING: ContextKey;
32
+ /**
33
+ * The structured API-call tag ({@link ApiCallInfo}) stamped by {@link LogApiCall} around every
34
+ * outbound (client) / inbound (server) call. It rides the magic context so EVERY log line emitted
35
+ * during the call inherits a filterable `api` object, surfacing in GCP as nested
36
+ * `jsonPayload.api.{side,type,result,path,method}`.
37
+ *
38
+ * - `httpHeader` UNDEFINED → NOT transferred over the wire. Per-hop only: each server/client hop
39
+ * stamps its own tag, so a downstream server records `side:'server'`, never the caller's `side:'client'`.
40
+ * - `isLogged` TRUE → emitted by the logging backends. It carries an OBJECT value, so the backends
41
+ * read it via {@link HeaderRegistry.buildStructuredLogFields} (object-aware); the flat
42
+ * `buildLogFields()` string map deliberately skips it (typeof-string guard).
43
+ */
44
+ static readonly API_CALL_INFO: ContextKey;
32
45
  /**
33
46
  * NO CREDENTIAL KEYS LIVE HERE.
34
47
  *
@@ -32,6 +32,19 @@ class WebpiecesCoreHeaders {
32
32
  * Transferred so recording follows the request across service hops.
33
33
  */
34
34
  static RECORDING = new ContextKey_1.ContextKey('recording', 'x-webpieces-recording');
35
+ /**
36
+ * The structured API-call tag ({@link ApiCallInfo}) stamped by {@link LogApiCall} around every
37
+ * outbound (client) / inbound (server) call. It rides the magic context so EVERY log line emitted
38
+ * during the call inherits a filterable `api` object, surfacing in GCP as nested
39
+ * `jsonPayload.api.{side,type,result,path,method}`.
40
+ *
41
+ * - `httpHeader` UNDEFINED → NOT transferred over the wire. Per-hop only: each server/client hop
42
+ * stamps its own tag, so a downstream server records `side:'server'`, never the caller's `side:'client'`.
43
+ * - `isLogged` TRUE → emitted by the logging backends. It carries an OBJECT value, so the backends
44
+ * read it via {@link HeaderRegistry.buildStructuredLogFields} (object-aware); the flat
45
+ * `buildLogFields()` string map deliberately skips it (typeof-string guard).
46
+ */
47
+ static API_CALL_INFO = new ContextKey_1.ContextKey('api', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);
35
48
  /**
36
49
  * NO CREDENTIAL KEYS LIVE HERE.
37
50
  *
@@ -58,6 +71,7 @@ class WebpiecesCoreHeaders {
58
71
  WebpiecesCoreHeaders.ORG_ID,
59
72
  WebpiecesCoreHeaders.USER_ROLES,
60
73
  WebpiecesCoreHeaders.RECORDING,
74
+ WebpiecesCoreHeaders.API_CALL_INFO,
61
75
  ];
62
76
  }
63
77
  }
@@ -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;;;;;;;;;;;;;;;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;SACjC,CAAC;IACN,CAAC;;AA/CL,oDAgDC","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 * 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 ];\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;;;;;;;;;;;;;;;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"]}
package/src/index.d.ts CHANGED
@@ -29,9 +29,13 @@ export type { ErrorTranslation } from './http/ErrorTranslation';
29
29
  export { templateDeriver } from './http/templateDeriver';
30
30
  export { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';
31
31
  export { ContextReader } from './http/ContextReader';
32
- export type { ContextRead } from './http/ContextReader';
32
+ export type { ContextRead, StructuredContextRead } from './http/ContextReader';
33
33
  export { ContextMgr } from './http/ContextMgr';
34
- export { LogApiCall } from './http/LogApiCall';
34
+ export { LogApiCall, LogApiCallImpl } from './http/LogApiCall';
35
+ export { ApiCallInfo } from './http/ApiCallInfo';
36
+ export type { ApiSide, ApiType, ApiResult } from './http/ApiCallInfo';
37
+ export { ApiCallContextHolder } from './http/ApiCallContext';
38
+ export type { ApiCallContext } from './http/ApiCallContext';
35
39
  export { TestCaseRecorder, RecorderKeys } from './http/recorder/TestCaseRecorder';
36
40
  export { RecordedEndpoint, RecordedError, RecordedTestCase } from './http/recorder/RecordedEndpoint';
37
41
  export { DoNotRecord, getDoNotRecordFields } from './http/recorder/DoNotRecord';
package/src/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.ENTITY_NOT_FOUND = exports.HttpUserError = 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.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.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 = 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.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 = 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");
@@ -115,9 +115,16 @@ Object.defineProperty(exports, "WebpiecesCoreHeaders", { enumerable: true, get:
115
115
  // RequestContextHeaders in the Node-only @webpieces/core-context.
116
116
  var ContextMgr_1 = require("./http/ContextMgr");
117
117
  Object.defineProperty(exports, "ContextMgr", { enumerable: true, get: function () { return ContextMgr_1.ContextMgr; } });
118
- // API-call logging helper (uses LogManager above)
118
+ // API-call logging helper (uses LogManager above). Singleton: use the LogApiCall constant, not `new`.
119
119
  var LogApiCall_1 = require("./http/LogApiCall");
120
120
  Object.defineProperty(exports, "LogApiCall", { enumerable: true, get: function () { return LogApiCall_1.LogApiCall; } });
121
+ Object.defineProperty(exports, "LogApiCallImpl", { enumerable: true, get: function () { return LogApiCall_1.LogApiCallImpl; } });
122
+ // The structured `api` tag + the context-writer seam LogApiCall stamps through. The Node
123
+ // RequestContext-backed impl is installed by @webpieces/core-context; the browser gets the no-op.
124
+ var ApiCallInfo_1 = require("./http/ApiCallInfo");
125
+ Object.defineProperty(exports, "ApiCallInfo", { enumerable: true, get: function () { return ApiCallInfo_1.ApiCallInfo; } });
126
+ var ApiCallContext_1 = require("./http/ApiCallContext");
127
+ Object.defineProperty(exports, "ApiCallContextHolder", { enumerable: true, get: function () { return ApiCallContext_1.ApiCallContextHolder; } });
121
128
  // Test-case recording contract (impl lives in http-server; hooks in http-client)
122
129
  var TestCaseRecorder_1 = require("./http/recorder/TestCaseRecorder");
123
130
  Object.defineProperty(exports, "RecorderKeys", { enumerable: true, get: function () { return TestCaseRecorder_1.RecorderKeys; } });
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 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 } 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 } 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)\nexport { LogApiCall } from './http/LogApiCall';\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 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 } 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 } 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"]}