@webpieces/core-util 0.4.388 → 0.4.390
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/ContextKey.d.ts +1 -11
- package/src/ContextKey.js +1 -12
- package/src/ContextKey.js.map +1 -1
- package/src/http/ApiCallInfo.d.ts +24 -18
- package/src/http/ApiCallInfo.js +7 -34
- package/src/http/ApiCallInfo.js.map +1 -1
- package/src/http/ApiMethodInfo.d.ts +40 -0
- package/src/http/ApiMethodInfo.js +43 -0
- package/src/http/ApiMethodInfo.js.map +1 -0
- package/src/http/HeaderRegistry.d.ts +1 -1
- package/src/http/HeaderRegistry.js +2 -20
- package/src/http/HeaderRegistry.js.map +1 -1
- package/src/http/LogApiCall.d.ts +6 -21
- package/src/http/LogApiCall.js +15 -35
- package/src/http/LogApiCall.js.map +1 -1
- package/src/http/WebpiecesCoreHeaders.d.ts +13 -0
- package/src/http/WebpiecesCoreHeaders.js +15 -0
- package/src/http/WebpiecesCoreHeaders.js.map +1 -1
- package/src/index.d.ts +4 -2
- package/src/index.js +3 -2
- package/src/index.js.map +1 -1
package/package.json
CHANGED
package/src/ContextKey.d.ts
CHANGED
|
@@ -18,9 +18,6 @@
|
|
|
18
18
|
* - `isLogged` Defaults to true. When false, the value is NEVER logged (used for
|
|
19
19
|
* object-valued/internal keys like the recorder or method-meta that
|
|
20
20
|
* must not be serialized into log lines).
|
|
21
|
-
* - `spread` Defaults to false. When true AND the value is an object, its entries
|
|
22
|
-
* are emitted as FLAT top-level log fields (jsonPayload.<entryKey>)
|
|
23
|
-
* instead of nested under this key's name (jsonPayload.<name>.<entryKey>).
|
|
24
21
|
*
|
|
25
22
|
* Per CLAUDE.md: data-only structures are classes, not interfaces.
|
|
26
23
|
*/
|
|
@@ -36,14 +33,7 @@ export declare class ContextKey {
|
|
|
36
33
|
readonly isSecured: boolean;
|
|
37
34
|
/** Whether this key is logged at all. Default true; false = never logged. */
|
|
38
35
|
readonly isLogged: boolean;
|
|
39
|
-
|
|
40
|
-
* When true AND the value is an object, its entries are emitted as FLAT top-level
|
|
41
|
-
* log fields (jsonPayload.<entryKey>) instead of nested under this key's name. For
|
|
42
|
-
* metric-style structs whose fields must be individually extractable (GCP
|
|
43
|
-
* EXTRACT(jsonPayload.inputTokens)). No effect on string values or on wire transfer.
|
|
44
|
-
*/
|
|
45
|
-
readonly spread: boolean;
|
|
46
|
-
constructor(name: string, httpHeader?: string, isSecured?: boolean, isLogged?: boolean, spread?: boolean);
|
|
36
|
+
constructor(name: string, httpHeader?: string, isSecured?: boolean, isLogged?: boolean);
|
|
47
37
|
/** True when this key is transferred over HTTP (has an httpHeader). */
|
|
48
38
|
isTransferred(): boolean;
|
|
49
39
|
/**
|
package/src/ContextKey.js
CHANGED
|
@@ -21,9 +21,6 @@ exports.ContextKey = void 0;
|
|
|
21
21
|
* - `isLogged` Defaults to true. When false, the value is NEVER logged (used for
|
|
22
22
|
* object-valued/internal keys like the recorder or method-meta that
|
|
23
23
|
* must not be serialized into log lines).
|
|
24
|
-
* - `spread` Defaults to false. When true AND the value is an object, its entries
|
|
25
|
-
* are emitted as FLAT top-level log fields (jsonPayload.<entryKey>)
|
|
26
|
-
* instead of nested under this key's name (jsonPayload.<name>.<entryKey>).
|
|
27
24
|
*
|
|
28
25
|
* Per CLAUDE.md: data-only structures are classes, not interfaces.
|
|
29
26
|
*/
|
|
@@ -39,19 +36,11 @@ class ContextKey {
|
|
|
39
36
|
isSecured;
|
|
40
37
|
/** Whether this key is logged at all. Default true; false = never logged. */
|
|
41
38
|
isLogged;
|
|
42
|
-
|
|
43
|
-
* When true AND the value is an object, its entries are emitted as FLAT top-level
|
|
44
|
-
* log fields (jsonPayload.<entryKey>) instead of nested under this key's name. For
|
|
45
|
-
* metric-style structs whose fields must be individually extractable (GCP
|
|
46
|
-
* EXTRACT(jsonPayload.inputTokens)). No effect on string values or on wire transfer.
|
|
47
|
-
*/
|
|
48
|
-
spread;
|
|
49
|
-
constructor(name, httpHeader, isSecured = false, isLogged = true, spread = false) {
|
|
39
|
+
constructor(name, httpHeader, isSecured = false, isLogged = true) {
|
|
50
40
|
this.name = name;
|
|
51
41
|
this.httpHeader = httpHeader;
|
|
52
42
|
this.isSecured = isSecured;
|
|
53
43
|
this.isLogged = isLogged;
|
|
54
|
-
this.spread = spread;
|
|
55
44
|
}
|
|
56
45
|
/** True when this key is transferred over HTTP (has an httpHeader). */
|
|
57
46
|
isTransferred() {
|
package/src/ContextKey.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextKey.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKey.ts"],"names":[],"mappings":";;;AAAA
|
|
1
|
+
{"version":3,"file":"ContextKey.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/ContextKey.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,UAAU;IACnB,qEAAqE;IAC5D,IAAI,CAAS;IAEtB;;;OAGG;IACM,UAAU,CAAU;IAE7B,2CAA2C;IAClC,SAAS,CAAU;IAE5B,6EAA6E;IACpE,QAAQ,CAAU;IAE3B,YACI,IAAY,EACZ,UAAmB,EACnB,SAAS,GAAG,KAAK,EACjB,QAAQ,GAAG,IAAI;QAEf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;IAED,uEAAuE;IACvE,aAAa;QACT,OAAO,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC;IACzC,CAAC;IAED;;;;;;OAMG;IACH,aAAa,CAAC,KAAa;QACvB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAClB,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;QACzB,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YACV,OAAO,+BAA+B,CAAC;QAC3C,CAAC;aAAM,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC;YACnB,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC;QACzC,CAAC;aAAM,CAAC;YACJ,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;QACpE,CAAC;IACL,CAAC;CACJ;AArDD,gCAqDC","sourcesContent":["/**\n * ContextKey - a single key that travels in the request's \"magic context\"\n * (RequestContext on the server, MutableContextStore in the browser).\n *\n * This ONE class replaces the old split of `Header` (interface) + `PlatformHeader`\n * (class) + `ContextKey` (class). Every context value — whether it rides over HTTP\n * (request-id, tenant, authorization) or stays in-process (method-meta, the\n * TestCaseRecorder) — is a `ContextKey`.\n *\n * The fields are named for what they DO (flipped from the old model):\n * - `name` ALWAYS set. The context storage key, the log/MDC key, and the\n * recorder name. e.g. 'requestId', 'tenantId', 'authorization'.\n * - `httpHeader` OPTIONAL. When set, this key is transferred over the wire under\n * this HTTP header name (inbound request -> context, and context ->\n * outbound request). e.g. 'x-request-id'. When UNSET, the key is\n * context-only and never leaves the process (method-meta, recorder).\n * - `isSecured` When true, the value is masked (partially) in logs.\n * - `isLogged` Defaults to true. When false, the value is NEVER logged (used for\n * object-valued/internal keys like the recorder or method-meta that\n * must not be serialized into log lines).\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\nexport class ContextKey {\n /** Context storage key + log/MDC key + recorder name. Always set. */\n readonly name: string;\n\n /**\n * HTTP header name when this key is transferred over the wire (e.g.\n * 'x-request-id'). Undefined = context-only, never transferred.\n */\n readonly httpHeader?: string;\n\n /** Mask this value (partially) in logs. */\n readonly isSecured: boolean;\n\n /** Whether this key is logged at all. Default true; false = never logged. */\n readonly isLogged: boolean;\n\n constructor(\n name: string,\n httpHeader?: string,\n isSecured = false,\n isLogged = true,\n ) {\n this.name = name;\n this.httpHeader = httpHeader;\n this.isSecured = isSecured;\n this.isLogged = isLogged;\n }\n\n /** True when this key is transferred over HTTP (has an httpHeader). */\n isTransferred(): boolean {\n return this.httpHeader !== undefined;\n }\n\n /**\n * The value as it should appear in a log line: returned as-is for a normal\n * key, partially masked when this key is secured. Masking is length-based:\n * - Length > 15: first 3 + \"...\" + last 3\n * - Length 8-15: first 2 + \"...\"\n * - Length < 8: \"<secure key too short to log>\"\n */\n maskIfSecured(value: string): string {\n if (!this.isSecured) {\n return value;\n }\n const len = value.length;\n if (len < 8) {\n return '<secure key too short to log>';\n } else if (len <= 15) {\n return `${value.substring(0, 2)}...`;\n } else {\n return `${value.substring(0, 3)}...${value.substring(len - 3)}`;\n }\n }\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ApiMethodInfo } from './ApiMethodInfo';
|
|
1
2
|
/**
|
|
2
3
|
* ApiCallInfo - the structured tag stamped into RequestContext around every API call
|
|
3
4
|
* (by {@link LogApiCall}), so ANY log line emitted during the call inherits a filterable
|
|
@@ -6,22 +7,26 @@
|
|
|
6
7
|
* The node logging backends (winston/bunyan) read this struct out of context via
|
|
7
8
|
* `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,
|
|
8
9
|
* which unlocks GCP Cloud Logging filters like:
|
|
9
|
-
* - `jsonPayload.api.side="client"`
|
|
10
|
-
* - `jsonPayload.api.side="server"`
|
|
11
|
-
* - `jsonPayload.api.
|
|
12
|
-
* - `jsonPayload.api
|
|
10
|
+
* - `jsonPayload.api.method.side="client"` — every outbound call this process made
|
|
11
|
+
* - `jsonPayload.api.method.side="server"` — every inbound call it handled
|
|
12
|
+
* - `jsonPayload.api.method.apiClass="SaveApi"` — one logical method, BOTH sides (client + server)
|
|
13
|
+
* - `jsonPayload.api.result="failure"` — failed exchanges only
|
|
14
|
+
* - `jsonPayload.api:*` — "API traffic only" (tracing + the recorder)
|
|
13
15
|
*
|
|
14
|
-
* IMPORTANT: the field names on
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* IMPORTANT: the field names here (and on the nested {@link ApiMethodInfo}) ARE the GCP field names —
|
|
17
|
+
* rename a field and the filter renames with it. The identity lives NESTED under `api.method`
|
|
18
|
+
* (`api.method.{side,apiClass,methodName,controllerName}`); `api.type` and `api.result` sit at the top.
|
|
19
|
+
*
|
|
20
|
+
* NOTE: the request `httpMethod`/`path` are NOT here — an inbound request stamps them as the separate
|
|
21
|
+
* top-level logged keys `jsonPayload.httpMethod` / `jsonPayload.requestPath` (see
|
|
22
|
+
* {@link WebpiecesCoreHeaders} + `RequestContextHeaders.fillFromRequest`). Outbound client calls have
|
|
23
|
+
* no inbound path, so they carry only the `api` identity.
|
|
17
24
|
*
|
|
18
25
|
* Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a
|
|
19
26
|
* downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.
|
|
20
27
|
*
|
|
21
28
|
* Per CLAUDE.md: data-only structures are classes, not interfaces.
|
|
22
29
|
*/
|
|
23
|
-
/** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */
|
|
24
|
-
export type ApiSide = 'client' | 'server';
|
|
25
30
|
/** Which half of the exchange this tag describes: the outgoing 'request' or the returning 'response'. */
|
|
26
31
|
export type ApiType = 'request' | 'response';
|
|
27
32
|
/**
|
|
@@ -29,18 +34,19 @@ export type ApiType = 'request' | 'response';
|
|
|
29
34
|
* handled "you made a mistake"); 'failure' is a genuine server error. See {@link LogApiCall.isUserError}.
|
|
30
35
|
*/
|
|
31
36
|
export type ApiResult = 'success' | 'failure';
|
|
37
|
+
/** Re-exported from {@link ApiMethodInfo} (its true home) so existing `ApiSide` imports keep working. */
|
|
38
|
+
export type { ApiSide } from './ApiMethodInfo';
|
|
32
39
|
export declare class ApiCallInfo {
|
|
33
|
-
|
|
40
|
+
/** The call identity (side, apiClass, methodName, controllerName) — surfaces nested under
|
|
41
|
+
* `jsonPayload.api.method`. */
|
|
42
|
+
readonly method: ApiMethodInfo;
|
|
34
43
|
readonly type: ApiType;
|
|
35
44
|
/** Response only — undefined on the 'request' tag. */
|
|
36
45
|
readonly result?: ApiResult | undefined;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
constructor(side: ApiSide, type: ApiType,
|
|
46
|
+
constructor(
|
|
47
|
+
/** The call identity (side, apiClass, methodName, controllerName) — surfaces nested under
|
|
48
|
+
* `jsonPayload.api.method`. */
|
|
49
|
+
method: ApiMethodInfo, type: ApiType,
|
|
42
50
|
/** Response only — undefined on the 'request' tag. */
|
|
43
|
-
result?: ApiResult | undefined
|
|
44
|
-
/** The controller/API class name (e.g. 'SaveController') — filter with jsonPayload.api.controller. */
|
|
45
|
-
controller?: string | undefined);
|
|
51
|
+
result?: ApiResult | undefined);
|
|
46
52
|
}
|
package/src/http/ApiCallInfo.js
CHANGED
|
@@ -1,46 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* ApiCallInfo - the structured tag stamped into RequestContext around every API call
|
|
4
|
-
* (by {@link LogApiCall}), so ANY log line emitted during the call inherits a filterable
|
|
5
|
-
* `api` object rather than only the req/resp text lines.
|
|
6
|
-
*
|
|
7
|
-
* The node logging backends (winston/bunyan) read this struct out of context via
|
|
8
|
-
* `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,
|
|
9
|
-
* which unlocks GCP Cloud Logging filters like:
|
|
10
|
-
* - `jsonPayload.api.side="client"` — every outbound call this process made
|
|
11
|
-
* - `jsonPayload.api.side="server"` — every inbound call it handled
|
|
12
|
-
* - `jsonPayload.api.result="failure"`— failed exchanges only
|
|
13
|
-
* - `jsonPayload.api:*` — "API traffic only" (tracing + the recorder)
|
|
14
|
-
*
|
|
15
|
-
* IMPORTANT: the field names on this class ARE the GCP field names (`api.side`, `api.type`,
|
|
16
|
-
* `api.result`, `api.path`, `api.method`, `api.controller`) — rename a field here and the filter
|
|
17
|
-
* renames with it.
|
|
18
|
-
*
|
|
19
|
-
* Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a
|
|
20
|
-
* downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.
|
|
21
|
-
*
|
|
22
|
-
* Per CLAUDE.md: data-only structures are classes, not interfaces.
|
|
23
|
-
*/
|
|
24
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
3
|
exports.ApiCallInfo = void 0;
|
|
26
4
|
class ApiCallInfo {
|
|
27
|
-
|
|
5
|
+
method;
|
|
28
6
|
type;
|
|
29
7
|
result;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
8
|
+
constructor(
|
|
9
|
+
/** The call identity (side, apiClass, methodName, controllerName) — surfaces nested under
|
|
10
|
+
* `jsonPayload.api.method`. */
|
|
11
|
+
method, type,
|
|
34
12
|
/** Response only — undefined on the 'request' tag. */
|
|
35
|
-
result
|
|
36
|
-
|
|
37
|
-
controller) {
|
|
38
|
-
this.side = side;
|
|
13
|
+
result) {
|
|
14
|
+
this.method = method;
|
|
39
15
|
this.type = type;
|
|
40
16
|
this.result = result;
|
|
41
|
-
this.path = path;
|
|
42
|
-
this.method = method;
|
|
43
|
-
this.controller = controller;
|
|
44
17
|
}
|
|
45
18
|
}
|
|
46
19
|
exports.ApiCallInfo = ApiCallInfo;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiCallInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiCallInfo.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ApiCallInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiCallInfo.ts"],"names":[],"mappings":";;;AA2CA,MAAa,WAAW;IAIP;IACA;IAEA;IANb;IACI;oCACgC;IACvB,MAAqB,EACrB,IAAa;IACtB,sDAAsD;IAC7C,MAAkB;QAHlB,WAAM,GAAN,MAAM,CAAe;QACrB,SAAI,GAAJ,IAAI,CAAS;QAEb,WAAM,GAAN,MAAM,CAAY;IAC5B,CAAC;CACP;AATD,kCASC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * ApiCallInfo - the structured tag stamped into RequestContext around every API call\n * (by {@link LogApiCall}), so ANY log line emitted during the call inherits a filterable\n * `api` object rather than only the req/resp text lines.\n *\n * The node logging backends (winston/bunyan) read this struct out of context via\n * `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,\n * which unlocks GCP Cloud Logging filters like:\n * - `jsonPayload.api.method.side=\"client\"` — every outbound call this process made\n * - `jsonPayload.api.method.side=\"server\"` — every inbound call it handled\n * - `jsonPayload.api.method.apiClass=\"SaveApi\"` — one logical method, BOTH sides (client + server)\n * - `jsonPayload.api.result=\"failure\"` — failed exchanges only\n * - `jsonPayload.api:*` — \"API traffic only\" (tracing + the recorder)\n *\n * IMPORTANT: the field names here (and on the nested {@link ApiMethodInfo}) ARE the GCP field names —\n * rename a field and the filter renames with it. The identity lives NESTED under `api.method`\n * (`api.method.{side,apiClass,methodName,controllerName}`); `api.type` and `api.result` sit at the top.\n *\n * NOTE: the request `httpMethod`/`path` are NOT here — an inbound request stamps them as the separate\n * top-level logged keys `jsonPayload.httpMethod` / `jsonPayload.requestPath` (see\n * {@link WebpiecesCoreHeaders} + `RequestContextHeaders.fillFromRequest`). Outbound client calls have\n * no inbound path, so they carry only the `api` identity.\n *\n * Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a\n * downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\n/** Which half of the exchange this tag describes: the outgoing 'request' or the returning 'response'. */\nexport type ApiType = 'request' | 'response';\n\n/**\n * Response outcome. 'success' covers 2xx AND user errors (400/401/403/404/266 — a successfully\n * handled \"you made a mistake\"); 'failure' is a genuine server error. See {@link LogApiCall.isUserError}.\n */\nexport type ApiResult = 'success' | 'failure';\n\n/** Re-exported from {@link ApiMethodInfo} (its true home) so existing `ApiSide` imports keep working. */\nexport type { ApiSide } from './ApiMethodInfo';\n\nexport class ApiCallInfo {\n constructor(\n /** The call identity (side, apiClass, methodName, controllerName) — surfaces nested under\n * `jsonPayload.api.method`. */\n readonly method: ApiMethodInfo,\n readonly type: ApiType,\n /** Response only — undefined on the 'request' tag. */\n readonly result?: ApiResult,\n ) {}\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApiMethodInfo - the transport-neutral identity of a single API method call, handed to
|
|
3
|
+
* {@link LogApiCall.execute} by every caller (server inbound, http/in-process clients, cloud tasks,
|
|
4
|
+
* and external wrapped clients like the firestore admin client).
|
|
5
|
+
*
|
|
6
|
+
* WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,
|
|
7
|
+
* pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.
|
|
8
|
+
* None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped
|
|
9
|
+
* `RouteMetadata`. This carries only what identifies the call.
|
|
10
|
+
*
|
|
11
|
+
* MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical
|
|
12
|
+
* method must log the SAME identity so `jsonPayload.api.method.apiClass="SaveApi"` filters both sides
|
|
13
|
+
* together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only
|
|
14
|
+
* ever knows it, and the server carries it alongside its impl name — so it, not the server's impl
|
|
15
|
+
* class, is the required key.
|
|
16
|
+
*
|
|
17
|
+
* Per CLAUDE.md: data-only structures are classes, not interfaces.
|
|
18
|
+
*/
|
|
19
|
+
/** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */
|
|
20
|
+
export type ApiSide = 'client' | 'server';
|
|
21
|
+
export declare class ApiMethodInfo {
|
|
22
|
+
readonly side: ApiSide;
|
|
23
|
+
/** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both
|
|
24
|
+
* sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */
|
|
25
|
+
readonly apiClass: string;
|
|
26
|
+
/** REQUIRED — the method on the contract (e.g. 'save'). */
|
|
27
|
+
readonly methodName: string;
|
|
28
|
+
/** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no
|
|
29
|
+
* impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */
|
|
30
|
+
readonly controllerName?: string | undefined;
|
|
31
|
+
constructor(side: ApiSide,
|
|
32
|
+
/** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both
|
|
33
|
+
* sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */
|
|
34
|
+
apiClass: string,
|
|
35
|
+
/** REQUIRED — the method on the contract (e.g. 'save'). */
|
|
36
|
+
methodName: string,
|
|
37
|
+
/** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no
|
|
38
|
+
* impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */
|
|
39
|
+
controllerName?: string | undefined);
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ApiMethodInfo - the transport-neutral identity of a single API method call, handed to
|
|
4
|
+
* {@link LogApiCall.execute} by every caller (server inbound, http/in-process clients, cloud tasks,
|
|
5
|
+
* and external wrapped clients like the firestore admin client).
|
|
6
|
+
*
|
|
7
|
+
* WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,
|
|
8
|
+
* pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.
|
|
9
|
+
* None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped
|
|
10
|
+
* `RouteMetadata`. This carries only what identifies the call.
|
|
11
|
+
*
|
|
12
|
+
* MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical
|
|
13
|
+
* method must log the SAME identity so `jsonPayload.api.method.apiClass="SaveApi"` filters both sides
|
|
14
|
+
* together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only
|
|
15
|
+
* ever knows it, and the server carries it alongside its impl name — so it, not the server's impl
|
|
16
|
+
* class, is the required key.
|
|
17
|
+
*
|
|
18
|
+
* Per CLAUDE.md: data-only structures are classes, not interfaces.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.ApiMethodInfo = void 0;
|
|
22
|
+
class ApiMethodInfo {
|
|
23
|
+
side;
|
|
24
|
+
apiClass;
|
|
25
|
+
methodName;
|
|
26
|
+
controllerName;
|
|
27
|
+
constructor(side,
|
|
28
|
+
/** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both
|
|
29
|
+
* sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */
|
|
30
|
+
apiClass,
|
|
31
|
+
/** REQUIRED — the method on the contract (e.g. 'save'). */
|
|
32
|
+
methodName,
|
|
33
|
+
/** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no
|
|
34
|
+
* impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */
|
|
35
|
+
controllerName) {
|
|
36
|
+
this.side = side;
|
|
37
|
+
this.apiClass = apiClass;
|
|
38
|
+
this.methodName = methodName;
|
|
39
|
+
this.controllerName = controllerName;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.ApiMethodInfo = ApiMethodInfo;
|
|
43
|
+
//# sourceMappingURL=ApiMethodInfo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiMethodInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiMethodInfo.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAKH,MAAa,aAAa;IAET;IAGA;IAEA;IAGA;IATb,YACa,IAAa;IACtB;2FACuF;IAC9E,QAAgB;IACzB,2DAA2D;IAClD,UAAkB;IAC3B;gGAC4F;IACnF,cAAuB;QARvB,SAAI,GAAJ,IAAI,CAAS;QAGb,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,eAAU,GAAV,UAAU,CAAQ;QAGlB,mBAAc,GAAd,cAAc,CAAS;IACjC,CAAC;CACP;AAZD,sCAYC","sourcesContent":["/**\n * ApiMethodInfo - the transport-neutral identity of a single API method call, handed to\n * {@link LogApiCall.execute} by every caller (server inbound, http/in-process clients, cloud tasks,\n * and external wrapped clients like the firestore admin client).\n *\n * WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,\n * pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.\n * None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped\n * `RouteMetadata`. This carries only what identifies the call.\n *\n * MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical\n * method must log the SAME identity so `jsonPayload.api.method.apiClass=\"SaveApi\"` filters both sides\n * together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only\n * ever knows it, and the server carries it alongside its impl name — so it, not the server's impl\n * class, is the required key.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\n/** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */\nexport type ApiSide = 'client' | 'server';\n\nexport class ApiMethodInfo {\n constructor(\n readonly side: ApiSide,\n /** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both\n * sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */\n readonly apiClass: string,\n /** REQUIRED — the method on the contract (e.g. 'save'). */\n readonly methodName: string,\n /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no\n * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */\n readonly controllerName?: string,\n ) {}\n}\n"]}
|
|
@@ -85,7 +85,7 @@ export declare class HeaderRegistry {
|
|
|
85
85
|
*/
|
|
86
86
|
private checkForDuplicates;
|
|
87
87
|
/**
|
|
88
|
-
* Two keys sharing a `name` must agree on httpHeader/isSecured/isLogged
|
|
88
|
+
* Two keys sharing a `name` must agree on httpHeader/isSecured/isLogged,
|
|
89
89
|
* otherwise the platform would behave differently depending on which module's
|
|
90
90
|
* definition happened to load first.
|
|
91
91
|
*/
|
|
@@ -140,22 +140,7 @@ class HeaderRegistry {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
else if (typeof value === 'object') {
|
|
143
|
-
|
|
144
|
-
// Flatten the struct's entries into top-level fields (jsonPayload.<entryKey>), so
|
|
145
|
-
// each is individually filterable / EXTRACT-able (GCP log-based metrics). The
|
|
146
|
-
// winston/bunyan formats loop this map doing info[name] = value per entry, so
|
|
147
|
-
// spread entries land flat automatically; their `if (info[name] === undefined)`
|
|
148
|
-
// guard keeps caller/earlier fields winning. Skip null/undefined entries. A spread
|
|
149
|
-
// struct should not reuse reserved field names (level, message, severity, ...).
|
|
150
|
-
for (const [field, v] of Object.entries(value)) {
|
|
151
|
-
if (v !== undefined && v !== null) {
|
|
152
|
-
fields.set(field, v);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
fields.set(key.name, value);
|
|
158
|
-
}
|
|
143
|
+
fields.set(key.name, value);
|
|
159
144
|
}
|
|
160
145
|
// Non-string primitives (number/boolean/bigint) are not expected for logged context keys;
|
|
161
146
|
// ignore them here rather than String()-flattening, keeping the shape honest.
|
|
@@ -195,7 +180,7 @@ class HeaderRegistry {
|
|
|
195
180
|
return Array.from(byName.values());
|
|
196
181
|
}
|
|
197
182
|
/**
|
|
198
|
-
* Two keys sharing a `name` must agree on httpHeader/isSecured/isLogged
|
|
183
|
+
* Two keys sharing a `name` must agree on httpHeader/isSecured/isLogged,
|
|
199
184
|
* otherwise the platform would behave differently depending on which module's
|
|
200
185
|
* definition happened to load first.
|
|
201
186
|
*/
|
|
@@ -210,9 +195,6 @@ class HeaderRegistry {
|
|
|
210
195
|
if (existing.isLogged !== duplicate.isLogged) {
|
|
211
196
|
conflicts.push(`isLogged (${existing.isLogged} vs ${duplicate.isLogged})`);
|
|
212
197
|
}
|
|
213
|
-
if (existing.spread !== duplicate.spread) {
|
|
214
|
-
conflicts.push(`spread (${existing.spread} vs ${duplicate.spread})`);
|
|
215
|
-
}
|
|
216
198
|
if (conflicts.length > 0) {
|
|
217
199
|
throw new Error(`Conflicting ContextKey definitions for '${existing.name}': ` +
|
|
218
200
|
`two modules registered it with different ${conflicts.join(', ')}. ` +
|
|
@@ -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, 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 if (key.spread) {\n // Flatten the struct's entries into top-level fields (jsonPayload.<entryKey>), so\n // each is individually filterable / EXTRACT-able (GCP log-based metrics). The\n // winston/bunyan formats loop this map doing info[name] = value per entry, so\n // spread entries land flat automatically; their `if (info[name] === undefined)`\n // guard keeps caller/earlier fields winning. Skip null/undefined entries. A spread\n // struct should not reuse reserved field names (level, message, severity, ...).\n for (const [field, v] of Object.entries(value)) {\n if (v !== undefined && v !== null) {\n fields.set(field, v as string | object);\n }\n }\n } else {\n fields.set(key.name, value);\n }\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/spread,\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 (existing.spread !== duplicate.spread) {\n conflicts.push(`spread (${existing.spread} vs ${duplicate.spread})`);\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,14 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ApiSide } from "./ApiCallInfo";
|
|
3
|
-
/**
|
|
4
|
-
* Options for {@link LogApiCallImpl.execute}. `allowVoidResponse` opts OUT of the strict
|
|
5
|
-
* falsy-response guard for callers that legitimately return void/undefined (e.g. a local firestore
|
|
6
|
-
* wrapper). Defaults to strict so RPC callers keep the safety net.
|
|
7
|
-
*/
|
|
8
|
-
export declare class LogApiCallOptions {
|
|
9
|
-
allowVoidResponse?: boolean;
|
|
10
|
-
constructor(allowVoidResponse?: boolean);
|
|
11
|
-
}
|
|
1
|
+
import { ApiMethodInfo } from "./ApiMethodInfo";
|
|
12
2
|
/**
|
|
13
3
|
* LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and
|
|
14
4
|
* client-side (ProxyClient) for one consistent logging shape across the framework.
|
|
@@ -18,7 +8,7 @@ export declare class LogApiCallOptions {
|
|
|
18
8
|
* 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the
|
|
19
9
|
* {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the
|
|
20
10
|
* req/resp lines) inherits a filterable `api` object — surfacing in GCP as
|
|
21
|
-
* `jsonPayload.api.{side,type,result
|
|
11
|
+
* `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.
|
|
22
12
|
*
|
|
23
13
|
* BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →
|
|
24
14
|
* BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).
|
|
@@ -38,15 +28,10 @@ export declare class LogApiCallImpl {
|
|
|
38
28
|
/**
|
|
39
29
|
* Execute an API call with logging + `api` context-tagging around it.
|
|
40
30
|
*
|
|
41
|
-
* @param
|
|
42
|
-
*
|
|
43
|
-
* @param requestDto - The request DTO
|
|
31
|
+
* @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,
|
|
32
|
+
* controllerName?). `apiClass` is what matches a client call to its server handler in the logs.
|
|
33
|
+
* @param requestDto - The request DTO (external multi-param callers synthesize a small object)
|
|
44
34
|
* @param method - The method to execute
|
|
45
|
-
* @param options - `allowVoidResponse: true` opts OUT of the strict falsy-response guard, for
|
|
46
|
-
* callers that legitimately return void/undefined (e.g. a local firestore wrapper whose
|
|
47
|
-
* `setDocument` returns void, or a `getDocById` miss returning undefined). Defaults to strict,
|
|
48
|
-
* so RPC callers (LogApiFilter, ProxyClient) keep the safety net: an HTTP endpoint returning
|
|
49
|
-
* nothing is almost always a bug.
|
|
50
35
|
*
|
|
51
36
|
* Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,
|
|
52
37
|
* reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only
|
|
@@ -55,7 +40,7 @@ export declare class LogApiCallImpl {
|
|
|
55
40
|
* it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is
|
|
56
41
|
* exactly what the GCP filters (`jsonPayload.api.*`) want.
|
|
57
42
|
*/
|
|
58
|
-
execute(
|
|
43
|
+
execute(methodInfo: ApiMethodInfo, requestDto: any, method: (dto: any) => Promise<any>): Promise<any>;
|
|
59
44
|
/**
|
|
60
45
|
* Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api
|
|
61
46
|
* result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?
|
package/src/http/LogApiCall.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LogApiCall = exports.LogApiCallImpl =
|
|
3
|
+
exports.LogApiCall = exports.LogApiCallImpl = void 0;
|
|
4
4
|
const errors_1 = require("./errors");
|
|
5
5
|
const errorUtils_1 = require("../lib/errorUtils");
|
|
6
6
|
const LogManager_1 = require("../logging/LogManager");
|
|
@@ -8,18 +8,6 @@ const ApiCallInfo_1 = require("./ApiCallInfo");
|
|
|
8
8
|
const ApiCallContext_1 = require("./ApiCallContext");
|
|
9
9
|
const WebpiecesCoreHeaders_1 = require("./WebpiecesCoreHeaders");
|
|
10
10
|
const log = LogManager_1.LogManager.getLogger('LogApiCall');
|
|
11
|
-
/**
|
|
12
|
-
* Options for {@link LogApiCallImpl.execute}. `allowVoidResponse` opts OUT of the strict
|
|
13
|
-
* falsy-response guard for callers that legitimately return void/undefined (e.g. a local firestore
|
|
14
|
-
* wrapper). Defaults to strict so RPC callers keep the safety net.
|
|
15
|
-
*/
|
|
16
|
-
class LogApiCallOptions {
|
|
17
|
-
allowVoidResponse;
|
|
18
|
-
constructor(allowVoidResponse) {
|
|
19
|
-
this.allowVoidResponse = allowVoidResponse;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
exports.LogApiCallOptions = LogApiCallOptions;
|
|
23
11
|
/**
|
|
24
12
|
* LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and
|
|
25
13
|
* client-side (ProxyClient) for one consistent logging shape across the framework.
|
|
@@ -29,7 +17,7 @@ exports.LogApiCallOptions = LogApiCallOptions;
|
|
|
29
17
|
* 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the
|
|
30
18
|
* {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the
|
|
31
19
|
* req/resp lines) inherits a filterable `api` object — surfacing in GCP as
|
|
32
|
-
* `jsonPayload.api.{side,type,result
|
|
20
|
+
* `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.
|
|
33
21
|
*
|
|
34
22
|
* BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →
|
|
35
23
|
* BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).
|
|
@@ -49,15 +37,10 @@ class LogApiCallImpl {
|
|
|
49
37
|
/**
|
|
50
38
|
* Execute an API call with logging + `api` context-tagging around it.
|
|
51
39
|
*
|
|
52
|
-
* @param
|
|
53
|
-
*
|
|
54
|
-
* @param requestDto - The request DTO
|
|
40
|
+
* @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,
|
|
41
|
+
* controllerName?). `apiClass` is what matches a client call to its server handler in the logs.
|
|
42
|
+
* @param requestDto - The request DTO (external multi-param callers synthesize a small object)
|
|
55
43
|
* @param method - The method to execute
|
|
56
|
-
* @param options - `allowVoidResponse: true` opts OUT of the strict falsy-response guard, for
|
|
57
|
-
* callers that legitimately return void/undefined (e.g. a local firestore wrapper whose
|
|
58
|
-
* `setDocument` returns void, or a `getDocById` miss returning undefined). Defaults to strict,
|
|
59
|
-
* so RPC callers (LogApiFilter, ProxyClient) keep the safety net: an HTTP endpoint returning
|
|
60
|
-
* nothing is almost always a bug.
|
|
61
44
|
*
|
|
62
45
|
* Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,
|
|
63
46
|
* reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only
|
|
@@ -66,13 +49,11 @@ class LogApiCallImpl {
|
|
|
66
49
|
* it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is
|
|
67
50
|
* exactly what the GCP filters (`jsonPayload.api.*`) want.
|
|
68
51
|
*/
|
|
69
|
-
async execute(
|
|
52
|
+
async execute(methodInfo,
|
|
70
53
|
// webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches ProxyClient)
|
|
71
54
|
requestDto,
|
|
72
55
|
// webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary
|
|
73
|
-
method
|
|
74
|
-
// webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary
|
|
75
|
-
) {
|
|
56
|
+
method) {
|
|
76
57
|
// Throws if no ApiCallContext was installed at startup, or there is no active scope to stamp
|
|
77
58
|
// into (loud misconfiguration — an api call with nowhere to tag is a bug).
|
|
78
59
|
const ctx = ApiCallContext_1.ApiCallContextHolder.get();
|
|
@@ -81,8 +62,9 @@ class LogApiCallImpl {
|
|
|
81
62
|
'RequestContext.run(...) a server filter opens; in a browser, build ClientHttpBrowserFactory first.');
|
|
82
63
|
}
|
|
83
64
|
const key = WebpiecesCoreHeaders_1.WebpiecesCoreHeaders.API_CALL_INFO;
|
|
65
|
+
const side = methodInfo.side;
|
|
84
66
|
const server = side === 'server';
|
|
85
|
-
const
|
|
67
|
+
const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;
|
|
86
68
|
// set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,
|
|
87
69
|
// never across an await, so a single browser global slot can never be clobbered by a concurrent call.
|
|
88
70
|
const stamp = (info, emit) => {
|
|
@@ -92,13 +74,11 @@ class LogApiCallImpl {
|
|
|
92
74
|
};
|
|
93
75
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller
|
|
94
76
|
try {
|
|
95
|
-
stamp(new ApiCallInfo_1.ApiCallInfo(
|
|
77
|
+
stamp(new ApiCallInfo_1.ApiCallInfo(methodInfo, 'request', undefined), () => log.info(`[API-${side}-req] ${id} request=${JSON.stringify(requestDto)}`));
|
|
96
78
|
if (!requestDto)
|
|
97
|
-
throw new Error(`Request cannot be null and was from ${
|
|
79
|
+
throw new Error(`Request cannot be null and was from ${id}`);
|
|
98
80
|
const response = await method(requestDto);
|
|
99
|
-
|
|
100
|
-
throw new Error(`Response cannot be null and was from ${cls}.${meta.methodName}`);
|
|
101
|
-
stamp(new ApiCallInfo_1.ApiCallInfo(side, 'response', 'success', meta.path, meta.methodName, cls), () => log.info(`[API-${side}-resp-SUCCESS] ${cls}.${meta.methodName} response=${JSON.stringify(response)}`));
|
|
81
|
+
stamp(new ApiCallInfo_1.ApiCallInfo(methodInfo, 'response', 'success'), () => log.info(`[API-${side}-resp-SUCCESS] ${id} response=${JSON.stringify(response)}`));
|
|
102
82
|
return response;
|
|
103
83
|
}
|
|
104
84
|
catch (err) {
|
|
@@ -108,9 +88,9 @@ class LogApiCallImpl {
|
|
|
108
88
|
// Side-dependent: a 4xx the SERVER raised is a handled non-failure (OTHER); the same 4xx a
|
|
109
89
|
// CLIENT receives means its call FAILED. HttpUserError (266) is never a failure, either side.
|
|
110
90
|
const isUser = this.isUserError(error, server);
|
|
111
|
-
stamp(new ApiCallInfo_1.ApiCallInfo(
|
|
112
|
-
? log.warn(`[API-${side}-resp-OTHER] ${
|
|
113
|
-
: log.error(`[API-${side}-resp-FAIL] ${
|
|
91
|
+
stamp(new ApiCallInfo_1.ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure'), () => isUser
|
|
92
|
+
? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)
|
|
93
|
+
: log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${errorMessage}`));
|
|
114
94
|
throw error;
|
|
115
95
|
}
|
|
116
96
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":"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","sourcesContent":["import {RouteMetadata} from \"./decorators\";\nimport {\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpNotFoundError,\n HttpUserError,\n} from './errors';\nimport {toError} from \"../lib/errorUtils\";\nimport {LogManager} from \"../logging/LogManager\";\nimport {ApiCallInfo, ApiSide} from \"./ApiCallInfo\";\nimport {ApiCallContextHolder} from \"./ApiCallContext\";\nimport {WebpiecesCoreHeaders} from \"./WebpiecesCoreHeaders\";\n\nconst log = LogManager.getLogger('LogApiCall');\n\n/**\n * Options for {@link LogApiCallImpl.execute}. `allowVoidResponse` opts OUT of the strict\n * falsy-response guard for callers that legitimately return void/undefined (e.g. a local firestore\n * wrapper). Defaults to strict so RPC callers keep the safety net.\n */\nexport class LogApiCallOptions {\n allowVoidResponse?: boolean;\n\n constructor(allowVoidResponse?: boolean) {\n this.allowVoidResponse = allowVoidResponse;\n }\n}\n\n/**\n * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and\n * client-side (ProxyClient) for one consistent logging shape across the framework.\n *\n * TWO things happen around each call:\n * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).\n * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the\n * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the\n * req/resp lines) inherits a filterable `api` object — surfacing in GCP as\n * `jsonPayload.api.{side,type,result,path,method}`.\n *\n * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →\n * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).\n * It stamps through the {@link ApiCallContext} seam instead: `setupRuntime` installs a\n * RequestContext-backed impl on a Node server, and `ClientHttpBrowserFactory` a module-global impl in a\n * browser. If neither ran, {@link ApiCallContextHolder.get} throws (loud misconfiguration).\n *\n * Singleton, mirroring `RequestContext`: use the exported {@link LogApiCall} constant, not `new`.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} (user errors)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (server errors)\n */\nexport class LogApiCallImpl {\n\n /**\n * Execute an API call with logging + `api` context-tagging around it.\n *\n * @param side - 'client' (outbound call this process made) or 'server' (inbound call it handled)\n * @param meta - Route metadata with controllerClassName and methodName\n * @param requestDto - The request DTO\n * @param method - The method to execute\n * @param options - `allowVoidResponse: true` opts OUT of the strict falsy-response guard, for\n * callers that legitimately return void/undefined (e.g. a local firestore wrapper whose\n * `setDocument` returns void, or a `getDocById` miss returning undefined). Defaults to strict,\n * so RPC callers (LogApiFilter, ProxyClient) keep the safety net: an HTTP endpoint returning\n * nothing is almost always a bug.\n *\n * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,\n * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only\n * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across\n * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber\n * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is\n * exactly what the GCP filters (`jsonPayload.api.*`) want.\n */\n public async execute(\n side: ApiSide,\n meta: RouteMetadata,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches ProxyClient)\n requestDto: any,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n method: (dto: any) => Promise<any>,\n options?: LogApiCallOptions\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n ): Promise<any> {\n // Throws if no ApiCallContext was installed at startup, or there is no active scope to stamp\n // into (loud misconfiguration — an api call with nowhere to tag is a bug).\n const ctx = ApiCallContextHolder.get();\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside the ' +\n 'RequestContext.run(...) a server filter opens; in a browser, build ClientHttpBrowserFactory first.',\n );\n }\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n const server = side === 'server';\n const cls = meta.controllerClassName;\n // set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,\n // never across an await, so a single browser global slot can never be clobbered by a concurrent call.\n const stamp = (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n emit();\n ctx.remove(key);\n };\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller\n try {\n stamp(new ApiCallInfo(side, 'request', undefined, meta.path, meta.methodName, cls), () =>\n log.info(`[API-${side}-req] ${cls}.${meta.methodName} ${meta.path} request=${JSON.stringify(requestDto)}`));\n\n if(!requestDto)\n throw new Error(`Request cannot be null and was from ${cls}.${meta.methodName}`);\n\n const response = await method(requestDto);\n\n if(!options?.allowVoidResponse && !response)\n throw new Error(`Response cannot be null and was from ${cls}.${meta.methodName}`);\n\n stamp(new ApiCallInfo(side, 'response', 'success', meta.path, meta.methodName, cls), () =>\n log.info(`[API-${side}-resp-SUCCESS] ${cls}.${meta.methodName} response=${JSON.stringify(response)}`));\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n const errorType = error.constructor.name;\n const errorMessage = error.message;\n // Side-dependent: a 4xx the SERVER raised is a handled non-failure (OTHER); the same 4xx a\n // CLIENT receives means its call FAILED. HttpUserError (266) is never a failure, either side.\n const isUser = this.isUserError(error, server);\n\n stamp(new ApiCallInfo(side, 'response', isUser ? 'success' : 'failure', meta.path, meta.methodName, cls), () =>\n isUser\n ? log.warn(`[API-${side}-resp-OTHER] ${cls}.${meta.methodName} errorType=${errorType}`)\n : log.error(`[API-${side}-resp-FAIL] ${cls}.${meta.methodName} errorType=${errorType} error=${errorMessage}`));\n throw error;\n }\n }\n\n /**\n * Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api\n * result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?\n *\n * The question is \"are things WORKING?\", NOT \"was it an HTTP 4xx vs 5xx\" — and the two are not the\n * same (see 408 below). Classification is by portable Error TYPE, never by transport: LogApiCall runs\n * deep in the stack (in-process calls, pubsub/queue handlers, HTTP — one code path) and only ever\n * sees a thrown Error. The Http* classes are poorly named for that (the `Http` prefix is historical),\n * but they ARE portable error types that travel with the throw anywhere, so matching the TYPE works\n * with or without any HTTP in the picture. Do NOT swap this for an `error.code` 4xx range check — that\n * both re-couples to HTTP AND would bury 408.\n *\n * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:\n * - HttpBadRequestError (400) \"your request is malformed\"\n * - HttpUnauthorizedError (401) \"you're not authenticated\"\n * - HttpForbiddenError (403) \"authenticated, but not allowed\"\n * - HttpNotFoundError (404) \"wrong url / no such entity\" (EndpointNotFoundError is a subclass)\n * → the server is fine, the caller erred → result:'success', logged OTHER.\n *\n * SERVER — something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):\n * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately\n * absent from the list below so it counts as a failure.\n * - 500 / 502 / 504 / 598, and any non-Http Error: real failures.\n *\n * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected \"user made a mistake\" signal.\n * CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.\n *\n * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)\n * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call\n * @returns true if this should be treated as a non-failure (OTHER / result:'success')\n */\n isUserError(error: Error, server: boolean): boolean {\n // 266 is the one error that is never a failure, on either side.\n if (error instanceof HttpUserError) {\n return true;\n }\n // A client that RECEIVED any error (except the 266 above) made a failed call.\n if (!server) {\n return false;\n }\n // SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408\n // (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.\n return (\n error instanceof HttpBadRequestError ||\n error instanceof HttpUnauthorizedError ||\n error instanceof HttpForbiddenError ||\n error instanceof HttpNotFoundError\n );\n }\n}\n\n/**\n * The process-wide {@link LogApiCallImpl} singleton — mirrors the `RequestContext` export pattern.\n * Callers use `LogApiCall.execute(...)`, never `new`.\n */\nexport const LogApiCall = new LogApiCallImpl();\n"]}
|
|
1
|
+
{"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpNotFoundError,\n HttpUserError,\n} from './errors';\nimport {toError} from \"../lib/errorUtils\";\nimport {LogManager} from \"../logging/LogManager\";\nimport {ApiCallInfo} from \"./ApiCallInfo\";\nimport {ApiMethodInfo} from \"./ApiMethodInfo\";\nimport {ApiCallContextHolder} from \"./ApiCallContext\";\nimport {WebpiecesCoreHeaders} from \"./WebpiecesCoreHeaders\";\n\nconst log = LogManager.getLogger('LogApiCall');\n\n/**\n * LogApiCall - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and\n * client-side (ProxyClient) for one consistent logging shape across the framework.\n *\n * TWO things happen around each call:\n * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).\n * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the\n * {@link ApiCallContextHolder} seam, so EVERY log line emitted during the call (not just the\n * req/resp lines) inherits a filterable `api` object — surfacing in GCP as\n * `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.\n *\n * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →\n * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).\n * It stamps through the {@link ApiCallContext} seam instead: `setupRuntime` installs a\n * RequestContext-backed impl on a Node server, and `ClientHttpBrowserFactory` a module-global impl in a\n * browser. If neither ran, {@link ApiCallContextHolder.get} throws (loud misconfiguration).\n *\n * Singleton, mirroring `RequestContext`: use the exported {@link LogApiCall} constant, not `new`.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} (user errors)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (server errors)\n */\nexport class LogApiCallImpl {\n\n /**\n * Execute an API call with logging + `api` context-tagging around it.\n *\n * @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,\n * controllerName?). `apiClass` is what matches a client call to its server handler in the logs.\n * @param requestDto - The request DTO (external multi-param callers synthesize a small object)\n * @param method - The method to execute\n *\n * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,\n * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only\n * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across\n * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber\n * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is\n * exactly what the GCP filters (`jsonPayload.api.*`) want.\n */\n public async execute(\n methodInfo: ApiMethodInfo,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches ProxyClient)\n requestDto: any,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n method: (dto: any) => Promise<any>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary\n ): Promise<any> {\n // Throws if no ApiCallContext was installed at startup, or there is no active scope to stamp\n // into (loud misconfiguration — an api call with nowhere to tag is a bug).\n const ctx = ApiCallContextHolder.get();\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside the ' +\n 'RequestContext.run(...) a server filter opens; in a browser, build ClientHttpBrowserFactory first.',\n );\n }\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n const side = methodInfo.side;\n const server = side === 'server';\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n // set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,\n // never across an await, so a single browser global slot can never be clobbered by a concurrent call.\n const stamp = (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n emit();\n ctx.remove(key);\n };\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller\n try {\n stamp(new ApiCallInfo(methodInfo, 'request', undefined), () =>\n log.info(`[API-${side}-req] ${id} request=${JSON.stringify(requestDto)}`));\n\n if(!requestDto)\n throw new Error(`Request cannot be null and was from ${id}`);\n\n const response = await method(requestDto);\n\n stamp(new ApiCallInfo(methodInfo, 'response', 'success'), () =>\n log.info(`[API-${side}-resp-SUCCESS] ${id} response=${JSON.stringify(response)}`));\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n const errorType = error.constructor.name;\n const errorMessage = error.message;\n // Side-dependent: a 4xx the SERVER raised is a handled non-failure (OTHER); the same 4xx a\n // CLIENT receives means its call FAILED. HttpUserError (266) is never a failure, either side.\n const isUser = this.isUserError(error, server);\n\n stamp(new ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure'), () =>\n isUser\n ? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)\n : log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${errorMessage}`));\n throw error;\n }\n }\n\n /**\n * Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api\n * result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?\n *\n * The question is \"are things WORKING?\", NOT \"was it an HTTP 4xx vs 5xx\" — and the two are not the\n * same (see 408 below). Classification is by portable Error TYPE, never by transport: LogApiCall runs\n * deep in the stack (in-process calls, pubsub/queue handlers, HTTP — one code path) and only ever\n * sees a thrown Error. The Http* classes are poorly named for that (the `Http` prefix is historical),\n * but they ARE portable error types that travel with the throw anywhere, so matching the TYPE works\n * with or without any HTTP in the picture. Do NOT swap this for an `error.code` 4xx range check — that\n * both re-couples to HTTP AND would bury 408.\n *\n * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:\n * - HttpBadRequestError (400) \"your request is malformed\"\n * - HttpUnauthorizedError (401) \"you're not authenticated\"\n * - HttpForbiddenError (403) \"authenticated, but not allowed\"\n * - HttpNotFoundError (404) \"wrong url / no such entity\" (EndpointNotFoundError is a subclass)\n * → the server is fine, the caller erred → result:'success', logged OTHER.\n *\n * SERVER — something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):\n * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately\n * absent from the list below so it counts as a failure.\n * - 500 / 502 / 504 / 598, and any non-Http Error: real failures.\n *\n * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected \"user made a mistake\" signal.\n * CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.\n *\n * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)\n * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call\n * @returns true if this should be treated as a non-failure (OTHER / result:'success')\n */\n isUserError(error: Error, server: boolean): boolean {\n // 266 is the one error that is never a failure, on either side.\n if (error instanceof HttpUserError) {\n return true;\n }\n // A client that RECEIVED any error (except the 266 above) made a failed call.\n if (!server) {\n return false;\n }\n // SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408\n // (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.\n return (\n error instanceof HttpBadRequestError ||\n error instanceof HttpUnauthorizedError ||\n error instanceof HttpForbiddenError ||\n error instanceof HttpNotFoundError\n );\n }\n}\n\n/**\n * The process-wide {@link LogApiCallImpl} singleton — mirrors the `RequestContext` export pattern.\n * Callers use `LogApiCall.execute(...)`, never `new`.\n */\nexport const LogApiCall = new LogApiCallImpl();\n"]}
|
|
@@ -42,6 +42,19 @@ export declare class WebpiecesCoreHeaders {
|
|
|
42
42
|
* `buildLogFields()` string map deliberately skips it (typeof-string guard).
|
|
43
43
|
*/
|
|
44
44
|
static readonly API_CALL_INFO: ContextKey;
|
|
45
|
+
/**
|
|
46
|
+
* The inbound request's HTTP method and path, stamped ONCE from the {@link HttpRequest} by
|
|
47
|
+
* `RequestContextHeaders.fillFromRequest` (the atomic inbound choke point every transport funnels
|
|
48
|
+
* through). They surface as top-level `jsonPayload.httpMethod` / `jsonPayload.requestPath` so every
|
|
49
|
+
* log line of the request carries them — they used to ride inside {@link ApiCallInfo} (`api.path` /
|
|
50
|
+
* `api.method`) but that coupled a per-CALL logger to the per-REQUEST transport shape.
|
|
51
|
+
*
|
|
52
|
+
* - `httpHeader` UNDEFINED → NOT transferred over the wire: a downstream hop stamps its OWN inbound
|
|
53
|
+
* method/path, never the caller's. Outbound client calls never set these (no inbound path).
|
|
54
|
+
* - `isLogged` TRUE → emitted by the logging backends as plain strings.
|
|
55
|
+
*/
|
|
56
|
+
static readonly HTTP_METHOD: ContextKey;
|
|
57
|
+
static readonly REQUEST_PATH: ContextKey;
|
|
45
58
|
/**
|
|
46
59
|
* NO CREDENTIAL KEYS LIVE HERE.
|
|
47
60
|
*
|
|
@@ -45,6 +45,19 @@ class WebpiecesCoreHeaders {
|
|
|
45
45
|
* `buildLogFields()` string map deliberately skips it (typeof-string guard).
|
|
46
46
|
*/
|
|
47
47
|
static API_CALL_INFO = new ContextKey_1.ContextKey('api', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);
|
|
48
|
+
/**
|
|
49
|
+
* The inbound request's HTTP method and path, stamped ONCE from the {@link HttpRequest} by
|
|
50
|
+
* `RequestContextHeaders.fillFromRequest` (the atomic inbound choke point every transport funnels
|
|
51
|
+
* through). They surface as top-level `jsonPayload.httpMethod` / `jsonPayload.requestPath` so every
|
|
52
|
+
* log line of the request carries them — they used to ride inside {@link ApiCallInfo} (`api.path` /
|
|
53
|
+
* `api.method`) but that coupled a per-CALL logger to the per-REQUEST transport shape.
|
|
54
|
+
*
|
|
55
|
+
* - `httpHeader` UNDEFINED → NOT transferred over the wire: a downstream hop stamps its OWN inbound
|
|
56
|
+
* method/path, never the caller's. Outbound client calls never set these (no inbound path).
|
|
57
|
+
* - `isLogged` TRUE → emitted by the logging backends as plain strings.
|
|
58
|
+
*/
|
|
59
|
+
static HTTP_METHOD = new ContextKey_1.ContextKey('httpMethod', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);
|
|
60
|
+
static REQUEST_PATH = new ContextKey_1.ContextKey('requestPath', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);
|
|
48
61
|
/**
|
|
49
62
|
* NO CREDENTIAL KEYS LIVE HERE.
|
|
50
63
|
*
|
|
@@ -72,6 +85,8 @@ class WebpiecesCoreHeaders {
|
|
|
72
85
|
WebpiecesCoreHeaders.USER_ROLES,
|
|
73
86
|
WebpiecesCoreHeaders.RECORDING,
|
|
74
87
|
WebpiecesCoreHeaders.API_CALL_INFO,
|
|
88
|
+
WebpiecesCoreHeaders.HTTP_METHOD,
|
|
89
|
+
WebpiecesCoreHeaders.REQUEST_PATH,
|
|
75
90
|
];
|
|
76
91
|
}
|
|
77
92
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesCoreHeaders.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesCoreHeaders.ts"],"names":[],"mappings":";;;AAAA,8CAA2C;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,MAAa,oBAAoB;IAC7B;;;OAGG;IACH,MAAM,CAAU,UAAU,GAAG,IAAI,uBAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAEzE,MAAM,CAAU,MAAM,GAAG,IAAI,uBAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAE7D,MAAM,CAAU,OAAO,GAAG,IAAI,uBAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IAEhE,MAAM,CAAU,UAAU,GAAG,IAAI,uBAAU,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IAE1E;;;OAGG;IACH,MAAM,CAAU,SAAS,GAAG,IAAI,uBAAU,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC;IAEjF;;;;;;;;;;;OAWG;IACH,MAAM,CAAU,aAAa,GAAG,IAAI,uBAAU,CAAC,KAAK,EAAE,cAAc,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IAExH;;;;;;;;;;;;;;;OAeG;IAEH;;OAEG;IACH,MAAM,CAAC,aAAa;QAChB,OAAO;YACH,oBAAoB,CAAC,UAAU;YAC/B,oBAAoB,CAAC,OAAO;YAC5B,oBAAoB,CAAC,MAAM;YAC3B,oBAAoB,CAAC,UAAU;YAC/B,oBAAoB,CAAC,SAAS;YAC9B,oBAAoB,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"WebpiecesCoreHeaders.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesCoreHeaders.ts"],"names":[],"mappings":";;;AAAA,8CAA2C;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,MAAa,oBAAoB;IAC7B;;;OAGG;IACH,MAAM,CAAU,UAAU,GAAG,IAAI,uBAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAEzE,MAAM,CAAU,MAAM,GAAG,IAAI,uBAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAE7D,MAAM,CAAU,OAAO,GAAG,IAAI,uBAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IAEhE,MAAM,CAAU,UAAU,GAAG,IAAI,uBAAU,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IAE1E;;;OAGG;IACH,MAAM,CAAU,SAAS,GAAG,IAAI,uBAAU,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC;IAEjF;;;;;;;;;;;OAWG;IACH,MAAM,CAAU,aAAa,GAAG,IAAI,uBAAU,CAAC,KAAK,EAAE,cAAc,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IAExH;;;;;;;;;;OAUG;IACH,MAAM,CAAU,WAAW,GAAG,IAAI,uBAAU,CAAC,YAAY,EAAE,cAAc,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IAE7H,MAAM,CAAU,YAAY,GAAG,IAAI,uBAAU,CAAC,aAAa,EAAE,cAAc,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IAE/H;;;;;;;;;;;;;;;OAeG;IAEH;;OAEG;IACH,MAAM,CAAC,aAAa;QAChB,OAAO;YACH,oBAAoB,CAAC,UAAU;YAC/B,oBAAoB,CAAC,OAAO;YAC5B,oBAAoB,CAAC,MAAM;YAC3B,oBAAoB,CAAC,UAAU;YAC/B,oBAAoB,CAAC,SAAS;YAC9B,oBAAoB,CAAC,aAAa;YAClC,oBAAoB,CAAC,WAAW;YAChC,oBAAoB,CAAC,YAAY;SACpC,CAAC;IACN,CAAC;;AA/EL,oDAgFC","sourcesContent":["import { ContextKey } from '../ContextKey';\n\n/**\n * Core framework context keys — the minimum the WebPieces framework needs to correlate one\n * request across every service it touches, and across every log line each of them writes.\n *\n * ONE id, propagated unchanged. The first service to see a request without an `x-request-id`\n * generates one (RequestContextHeaders.fillFromRequest); every hop copies it onward verbatim. Grep that id and you\n * have the whole call tree. There is no per-hop id and no parent pointer: a chain of ids you must\n * stitch back together buys nothing a single shared id does not already give you.\n *\n * Lives in core-util (browser-safe) so both the http clients and http-server can reference it.\n *\n * Exposed as {@link HeaderRegistry.DEFAULT_HEADERS} — a service opts into these by\n * passing `platformHeaders=true` to `HeaderRegistry.configure(...)`.\n *\n * Each key's `name` is the logical/log name; `httpHeader` is the wire name.\n */\nexport class WebpiecesCoreHeaders {\n /**\n * The id that correlates every hop of one request, and every log line of every hop.\n * Generated by the first service to see a request without one; propagated unchanged after that.\n */\n static readonly REQUEST_ID = new ContextKey('requestId', 'x-request-id');\n\n static readonly ORG_ID = new ContextKey('orgId', 'x-org-id');\n\n static readonly USER_ID = new ContextKey('userId', 'x-user-id');\n\n static readonly USER_ROLES = new ContextKey('roles', 'x-webpieces-roles');\n\n /**\n * Turns on test-case recording for this request (Java: x-webpieces-recording).\n * Transferred so recording follows the request across service hops.\n */\n static readonly RECORDING = new ContextKey('recording', 'x-webpieces-recording');\n\n /**\n * The structured API-call tag ({@link ApiCallInfo}) stamped by {@link LogApiCall} around every\n * outbound (client) / inbound (server) call. It rides the magic context so EVERY log line emitted\n * during the call inherits a filterable `api` object, surfacing in GCP as nested\n * `jsonPayload.api.{side,type,result,path,method}`.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire. Per-hop only: each server/client hop\n * stamps its own tag, so a downstream server records `side:'server'`, never the caller's `side:'client'`.\n * - `isLogged` TRUE → emitted by the logging backends. It carries an OBJECT value, so the backends\n * read it via {@link HeaderRegistry.buildStructuredLogFields} (object-aware); the flat\n * `buildLogFields()` string map deliberately skips it (typeof-string guard).\n */\n static readonly API_CALL_INFO = new ContextKey('api', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);\n\n /**\n * The inbound request's HTTP method and path, stamped ONCE from the {@link HttpRequest} by\n * `RequestContextHeaders.fillFromRequest` (the atomic inbound choke point every transport funnels\n * through). They surface as top-level `jsonPayload.httpMethod` / `jsonPayload.requestPath` so every\n * log line of the request carries them — they used to ride inside {@link ApiCallInfo} (`api.path` /\n * `api.method`) but that coupled a per-CALL logger to the per-REQUEST transport shape.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire: a downstream hop stamps its OWN inbound\n * method/path, never the caller's. Outbound client calls never set these (no inbound path).\n * - `isLogged` TRUE → emitted by the logging backends as plain strings.\n */\n static readonly HTTP_METHOD = new ContextKey('httpMethod', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);\n\n static readonly REQUEST_PATH = new ContextKey('requestPath', /*httpHeader*/ undefined, /*isSecured*/ false, /*isLogged*/ true);\n\n /**\n * NO CREDENTIAL KEYS LIVE HERE.\n *\n * `authorization` and `x-webpieces-shared-secret` used to be ContextKeys. That made them\n * TRANSFERRED keys, so the inbound transfer copied them off the request into the\n * RequestContext, and every outbound RPC call and enqueued Cloud Task then carried the\n * caller's credential onward — to services that had no business seeing it.\n *\n * A credential belongs to ONE request hop. It is read straight off the {@link HttpRequest}\n * by the framework AuthFilter, and written straight onto the outbound request by the client\n * that mints it (NodeProxyClient, GcpTaskInvoker, InMemoryTaskInvoker). It never enters the\n * magic context, so nothing can propagate it by accident.\n *\n * An app that genuinely wants a credential to travel can still register its own ContextKey for\n * it — but that is now an explicit, visible decision rather than the default.\n */\n\n /**\n * Get all core context keys as an array (the platform DEFAULT_HEADERS set).\n */\n static getAllHeaders(): ContextKey[] {\n return [\n WebpiecesCoreHeaders.REQUEST_ID,\n WebpiecesCoreHeaders.USER_ID,\n WebpiecesCoreHeaders.ORG_ID,\n WebpiecesCoreHeaders.USER_ROLES,\n WebpiecesCoreHeaders.RECORDING,\n WebpiecesCoreHeaders.API_CALL_INFO,\n WebpiecesCoreHeaders.HTTP_METHOD,\n WebpiecesCoreHeaders.REQUEST_PATH,\n ];\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -31,9 +31,11 @@ export { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';
|
|
|
31
31
|
export { ContextReader } from './http/ContextReader';
|
|
32
32
|
export type { ContextRead, StructuredContextRead } from './http/ContextReader';
|
|
33
33
|
export { ContextMgr } from './http/ContextMgr';
|
|
34
|
-
export { LogApiCall, LogApiCallImpl
|
|
34
|
+
export { LogApiCall, LogApiCallImpl } from './http/LogApiCall';
|
|
35
35
|
export { ApiCallInfo } from './http/ApiCallInfo';
|
|
36
|
-
export type {
|
|
36
|
+
export type { ApiType, ApiResult } from './http/ApiCallInfo';
|
|
37
|
+
export { ApiMethodInfo } from './http/ApiMethodInfo';
|
|
38
|
+
export type { ApiSide } from './http/ApiMethodInfo';
|
|
37
39
|
export { ApiCallContextHolder } from './http/ApiCallContext';
|
|
38
40
|
export type { ApiCallContext } from './http/ApiCallContext';
|
|
39
41
|
export { TestCaseRecorder, RecorderKeys } from './http/recorder/TestCaseRecorder';
|
package/src/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.HttpVendorError = exports.HttpInternalServerError = exports.HttpGatewayTimeoutError = exports.HttpBadGatewayError = exports.HttpTimeoutError = exports.HttpForbiddenError = exports.HttpUnauthorizedError = exports.HttpBadRequestError = exports.EndpointNotFoundError = exports.HttpNotFoundError = exports.HttpError = exports.ProtocolError = exports.Secrets = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.validateNoConflictingDecorators = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.isFormPost = exports.getEndpointOptions = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.AuthSharedSecret = exports.AuthOidc = exports.Auth = exports.AuthJwt = exports.Public = exports.AuthenticationConfig = exports.Authentication = exports.Endpoint = exports.ApiPath = exports.LogManager = exports.ConsoleLoggerFactory = exports.ConsoleLogger = exports.DESIGN_METADATA_KEYS = exports.isDocumentDesign = exports.DocumentDesign = exports.ContextTuple = exports.ContextKey = exports.toError = void 0;
|
|
12
|
-
exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.
|
|
12
|
+
exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.ApiMethodInfo = exports.ApiCallInfo = exports.LogApiCallImpl = exports.LogApiCall = exports.ContextMgr = exports.WebpiecesCoreHeaders = exports.templateDeriver = exports.ErrorWireForm = exports.ClientRegistry = exports.HeaderRegistry = exports.DateTimeUtil = exports.TimeUtil = exports.DateUtil = exports.InstantUtil = exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.HttpUserError = 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");
|
|
@@ -121,11 +121,12 @@ Object.defineProperty(exports, "ContextMgr", { enumerable: true, get: function (
|
|
|
121
121
|
var LogApiCall_1 = require("./http/LogApiCall");
|
|
122
122
|
Object.defineProperty(exports, "LogApiCall", { enumerable: true, get: function () { return LogApiCall_1.LogApiCall; } });
|
|
123
123
|
Object.defineProperty(exports, "LogApiCallImpl", { enumerable: true, get: function () { return LogApiCall_1.LogApiCallImpl; } });
|
|
124
|
-
Object.defineProperty(exports, "LogApiCallOptions", { enumerable: true, get: function () { return LogApiCall_1.LogApiCallOptions; } });
|
|
125
124
|
// The structured `api` tag + the context-writer seam LogApiCall stamps through. The Node
|
|
126
125
|
// RequestContext-backed impl is installed by @webpieces/core-context; the browser gets the no-op.
|
|
127
126
|
var ApiCallInfo_1 = require("./http/ApiCallInfo");
|
|
128
127
|
Object.defineProperty(exports, "ApiCallInfo", { enumerable: true, get: function () { return ApiCallInfo_1.ApiCallInfo; } });
|
|
128
|
+
var ApiMethodInfo_1 = require("./http/ApiMethodInfo");
|
|
129
|
+
Object.defineProperty(exports, "ApiMethodInfo", { enumerable: true, get: function () { return ApiMethodInfo_1.ApiMethodInfo; } });
|
|
129
130
|
var ApiCallContext_1 = require("./http/ApiCallContext");
|
|
130
131
|
Object.defineProperty(exports, "ApiCallContextHolder", { enumerable: true, get: function () { return ApiCallContext_1.ApiCallContextHolder; } });
|
|
131
132
|
// Test-case recording contract (impl lives in http-server; hooks in http-client)
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @webpieces/core-util\n *\n * Utility functions for WebPieces applications.\n * This package works in both browser and Node.js environments.\n *\n * @packageDocumentation\n */\n\nexport { toError } from './lib/errorUtils';\nexport { ContextKey } from './ContextKey';\nexport { ContextTuple } from './ContextTuple';\n\n// @DocumentDesign — DI-design-root marker. Applies to ANY project kind (server\n// controllers AND library impl classes), so it lives here (browser + Node) rather\n// than in a server-only routing package.\nexport { DocumentDesign, isDocumentDesign, DESIGN_METADATA_KEYS } from './DocumentDesign';\n\n// Logging (merged from former @webpieces/wp-logging).\n// Pluggable logging interface + a browser-safe console default; apps plug in\n// bunyan/winston/pino/etc. via LogManager.setFactory(...). Browser + Node.\nexport type { Logger, LogLevel } from './logging/Logger';\nexport type { LoggerFactory } from './logging/LoggerFactory';\nexport { ConsoleLogger } from './logging/ConsoleLogger';\nexport { ConsoleLoggerFactory } from './logging/ConsoleLoggerFactory';\nexport { LogManager } from './logging/LogManager';\n\n// HTTP API contract (merged from former @webpieces/http-api).\n// Shared HTTP API definition consumed by both client and server: REST\n// decorators, the HttpError hierarchy, datetime DTOs, platform-header\n// registry/readers, ValidateImplementation, and the test-case recorder\n// contract. Pure definitions — express-free, browser + Node safe.\n\n// API definition decorators\nexport {\n ApiPath,\n Endpoint,\n Authentication,\n AuthenticationConfig,\n // Auth mode decorators (clean service-to-service + user JWT model)\n Public,\n AuthJwt,\n Auth,\n AuthOidc,\n AuthSharedSecret,\n // API kind (RPC vs PubSub/Cloud Tasks) + queue naming\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n isFormPost,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n validateNoConflictingDecorators,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n} from './http/decorators';\nexport type { AuthMode, ApiKind, JwtRequirement, EndpointOptions } from './http/decorators';\n// Client-side shared-secret store (the value THIS service sends per @AuthSharedSecret key).\nexport { Secrets } from './http/Secrets';\n\n// Type validators\nexport { ValidateImplementation } from './http/validators';\n\n// HTTP errors\nexport {\n ProtocolError,\n HttpError,\n HttpNotFoundError,\n EndpointNotFoundError,\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpTimeoutError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n HttpInternalServerError,\n HttpVendorError,\n HttpUserError,\n // Error subtype constants\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './http/errors';\n\n// Date/Time DTOs and Utilities (inspired by Java Time / JSR-310)\nexport {\n InstantDto,\n DateDto,\n TimeDto,\n DateTimeDto,\n InstantUtil,\n DateUtil,\n TimeUtil,\n DateTimeUtil,\n} from './http/datetime';\n\n// Context keys + registry (the global magic-context header system)\nexport { HeaderRegistry } from './http/HeaderRegistry';\nexport { ClientRegistry } from './http/ClientRegistry';\nexport type { ServiceUrlDeriver } from './http/ClientRegistry';\n// Pluggable, bidirectional error translation (app exception <-> wire form). Registered on\n// ClientRegistry at startup; consulted before the built-in webpieces mapping on BOTH sides.\nexport { ErrorWireForm } from './http/ErrorTranslation';\nexport type { ErrorTranslation } from './http/ErrorTranslation';\nexport { templateDeriver } from './http/templateDeriver';\nexport { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';\nexport { ContextReader } from './http/ContextReader';\nexport type { ContextRead, StructuredContextRead } from './http/ContextReader';\n\n// BROWSER-ONLY outbound-header propagation (app-held store + registry -> outbound HTTP headers).\n// Only @webpieces/http-client-browser may name it; the server reads RequestContext directly via\n// RequestContextHeaders in the Node-only @webpieces/core-context.\nexport { ContextMgr } from './http/ContextMgr';\n\n// API-call logging helper (uses LogManager above). Singleton: use the LogApiCall constant, not `new`.\nexport { LogApiCall, LogApiCallImpl } from './http/LogApiCall';\n\n// The structured `api` tag + the context-writer seam LogApiCall stamps through. The Node\n// RequestContext-backed impl is installed by @webpieces/core-context; the browser gets the no-op.\nexport { ApiCallInfo } from './http/ApiCallInfo';\nexport type { ApiType, ApiResult } from './http/ApiCallInfo';\nexport { ApiMethodInfo } from './http/ApiMethodInfo';\nexport type { ApiSide } from './http/ApiMethodInfo';\nexport { ApiCallContextHolder } from './http/ApiCallContext';\nexport type { ApiCallContext } from './http/ApiCallContext';\n\n// Test-case recording contract (impl lives in http-server; hooks in http-client)\nexport { TestCaseRecorder, RecorderKeys } from './http/recorder/TestCaseRecorder';\nexport { RecordedEndpoint, RecordedError, RecordedTestCase } from './http/recorder/RecordedEndpoint';\nexport { DoNotRecord, getDoNotRecordFields } from './http/recorder/DoNotRecord';\nexport { RecordSerializer, SerializedMap, SerializedError } from './http/recorder/RecordSerializer';\n"]}
|