@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 +1 -1
- package/src/http/ApiCallContext.d.ts +56 -0
- package/src/http/ApiCallContext.js +39 -0
- package/src/http/ApiCallContext.js.map +1 -0
- package/src/http/ApiCallInfo.d.ts +46 -0
- package/src/http/ApiCallInfo.js +47 -0
- package/src/http/ApiCallInfo.js.map +1 -0
- package/src/http/ContextReader.d.ts +7 -0
- package/src/http/ContextReader.js.map +1 -1
- package/src/http/HeaderRegistry.d.ts +13 -1
- package/src/http/HeaderRegistry.js +36 -1
- package/src/http/HeaderRegistry.js.map +1 -1
- package/src/http/LogApiCall.d.ts +64 -25
- package/src/http/LogApiCall.js +112 -41
- package/src/http/LogApiCall.js.map +1 -1
- package/src/http/WebpiecesCoreHeaders.d.ts +13 -0
- package/src/http/WebpiecesCoreHeaders.js +14 -0
- package/src/http/WebpiecesCoreHeaders.js.map +1 -1
- package/src/index.d.ts +6 -2
- package/src/index.js +9 -2
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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"]}
|
package/src/http/LogApiCall.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
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-{
|
|
10
|
-
* - [API-{
|
|
11
|
-
* - [API-{
|
|
12
|
-
* - [API-{
|
|
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
|
|
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
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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(
|
|
44
|
+
execute(side: ApiSide, meta: RouteMetadata, requestDto: any, method: (dto: any) => Promise<any>): Promise<any>;
|
|
28
45
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
*
|
|
33
|
-
* -
|
|
34
|
-
*
|
|
35
|
-
* -
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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
|
-
|
|
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;
|
package/src/http/LogApiCall.js
CHANGED
|
@@ -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
|
-
*
|
|
12
|
-
*
|
|
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-{
|
|
16
|
-
* - [API-{
|
|
17
|
-
* - [API-{
|
|
18
|
-
* - [API-{
|
|
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
|
|
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
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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(
|
|
34
|
-
|
|
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 ${
|
|
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 ${
|
|
42
|
-
|
|
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
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
log.error(`[API-${
|
|
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
|
-
*
|
|
62
|
-
*
|
|
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
|
-
*
|
|
65
|
-
* -
|
|
66
|
-
*
|
|
67
|
-
* -
|
|
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
|
-
*
|
|
72
|
-
*
|
|
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
|
-
|
|
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.
|
|
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;
|
|
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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
|
|
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"]}
|