@webpieces/core-util 0.4.412 → 0.4.414

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/core-util",
3
- "version": "0.4.412",
3
+ "version": "0.4.414",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -1,5 +1,7 @@
1
1
  import { ErrorTranslation, ErrorWireForm } from './ErrorTranslation';
2
2
  import { ProtocolError } from './errors';
3
+ import { FailureClassifier } from './FailureClassifier';
4
+ import { ApiMethodInfo } from './ApiMethodInfo';
3
5
  /**
4
6
  * Derives a base URL for a service name that has NO registered mapping — the pluggable half of
5
7
  * {@link ClientRegistry} resolution. One per environment, installed once at startup:
@@ -54,6 +56,20 @@ export declare class ClientRegistry {
54
56
  * work. See {@link ErrorTranslation}.
55
57
  */
56
58
  private static readonly errorTranslations;
59
+ /**
60
+ * The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
61
+ * so a single strategy covers the server router AND all internal clients. Undefined = use the
62
+ * webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}). Populated once at startup on the
63
+ * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
64
+ */
65
+ private static appDefaultFailureClassifier;
66
+ /**
67
+ * Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}
68
+ * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external
69
+ * client overrides the default for its own apiClass only. Internal clients / the server register
70
+ * nothing here and fall through to the default. See {@link ClientRegistry.classifyFailure}.
71
+ */
72
+ private static readonly failureClassifiersByApiClass;
57
73
  /** Map a service name to `http://localhost:<port>`. */
58
74
  static addMapping(svcName: string, port: number): void;
59
75
  /**
@@ -108,6 +124,31 @@ export declare class ClientRegistry {
108
124
  * to the generic webpieces switch.
109
125
  */
110
126
  static tryTranslateFromWire(statusCode: number, protocolError: ProtocolError): Error | undefined;
111
- /** Reset mappings, the deriver, AND error translations. For tests, so the process-globals do not leak across specs. */
127
+ /**
128
+ * Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and
129
+ * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the
130
+ * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier
131
+ * claims. See {@link FailureClassifier}.
132
+ */
133
+ static setDefaultFailureClassifier(classifier: FailureClassifier): void;
134
+ /**
135
+ * Register a per-EXTERNAL-client failure classifier, keyed by {@link ApiMethodInfo.apiClass}
136
+ * (e.g. 'FirestoreAdminClient'). Consulted BEFORE the default tier for calls with that apiClass.
137
+ * Call ONCE at startup. See {@link FailureClassifier}.
138
+ */
139
+ static addFailureClassifier(apiClass: string, classifier: FailureClassifier): void;
140
+ /**
141
+ * Is this thrown API-call error a real FAILURE (true) or an expected non-failure (false)?
142
+ * Resolved most-specific-first, and ALWAYS definitive (returns a boolean):
143
+ * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;
144
+ * 2. else the app default classifier, if one was set — unless it defers;
145
+ * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}), which never defers.
146
+ * A `?? true` backstop keeps an unexpectedly-deferring built-in fail-safe (treat as failure).
147
+ */
148
+ static classifyFailure(error: Error, methodInfo: ApiMethodInfo): boolean;
149
+ /**
150
+ * Reset mappings, the deriver, error translations, AND failure classifiers. For tests, so the
151
+ * process-globals do not leak across specs.
152
+ */
112
153
  static clear(): void;
113
154
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClientRegistry = void 0;
4
+ const WebpiecesDefaultFailureClassifier_1 = require("./WebpiecesDefaultFailureClassifier");
4
5
  /**
5
6
  * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every
6
7
  * environment. A client is built once but a URL is per-environment, so the URL belongs here rather
@@ -42,6 +43,20 @@ class ClientRegistry {
42
43
  * work. See {@link ErrorTranslation}.
43
44
  */
44
45
  static errorTranslations = [];
46
+ /**
47
+ * The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
48
+ * so a single strategy covers the server router AND all internal clients. Undefined = use the
49
+ * webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}). Populated once at startup on the
50
+ * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
51
+ */
52
+ static appDefaultFailureClassifier;
53
+ /**
54
+ * Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}
55
+ * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external
56
+ * client overrides the default for its own apiClass only. Internal clients / the server register
57
+ * nothing here and fall through to the default. See {@link ClientRegistry.classifyFailure}.
58
+ */
59
+ static failureClassifiersByApiClass = new Map();
45
60
  /** Map a service name to `http://localhost:<port>`. */
46
61
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
47
62
  static addMapping(svcName, port) {
@@ -159,12 +174,61 @@ class ClientRegistry {
159
174
  }
160
175
  return undefined;
161
176
  }
162
- /** Reset mappings, the deriver, AND error translations. For tests, so the process-globals do not leak across specs. */
177
+ /**
178
+ * Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and
179
+ * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the
180
+ * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier
181
+ * claims. See {@link FailureClassifier}.
182
+ */
183
+ // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
184
+ static setDefaultFailureClassifier(classifier) {
185
+ ClientRegistry.appDefaultFailureClassifier = classifier;
186
+ }
187
+ /**
188
+ * Register a per-EXTERNAL-client failure classifier, keyed by {@link ApiMethodInfo.apiClass}
189
+ * (e.g. 'FirestoreAdminClient'). Consulted BEFORE the default tier for calls with that apiClass.
190
+ * Call ONCE at startup. See {@link FailureClassifier}.
191
+ */
192
+ // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
193
+ static addFailureClassifier(apiClass, classifier) {
194
+ ClientRegistry.failureClassifiersByApiClass.set(apiClass, classifier);
195
+ }
196
+ /**
197
+ * Is this thrown API-call error a real FAILURE (true) or an expected non-failure (false)?
198
+ * Resolved most-specific-first, and ALWAYS definitive (returns a boolean):
199
+ * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;
200
+ * 2. else the app default classifier, if one was set — unless it defers;
201
+ * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}), which never defers.
202
+ * A `?? true` backstop keeps an unexpectedly-deferring built-in fail-safe (treat as failure).
203
+ */
204
+ // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
205
+ static classifyFailure(error, methodInfo) {
206
+ const perClient = ClientRegistry.failureClassifiersByApiClass.get(methodInfo.apiClass);
207
+ if (perClient !== undefined) {
208
+ const verdict = perClient.isFailure(error, methodInfo);
209
+ if (verdict !== undefined) {
210
+ return verdict;
211
+ }
212
+ }
213
+ if (ClientRegistry.appDefaultFailureClassifier !== undefined) {
214
+ const verdict = ClientRegistry.appDefaultFailureClassifier.isFailure(error, methodInfo);
215
+ if (verdict !== undefined) {
216
+ return verdict;
217
+ }
218
+ }
219
+ return WebpiecesDefaultFailureClassifier_1.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, methodInfo) ?? true;
220
+ }
221
+ /**
222
+ * Reset mappings, the deriver, error translations, AND failure classifiers. For tests, so the
223
+ * process-globals do not leak across specs.
224
+ */
163
225
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
164
226
  static clear() {
165
227
  ClientRegistry.mappings.clear();
166
228
  ClientRegistry.deriver = undefined;
167
229
  ClientRegistry.errorTranslations.length = 0;
230
+ ClientRegistry.appDefaultFailureClassifier = undefined;
231
+ ClientRegistry.failureClassifiersByApiClass.clear();
168
232
  }
169
233
  }
170
234
  exports.ClientRegistry = ClientRegistry;
@@ -1 +1 @@
1
- {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslation, ErrorWireForm } from './ErrorTranslation';\nimport { ProtocolError } from './errors';\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * App-supplied error translations, consulted BEFORE webpieces' built-in error mapping (both\n * directions). Process-global, populated once at startup on the SERVER and in the BROWSER — the\n * same no-DI pattern as {@link ClientRegistry.mappings} above. Consulted in registration order,\n * first match wins, so a later-registered app type AND an override of a built-in status both\n * work. See {@link ErrorTranslation}.\n */\n private static readonly errorTranslations: ErrorTranslation[] = [];\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - typo? svcName must be the CLOUD RUN service name, not your project/module name`,\n );\n }\n return url;\n }\n\n /**\n * Register an app error translation. Consulted BEFORE webpieces' built-in mapping, in\n * registration order (first match wins), so later app types AND overrides of built-ins both\n * work. Call ONCE at startup — on the server AND in the browser — mirroring\n * {@link ClientRegistry.addMapping}. See {@link ErrorTranslation}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addErrorTranslation(translation: ErrorTranslation): void {\n ClientRegistry.errorTranslations.push(translation);\n }\n\n /**\n * exception → wire (SERVER side). The first registered translation that claims `error` wins;\n * `undefined` if none does — the caller then falls through to the generic webpieces mapping.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryTranslateToWire(error: Error): ErrorWireForm | undefined {\n for (const translation of ClientRegistry.errorTranslations) {\n const wire = translation.toWire(error);\n if (wire !== undefined) {\n return wire;\n }\n }\n return undefined;\n }\n\n /**\n * wire → exception (CLIENT side). The first registered translation that claims\n * `(statusCode, protocolError)` wins; `undefined` if none does — the caller then falls through\n * to the generic webpieces switch.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryTranslateFromWire(statusCode: number, protocolError: ProtocolError): Error | undefined {\n for (const translation of ClientRegistry.errorTranslations) {\n const err = translation.fromWire(statusCode, protocolError);\n if (err !== undefined) {\n return err;\n }\n }\n return undefined;\n }\n\n /** Reset mappings, the deriver, AND error translations. For tests, so the process-globals do not leak across specs. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static clear(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslations.length = 0;\n }\n}\n"]}
1
+ {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslation, ErrorWireForm } from './ErrorTranslation';\nimport { ProtocolError } from './errors';\nimport { FailureClassifier } from './FailureClassifier';\nimport { WEBPIECES_DEFAULT_FAILURE_CLASSIFIER } from './WebpiecesDefaultFailureClassifier';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * App-supplied error translations, consulted BEFORE webpieces' built-in error mapping (both\n * directions). Process-global, populated once at startup on the SERVER and in the BROWSER — the\n * same no-DI pattern as {@link ClientRegistry.mappings} above. Consulted in registration order,\n * first match wins, so a later-registered app type AND an override of a built-in status both\n * work. See {@link ErrorTranslation}.\n */\n private static readonly errorTranslations: ErrorTranslation[] = [];\n\n /**\n * The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}\n * so a single strategy covers the server router AND all internal clients. Undefined = use the\n * webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}). Populated once at startup on the\n * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.\n */\n private static appDefaultFailureClassifier: FailureClassifier | undefined;\n\n /**\n * Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}\n * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external\n * client overrides the default for its own apiClass only. Internal clients / the server register\n * nothing here and fall through to the default. See {@link ClientRegistry.classifyFailure}.\n */\n private static readonly failureClassifiersByApiClass = new Map<string, FailureClassifier>();\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - typo? svcName must be the CLOUD RUN service name, not your project/module name`,\n );\n }\n return url;\n }\n\n /**\n * Register an app error translation. Consulted BEFORE webpieces' built-in mapping, in\n * registration order (first match wins), so later app types AND overrides of built-ins both\n * work. Call ONCE at startup — on the server AND in the browser — mirroring\n * {@link ClientRegistry.addMapping}. See {@link ErrorTranslation}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addErrorTranslation(translation: ErrorTranslation): void {\n ClientRegistry.errorTranslations.push(translation);\n }\n\n /**\n * exception → wire (SERVER side). The first registered translation that claims `error` wins;\n * `undefined` if none does — the caller then falls through to the generic webpieces mapping.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryTranslateToWire(error: Error): ErrorWireForm | undefined {\n for (const translation of ClientRegistry.errorTranslations) {\n const wire = translation.toWire(error);\n if (wire !== undefined) {\n return wire;\n }\n }\n return undefined;\n }\n\n /**\n * wire → exception (CLIENT side). The first registered translation that claims\n * `(statusCode, protocolError)` wins; `undefined` if none does — the caller then falls through\n * to the generic webpieces switch.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryTranslateFromWire(statusCode: number, protocolError: ProtocolError): Error | undefined {\n for (const translation of ClientRegistry.errorTranslations) {\n const err = translation.fromWire(statusCode, protocolError);\n if (err !== undefined) {\n return err;\n }\n }\n return undefined;\n }\n\n /**\n * Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and\n * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the\n * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier\n * claims. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDefaultFailureClassifier(classifier: FailureClassifier): void {\n ClientRegistry.appDefaultFailureClassifier = classifier;\n }\n\n /**\n * Register a per-EXTERNAL-client failure classifier, keyed by {@link ApiMethodInfo.apiClass}\n * (e.g. 'FirestoreAdminClient'). Consulted BEFORE the default tier for calls with that apiClass.\n * Call ONCE at startup. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addFailureClassifier(apiClass: string, classifier: FailureClassifier): void {\n ClientRegistry.failureClassifiersByApiClass.set(apiClass, classifier);\n }\n\n /**\n * Is this thrown API-call error a real FAILURE (true) or an expected non-failure (false)?\n * Resolved most-specific-first, and ALWAYS definitive (returns a boolean):\n * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;\n * 2. else the app default classifier, if one was set — unless it defers;\n * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}), which never defers.\n * A `?? true` backstop keeps an unexpectedly-deferring built-in fail-safe (treat as failure).\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classifyFailure(error: Error, methodInfo: ApiMethodInfo): boolean {\n const perClient = ClientRegistry.failureClassifiersByApiClass.get(methodInfo.apiClass);\n if (perClient !== undefined) {\n const verdict = perClient.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n if (ClientRegistry.appDefaultFailureClassifier !== undefined) {\n const verdict = ClientRegistry.appDefaultFailureClassifier.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n return WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, methodInfo) ?? true;\n }\n\n /**\n * Reset mappings, the deriver, error translations, AND failure classifiers. For tests, so the\n * process-globals do not leak across specs.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static clear(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslations.length = 0;\n ClientRegistry.appDefaultFailureClassifier = undefined;\n ClientRegistry.failureClassifiersByApiClass.clear();\n }\n}\n"]}
@@ -0,0 +1,48 @@
1
+ import { ApiMethodInfo } from './ApiMethodInfo';
2
+ /**
3
+ * Decides whether a thrown API-call error is a real FAILURE — the process not working, SURFACE it
4
+ * (LogApiCall logs `[API-*-resp-FAIL]`, `jsonPayload.api.result='failure'` — what dashboards/alerts
5
+ * count) — or an EXPECTED non-failure — the process working correctly, a handled condition
6
+ * (`[API-*-resp-OTHER]`, `result='success'`).
7
+ *
8
+ * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method
9
+ * rather than a bare function — so "find usages" in an IDE lands on every implementation.
10
+ *
11
+ * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once
12
+ * singleton that owns URL mappings and {@link ErrorTranslation}. TWO tiers, resolved most-specific
13
+ * first (see {@link ClientRegistry.classifyFailure}):
14
+ *
15
+ * 1. `ClientRegistry.setDefaultFailureClassifier(c)` — ONE per app/company. It reads
16
+ * {@link ApiMethodInfo.side}, so a single strategy covers the SERVER router AND all INTERNAL
17
+ * clients (webpieces http client, cloud tasks). Optional: if unset, webpieces uses
18
+ * {@link WebpiecesDefaultFailureClassifier} (server 4xx = non-failure; client non-266 = failure).
19
+ * 2. `ClientRegistry.addFailureClassifier(apiClass, c)` — per EXTERNAL client, keyed by
20
+ * {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi', 'TwilioApi'). Each external
21
+ * API qualifies errors differently (a Firestore 404 miss, a Twilio 429 retry are EXPECTED, not
22
+ * failures), so it overrides the default for that apiClass only.
23
+ *
24
+ * Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal;
25
+ * they fall through to tier 1. An external client that forgets to register is FAIL-SAFE: it also
26
+ * falls to tier 1 (client side ⇒ every non-266 = failure) until it opts in.
27
+ */
28
+ export interface FailureClassifier {
29
+ /**
30
+ * @param error - the already-normalized thrown error (callers pass `toError(err)`)
31
+ * @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client
32
+ * @returns `true` = real failure; `false` = expected/non-failure; `undefined` = DEFER
33
+ * (a per-apiClass classifier defers to the app default; the app default defers to the
34
+ * webpieces built-in). Deferring is what makes classifiers additive AND override-capable.
35
+ */
36
+ isFailure(error: Error, methodInfo: ApiMethodInfo): boolean | undefined;
37
+ }
38
+ /**
39
+ * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the
40
+ * {@link FailureClassifier} to apply for it. A data-only structure, so it is a class (not an inline
41
+ * object literal), per CLAUDE.md — lets startup wiring carry a list of these and hand each to
42
+ * {@link ClientRegistry.addFailureClassifier}.
43
+ */
44
+ export declare class KeyedFailureClassifier {
45
+ readonly apiClass: string;
46
+ readonly classifier: FailureClassifier;
47
+ constructor(apiClass: string, classifier: FailureClassifier);
48
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeyedFailureClassifier = void 0;
4
+ /**
5
+ * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the
6
+ * {@link FailureClassifier} to apply for it. A data-only structure, so it is a class (not an inline
7
+ * object literal), per CLAUDE.md — lets startup wiring carry a list of these and hand each to
8
+ * {@link ClientRegistry.addFailureClassifier}.
9
+ */
10
+ class KeyedFailureClassifier {
11
+ apiClass;
12
+ classifier;
13
+ constructor(apiClass, classifier) {
14
+ this.apiClass = apiClass;
15
+ this.classifier = classifier;
16
+ }
17
+ }
18
+ exports.KeyedFailureClassifier = KeyedFailureClassifier;
19
+ //# sourceMappingURL=FailureClassifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FailureClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/FailureClassifier.ts"],"names":[],"mappings":";;;AAuCA;;;;;GAKG;AACH,MAAa,sBAAsB;IAEX;IACA;IAFpB,YACoB,QAAgB,EAChB,UAA6B;QAD7B,aAAQ,GAAR,QAAQ,CAAQ;QAChB,eAAU,GAAV,UAAU,CAAmB;IAC9C,CAAC;CACP;AALD,wDAKC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Decides whether a thrown API-call error is a real FAILURE — the process not working, SURFACE it\n * (LogApiCall logs `[API-*-resp-FAIL]`, `jsonPayload.api.result='failure'` — what dashboards/alerts\n * count) — or an EXPECTED non-failure — the process working correctly, a handled condition\n * (`[API-*-resp-OTHER]`, `result='success'`).\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once\n * singleton that owns URL mappings and {@link ErrorTranslation}. TWO tiers, resolved most-specific\n * first (see {@link ClientRegistry.classifyFailure}):\n *\n * 1. `ClientRegistry.setDefaultFailureClassifier(c)` — ONE per app/company. It reads\n * {@link ApiMethodInfo.side}, so a single strategy covers the SERVER router AND all INTERNAL\n * clients (webpieces http client, cloud tasks). Optional: if unset, webpieces uses\n * {@link WebpiecesDefaultFailureClassifier} (server 4xx = non-failure; client non-266 = failure).\n * 2. `ClientRegistry.addFailureClassifier(apiClass, c)` — per EXTERNAL client, keyed by\n * {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi', 'TwilioApi'). Each external\n * API qualifies errors differently (a Firestore 404 miss, a Twilio 429 retry are EXPECTED, not\n * failures), so it overrides the default for that apiClass only.\n *\n * Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal;\n * they fall through to tier 1. An external client that forgets to register is FAIL-SAFE: it also\n * falls to tier 1 (client side ⇒ every non-266 = failure) until it opts in.\n */\nexport interface FailureClassifier {\n /**\n * @param error - the already-normalized thrown error (callers pass `toError(err)`)\n * @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client\n * @returns `true` = real failure; `false` = expected/non-failure; `undefined` = DEFER\n * (a per-apiClass classifier defers to the app default; the app default defers to the\n * webpieces built-in). Deferring is what makes classifiers additive AND override-capable.\n */\n isFailure(error: Error, methodInfo: ApiMethodInfo): boolean | undefined;\n}\n\n/**\n * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the\n * {@link FailureClassifier} to apply for it. A data-only structure, so it is a class (not an inline\n * object literal), per CLAUDE.md — lets startup wiring carry a list of these and hand each to\n * {@link ClientRegistry.addFailureClassifier}.\n */\nexport class KeyedFailureClassifier {\n constructor(\n public readonly apiClass: string,\n public readonly classifier: FailureClassifier,\n ) {}\n}\n"]}
@@ -60,28 +60,12 @@ export declare class LogApiCallImpl {
60
60
  * Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api
61
61
  * result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?
62
62
  *
63
- * The question is "are things WORKING?", NOT "was it an HTTP 4xx vs 5xx" — and the two are not the
64
- * same (see 408 below). Classification is by portable Error TYPE, never by transport: LogApiCall runs
65
- * deep in the stack (in-process calls, pubsub/queue handlers, HTTP — one code path) and only ever
66
- * sees a thrown Error. The Http* classes are poorly named for that (the `Http` prefix is historical),
67
- * but they ARE portable error types that travel with the throw anywhere, so matching the TYPE works
68
- * with or without any HTTP in the picture. Do NOT swap this for an `error.code` 4xx range check — that
69
- * both re-couples to HTTP AND would bury 408.
70
- *
71
- * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
72
- * - HttpBadRequestError (400) "your request is malformed"
73
- * - HttpUnauthorizedError (401) "you're not authenticated"
74
- * - HttpForbiddenError (403) "authenticated, but not allowed"
75
- * - HttpNotFoundError (404) "wrong url / no such entity" (EndpointNotFoundError is a subclass)
76
- * → the server is fine, the caller erred → result:'success', logged OTHER.
77
- *
78
- * SERVER — something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):
79
- * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
80
- * absent from the list below so it counts as a failure.
81
- * - 500 / 502 / 504 / 598, and any non-Http Error: real failures.
82
- *
83
- * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake" signal.
84
- * CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.
63
+ * BACK-COMPAT SHIM: the canonical logic now lives in {@link WebpiecesDefaultFailureClassifier}
64
+ * (the webpieces built-in tier), and the LIVE classification path is
65
+ * {@link ClientRegistry.classifyFailure} (per-apiClass app default built-in). This method
66
+ * delegates to the built-in so existing callers/tests keep the exact old behavior; it does NOT
67
+ * consult registered classifiers. `apiClass`/`methodName` are irrelevant to the built-in (it reads
68
+ * only `side`), hence the empty strings.
85
69
  *
86
70
  * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)
87
71
  * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call
@@ -1,13 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LogApiCall = exports.LogApiCallImpl = void 0;
4
- const errors_1 = require("./errors");
5
4
  const errorUtils_1 = require("../lib/errorUtils");
6
5
  const LogManager_1 = require("../logging/LogManager");
7
6
  const ApiCallInfo_1 = require("./ApiCallInfo");
7
+ const ApiMethodInfo_1 = require("./ApiMethodInfo");
8
8
  const ApiCallContext_1 = require("./ApiCallContext");
9
9
  const WebpiecesCoreHeaders_1 = require("./WebpiecesCoreHeaders");
10
10
  const ApiCallLogName_1 = require("./ApiCallLogName");
11
+ const ClientRegistry_1 = require("./ClientRegistry");
12
+ const WebpiecesDefaultFailureClassifier_1 = require("./WebpiecesDefaultFailureClassifier");
11
13
  // The console backends special-case THIS logger name into a self-describing [API.{side}.{phase}]
12
14
  // bracket (see ApiCallLogName) — so the name here and the name they match are the one constant.
13
15
  const log = LogManager_1.LogManager.getLogger(ApiCallLogName_1.LOG_API_CALL_LOGGER_NAME);
@@ -114,9 +116,11 @@ class LogApiCallImpl {
114
116
  const side = methodInfo.side;
115
117
  const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;
116
118
  const errorType = error.constructor.name;
117
- // Side-dependent: a 4xx the SERVER raised is a handled non-failure (OTHER); the same 4xx a
118
- // CLIENT receives means its call FAILED. HttpUserError (266) is never a failure, either side.
119
- const isUser = this.isUserError(error, side === 'server');
119
+ // Pluggable classification (ClientRegistry): a per-apiClass EXTERNAL-client classifier wins,
120
+ // else the app default, else the webpieces built-in — which is side-dependent (a 4xx the SERVER
121
+ // raised is a handled non-failure; the same 4xx a CLIENT receives means its call FAILED; 266 is
122
+ // never a failure either side). `isUser` = "treat as non-failure (OTHER / result:'success')".
123
+ const isUser = !ClientRegistry_1.ClientRegistry.classifyFailure(error, methodInfo);
120
124
  stamp(new ApiCallInfo_1.ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure', durationMs, requestSize), () => isUser
121
125
  ? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)
122
126
  : log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`));
@@ -136,48 +140,19 @@ class LogApiCallImpl {
136
140
  * Is this error a NON-failure for HEALTH/METRICS — the process working CORRECTLY (log OTHER, api
137
141
  * result:'success') — rather than a real failure to surface (log FAIL, result:'failure')?
138
142
  *
139
- * The question is "are things WORKING?", NOT "was it an HTTP 4xx vs 5xx" — and the two are not the
140
- * same (see 408 below). Classification is by portable Error TYPE, never by transport: LogApiCall runs
141
- * deep in the stack (in-process calls, pubsub/queue handlers, HTTP — one code path) and only ever
142
- * sees a thrown Error. The Http* classes are poorly named for that (the `Http` prefix is historical),
143
- * but they ARE portable error types that travel with the throw anywhere, so matching the TYPE works
144
- * with or without any HTTP in the picture. Do NOT swap this for an `error.code` 4xx range check — that
145
- * both re-couples to HTTP AND would bury 408.
146
- *
147
- * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
148
- * - HttpBadRequestError (400) "your request is malformed"
149
- * - HttpUnauthorizedError (401) "you're not authenticated"
150
- * - HttpForbiddenError (403) "authenticated, but not allowed"
151
- * - HttpNotFoundError (404) "wrong url / no such entity" (EndpointNotFoundError is a subclass)
152
- * → the server is fine, the caller erred → result:'success', logged OTHER.
153
- *
154
- * SERVER — something may actually be WRONG, so SURFACE it (result:'failure', logged FAIL):
155
- * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
156
- * absent from the list below so it counts as a failure.
157
- * - 500 / 502 / 504 / 598, and any non-Http Error: real failures.
158
- *
159
- * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake" signal.
160
- * CLIENT: receiving ANY error except 266 means the outbound call FAILED → result:'failure'.
143
+ * BACK-COMPAT SHIM: the canonical logic now lives in {@link WebpiecesDefaultFailureClassifier}
144
+ * (the webpieces built-in tier), and the LIVE classification path is
145
+ * {@link ClientRegistry.classifyFailure} (per-apiClass app default built-in). This method
146
+ * delegates to the built-in so existing callers/tests keep the exact old behavior; it does NOT
147
+ * consult registered classifiers. `apiClass`/`methodName` are irrelevant to the built-in (it reads
148
+ * only `side`), hence the empty strings.
161
149
  *
162
150
  * @param error - The already-normalized error (callers pass toError(err), never a raw catch value)
163
151
  * @param server - True when this side is the SERVER handling an inbound call; false for a CLIENT's outbound call
164
152
  * @returns true if this should be treated as a non-failure (OTHER / result:'success')
165
153
  */
166
154
  isUserError(error, server) {
167
- // 266 is the one error that is never a failure, on either side.
168
- if (error instanceof errors_1.HttpUserError) {
169
- return true;
170
- }
171
- // A client that RECEIVED any error (except the 266 above) made a failed call.
172
- if (!server) {
173
- return false;
174
- }
175
- // SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408
176
- // (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.
177
- return (error instanceof errors_1.HttpBadRequestError ||
178
- error instanceof errors_1.HttpUnauthorizedError ||
179
- error instanceof errors_1.HttpForbiddenError ||
180
- error instanceof errors_1.HttpNotFoundError);
155
+ return !WebpiecesDefaultFailureClassifier_1.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, new ApiMethodInfo_1.ApiMethodInfo(server ? 'server' : 'client', '', ''));
181
156
  }
182
157
  }
183
158
  exports.LogApiCallImpl = LogApiCallImpl;
@@ -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 {\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 {ApiCallContext, ApiCallContextHolder} from \"./ApiCallContext\";\nimport {WebpiecesCoreHeaders} from \"./WebpiecesCoreHeaders\";\nimport {LOG_API_CALL_LOGGER_NAME} from \"./ApiCallLogName\";\n\n// The console backends special-case THIS logger name into a self-describing [API.{side}.{phase}]\n// bracket (see ApiCallLogName) — so the name here and the name they match are the one constant.\nconst log = LogManager.getLogger(LOG_API_CALL_LOGGER_NAME);\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 const ctx = this.activeContext();\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n const side = methodInfo.side;\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 // Stringify ONCE and reuse for both the log text and the size — a second JSON.stringify of a\n // large DTO purely to measure it would double the cost of the thing we are measuring.\n const requestBody = JSON.stringify(requestDto);\n const requestSize = this.byteSize(requestBody);\n // Declared out here so the catch below can read it too. Reassigned just before the call, so\n // the number times ONLY the call and not our own request-logging.\n let startMs = Date.now();\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, undefined, requestSize), () =>\n log.info(`[API-${side}-req] ${id} request=${requestBody}`));\n\n if(!requestDto)\n throw new Error(`Request cannot be null and was from ${id}`);\n\n startMs = Date.now();\n const response = await method(requestDto);\n const durationMs = Date.now() - startMs;\n\n const responseBody = JSON.stringify(response);\n stamp(\n new ApiCallInfo(\n methodInfo, 'response', 'success', durationMs, requestSize, this.byteSize(responseBody),\n ),\n () => log.info(`[API-${side}-resp-SUCCESS] ${id} response=${responseBody}`));\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n // Duration comes off the SAME start as the success path, so a slow failure (a timeout, a\n // hung dependency) reports its real cost rather than nothing.\n this.logFailure(error, methodInfo, Date.now() - startMs, requestSize, stamp);\n throw error;\n }\n }\n\n /**\n * The ApiCallContext to stamp into. Throws if none was installed at startup, or if there is no\n * active scope — loud misconfiguration, because an api call with nowhere to tag is a bug.\n */\n private activeContext(): ApiCallContext {\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 return ctx;\n }\n\n /**\n * Tag + log a thrown call. There is no responseSize — a throw produced no response body to measure.\n */\n private logFailure(\n error: Error,\n methodInfo: ApiMethodInfo,\n durationMs: number,\n requestSize: number | undefined,\n stamp: (info: ApiCallInfo, emit: () => void) => void,\n ): void {\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const errorType = error.constructor.name;\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, side === 'server');\n\n stamp(\n new ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure', durationMs, requestSize),\n () => isUser\n ? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)\n : log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`));\n }\n\n /**\n * UTF-8 byte size of an already-serialized body. TextEncoder, not Buffer: LogApiCall runs in the\n * browser bundle. Undefined in, undefined out — a `Promise<void>` method has no body to measure,\n * and a 0 there would be a lie (JSON.stringify(undefined) returns undefined, not '').\n */\n private byteSize(serialized: string | undefined): number | undefined {\n if (serialized === undefined) {\n return undefined;\n }\n return new TextEncoder().encode(serialized).length;\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 {toError} from \"../lib/errorUtils\";\nimport {LogManager} from \"../logging/LogManager\";\nimport {ApiCallInfo} from \"./ApiCallInfo\";\nimport {ApiMethodInfo} from \"./ApiMethodInfo\";\nimport {ApiCallContext, ApiCallContextHolder} from \"./ApiCallContext\";\nimport {WebpiecesCoreHeaders} from \"./WebpiecesCoreHeaders\";\nimport {LOG_API_CALL_LOGGER_NAME} from \"./ApiCallLogName\";\nimport {ClientRegistry} from \"./ClientRegistry\";\nimport {WEBPIECES_DEFAULT_FAILURE_CLASSIFIER} from \"./WebpiecesDefaultFailureClassifier\";\n\n// The console backends special-case THIS logger name into a self-describing [API.{side}.{phase}]\n// bracket (see ApiCallLogName) — so the name here and the name they match are the one constant.\nconst log = LogManager.getLogger(LOG_API_CALL_LOGGER_NAME);\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 const ctx = this.activeContext();\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n const side = methodInfo.side;\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 // Stringify ONCE and reuse for both the log text and the size — a second JSON.stringify of a\n // large DTO purely to measure it would double the cost of the thing we are measuring.\n const requestBody = JSON.stringify(requestDto);\n const requestSize = this.byteSize(requestBody);\n // Declared out here so the catch below can read it too. Reassigned just before the call, so\n // the number times ONLY the call and not our own request-logging.\n let startMs = Date.now();\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, undefined, requestSize), () =>\n log.info(`[API-${side}-req] ${id} request=${requestBody}`));\n\n if(!requestDto)\n throw new Error(`Request cannot be null and was from ${id}`);\n\n startMs = Date.now();\n const response = await method(requestDto);\n const durationMs = Date.now() - startMs;\n\n const responseBody = JSON.stringify(response);\n stamp(\n new ApiCallInfo(\n methodInfo, 'response', 'success', durationMs, requestSize, this.byteSize(responseBody),\n ),\n () => log.info(`[API-${side}-resp-SUCCESS] ${id} response=${responseBody}`));\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n // Duration comes off the SAME start as the success path, so a slow failure (a timeout, a\n // hung dependency) reports its real cost rather than nothing.\n this.logFailure(error, methodInfo, Date.now() - startMs, requestSize, stamp);\n throw error;\n }\n }\n\n /**\n * The ApiCallContext to stamp into. Throws if none was installed at startup, or if there is no\n * active scope — loud misconfiguration, because an api call with nowhere to tag is a bug.\n */\n private activeContext(): ApiCallContext {\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 return ctx;\n }\n\n /**\n * Tag + log a thrown call. There is no responseSize — a throw produced no response body to measure.\n */\n private logFailure(\n error: Error,\n methodInfo: ApiMethodInfo,\n durationMs: number,\n requestSize: number | undefined,\n stamp: (info: ApiCallInfo, emit: () => void) => void,\n ): void {\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const errorType = error.constructor.name;\n // Pluggable classification (ClientRegistry): a per-apiClass EXTERNAL-client classifier wins,\n // else the app default, else the webpieces built-in — which is side-dependent (a 4xx the SERVER\n // raised is a handled non-failure; the same 4xx a CLIENT receives means its call FAILED; 266 is\n // never a failure either side). `isUser` = \"treat as non-failure (OTHER / result:'success')\".\n const isUser = !ClientRegistry.classifyFailure(error, methodInfo);\n\n stamp(\n new ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure', durationMs, requestSize),\n () => isUser\n ? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType}`)\n : log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`));\n }\n\n /**\n * UTF-8 byte size of an already-serialized body. TextEncoder, not Buffer: LogApiCall runs in the\n * browser bundle. Undefined in, undefined out — a `Promise<void>` method has no body to measure,\n * and a 0 there would be a lie (JSON.stringify(undefined) returns undefined, not '').\n */\n private byteSize(serialized: string | undefined): number | undefined {\n if (serialized === undefined) {\n return undefined;\n }\n return new TextEncoder().encode(serialized).length;\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 * BACK-COMPAT SHIM: the canonical logic now lives in {@link WebpiecesDefaultFailureClassifier}\n * (the webpieces built-in tier), and the LIVE classification path is\n * {@link ClientRegistry.classifyFailure} (per-apiClass → app default → built-in). This method\n * delegates to the built-in so existing callers/tests keep the exact old behavior; it does NOT\n * consult registered classifiers. `apiClass`/`methodName` are irrelevant to the built-in (it reads\n * only `side`), hence the empty strings.\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 return !WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(\n error,\n new ApiMethodInfo(server ? 'server' : 'client', '', ''),\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"]}
@@ -33,8 +33,8 @@
33
33
  * DOES NOT THROW ON READ. {@link getName} / {@link getVersion} return `undefined` when unset, so a
34
34
  * log line emitted before `setInfo` (early boot) still ships — it simply omits the version. Logging
35
35
  * never blocks on identity. The "a deployed build MUST identify itself" guarantee is enforced ONCE,
36
- * loudly, at startup by whoever requires it (`setupRuntime` calls {@link assertIdentified}), instead
37
- * of by every reader throwing.
36
+ * loudly, at startup by whoever calls {@link setInfo} — `setupRuntime` takes name+version as REQUIRED
37
+ * inputs and calls it, so a blank value throws and kills the deploy — instead of every reader throwing.
38
38
  */
39
39
  export declare class ServiceInfo {
40
40
  /** This service's name. Process-global; set once at startup. */
@@ -56,31 +56,15 @@ export declare class ServiceInfo {
56
56
  /**
57
57
  * This service's name, or `undefined` when {@link setInfo} has not been called. Does NOT throw:
58
58
  * readers (logging backends, requestIdSource, outbound CLIENT_VERSION) simply omit the field when
59
- * it is missing, so a pre-`setInfo` log line still emits. Callers that REQUIRE identity call
60
- * {@link assertIdentified} at startup instead.
59
+ * it is missing, so a pre-`setInfo` log line still emits. Identity is REQUIRED where it matters —
60
+ * `setupRuntime` takes name+version and calls {@link setInfo} at startup, so a real server is
61
+ * always identified before it serves traffic.
61
62
  */
62
63
  static getName(): string | undefined;
63
64
  /**
64
65
  * This build's version, or `undefined` when unset — same non-throwing contract as {@link getName}.
65
66
  */
66
67
  static getVersion(): string | undefined;
67
- /**
68
- * FAIL FAST, AT STARTUP. Throws unless BOTH name and version are set. Whoever wants the "a
69
- * deployed build must be able to say which build it is" guarantee calls this while booting
70
- * (`setupRuntime` does) — so a forgotten `setInfo` kills the deploy (the revision never goes
71
- * healthy) instead of quietly shipping logs that cannot say which build emitted them. Nothing on
72
- * the REQUEST path calls this: a missing log field must never 500 live traffic.
73
- *
74
- * @throws Error if {@link setInfo} was never called.
75
- */
76
- static assertIdentified(): void;
77
68
  /** Reset — for tests, mirroring {@link ClientRegistry.clear}. */
78
69
  static clear(): void;
79
- /**
80
- * The one actionable "you forgot to call setInfo" message, thrown by {@link assertIdentified}:
81
- * setInfo sets BOTH, so either being missing has the identical cause and the identical fix —
82
- * telling the caller only about the half they happened to read first would send them back for a
83
- * second round.
84
- */
85
- private static notSetMessage;
86
70
  }
@@ -36,8 +36,8 @@ exports.ServiceInfo = void 0;
36
36
  * DOES NOT THROW ON READ. {@link getName} / {@link getVersion} return `undefined` when unset, so a
37
37
  * log line emitted before `setInfo` (early boot) still ships — it simply omits the version. Logging
38
38
  * never blocks on identity. The "a deployed build MUST identify itself" guarantee is enforced ONCE,
39
- * loudly, at startup by whoever requires it (`setupRuntime` calls {@link assertIdentified}), instead
40
- * of by every reader throwing.
39
+ * loudly, at startup by whoever calls {@link setInfo} — `setupRuntime` takes name+version as REQUIRED
40
+ * inputs and calls it, so a blank value throws and kills the deploy — instead of every reader throwing.
41
41
  */
42
42
  class ServiceInfo {
43
43
  /** This service's name. Process-global; set once at startup. */
@@ -71,8 +71,9 @@ class ServiceInfo {
71
71
  /**
72
72
  * This service's name, or `undefined` when {@link setInfo} has not been called. Does NOT throw:
73
73
  * readers (logging backends, requestIdSource, outbound CLIENT_VERSION) simply omit the field when
74
- * it is missing, so a pre-`setInfo` log line still emits. Callers that REQUIRE identity call
75
- * {@link assertIdentified} at startup instead.
74
+ * it is missing, so a pre-`setInfo` log line still emits. Identity is REQUIRED where it matters —
75
+ * `setupRuntime` takes name+version and calls {@link setInfo} at startup, so a real server is
76
+ * always identified before it serves traffic.
76
77
  */
77
78
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected
78
79
  static getName() {
@@ -85,42 +86,12 @@ class ServiceInfo {
85
86
  static getVersion() {
86
87
  return ServiceInfo.svcVersion;
87
88
  }
88
- /**
89
- * FAIL FAST, AT STARTUP. Throws unless BOTH name and version are set. Whoever wants the "a
90
- * deployed build must be able to say which build it is" guarantee calls this while booting
91
- * (`setupRuntime` does) — so a forgotten `setInfo` kills the deploy (the revision never goes
92
- * healthy) instead of quietly shipping logs that cannot say which build emitted them. Nothing on
93
- * the REQUEST path calls this: a missing log field must never 500 live traffic.
94
- *
95
- * @throws Error if {@link setInfo} was never called.
96
- */
97
- // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected
98
- static assertIdentified() {
99
- if (!ServiceInfo.svcName || !ServiceInfo.svcVersion) {
100
- throw new Error(ServiceInfo.notSetMessage());
101
- }
102
- }
103
89
  /** Reset — for tests, mirroring {@link ClientRegistry.clear}. */
104
90
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected
105
91
  static clear() {
106
92
  ServiceInfo.svcName = undefined;
107
93
  ServiceInfo.svcVersion = undefined;
108
94
  }
109
- /**
110
- * The one actionable "you forgot to call setInfo" message, thrown by {@link assertIdentified}:
111
- * setInfo sets BOTH, so either being missing has the identical cause and the identical fix —
112
- * telling the caller only about the half they happened to read first would send them back for a
113
- * second round.
114
- */
115
- // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected
116
- static notSetMessage() {
117
- return ('ServiceInfo.setInfo(...) has not been called. Identify this service at startup:\n' +
118
- " ServiceInfo.setInfo('my-service', '2.1.0');\n" +
119
- 'The name+version stamp every log line (so you can tell WHICH BUILD emitted a line), ' +
120
- 'the name records which service minted a request id (requestIdSource), and the version ' +
121
- 'travels to downstream servers as clientVersion. The version is opaque — a git SHA, a ' +
122
- 'semver tag, a CI build number, whatever identifies your build.');
123
- }
124
95
  }
125
96
  exports.ServiceInfo = ServiceInfo;
126
97
  //# sourceMappingURL=ServiceInfo.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ServiceInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ServiceInfo.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAa,WAAW;IACpB,gEAAgE;IACxD,MAAM,CAAC,OAAO,CAAqB;IAE3C,+FAA+F;IACvF,MAAM,CAAC,UAAU,CAAqB;IAE9C;;;;;;;;;;OAUG;IACH,4JAA4J;IAC5J,MAAM,CAAC,OAAO,CAAC,IAAY,EAAE,OAAe;QACxC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACX,qFAAqF;gBACrF,qFAAqF;gBACrF,2BAA2B,CAC9B,CAAC;QACN,CAAC;QACD,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,WAAW,CAAC,UAAU,GAAG,OAAO,CAAC;IACrC,CAAC;IAED;;;;;OAKG;IACH,4JAA4J;IAC5J,MAAM,CAAC,OAAO;QACV,OAAO,WAAW,CAAC,OAAO,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,4JAA4J;IAC5J,MAAM,CAAC,UAAU;QACb,OAAO,WAAW,CAAC,UAAU,CAAC;IAClC,CAAC;IAED;;;;;;;;OAQG;IACH,4JAA4J;IAC5J,MAAM,CAAC,gBAAgB;QACnB,IAAI,CAAC,WAAW,CAAC,OAAO,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,WAAW,CAAC,aAAa,EAAE,CAAC,CAAC;QACjD,CAAC;IACL,CAAC;IAED,iEAAiE;IACjE,4JAA4J;IAC5J,MAAM,CAAC,KAAK;QACR,WAAW,CAAC,OAAO,GAAG,SAAS,CAAC;QAChC,WAAW,CAAC,UAAU,GAAG,SAAS,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACH,4JAA4J;IACpJ,MAAM,CAAC,aAAa;QACxB,OAAO,CACH,mFAAmF;YACnF,mDAAmD;YACnD,sFAAsF;YACtF,wFAAwF;YACxF,uFAAuF;YACvF,gEAAgE,CACnE,CAAC;IACN,CAAC;CACJ;AA7FD,kCA6FC","sourcesContent":["/**\n * ServiceInfo - the ONE answer to \"what service am I, and which build of it?\", for every part of\n * webpieces that needs it.\n *\n * Configured like {@link HeaderRegistry} / {@link ClientRegistry} / LogManager — populated once at\n * startup, then globally accessible with NO DI wiring. Browser-safe (no `process.env`, no node-only\n * deps), which is why it lives in core-util beside {@link ClientRegistry}.\n *\n * ```ts\n * // startup, FIRST:\n * ServiceInfo.setInfo('my-service', '2.1.0');\n * ```\n *\n * WHY THIS EXISTS. Both facts used to be artifacts of WHICH LOGGING LIBRARY YOU PICKED rather than\n * facts about the service:\n * - the name lived on `BunyanFactoryOptions`, because bunyan REQUIRES a root-logger name\n * (`options.name (string) is required` — bunyan's own TypeError). winston has no such concept, so\n * a winston app shipped its logs UNNAMED.\n * - the version lived on `WinstonFactoryOptions` as `svcGitHash`, so a bunyan app could not stamp a\n * version AT ALL — and the name presumed a git SHA, which not every project deploys from.\n *\n * Switching backends silently changed which fields your logs carried. Several unrelated readers\n * need these same two facts, so they belong to the framework, not to a backend:\n * - the winston backend, to stamp `svcName` + `version`;\n * - the bunyan backend, to stamp `version` (its root-logger `name` is a fixed constant now);\n * - {@link WebpiecesCoreHeaders.REQUEST_ID_SOURCE}, to record WHO minted a request id;\n * - {@link WebpiecesCoreHeaders.CLIENT_VERSION}, so a downstream server can log which build called it.\n *\n * VERSION IS OPAQUE. It is whatever string identifies THIS build — a git SHA, a semver tag, a CI\n * build number. webpieces neither parses nor derives it; the app decides where it comes from (a\n * generated file, an env var, a Docker build arg) and passes it here.\n *\n * DOES NOT THROW ON READ. {@link getName} / {@link getVersion} return `undefined` when unset, so a\n * log line emitted before `setInfo` (early boot) still ships — it simply omits the version. Logging\n * never blocks on identity. The \"a deployed build MUST identify itself\" guarantee is enforced ONCE,\n * loudly, at startup by whoever requires it (`setupRuntime` calls {@link assertIdentified}), instead\n * of by every reader throwing.\n */\nexport class ServiceInfo {\n /** This service's name. Process-global; set once at startup. */\n private static svcName: string | undefined;\n\n /** This build's version — an opaque app-chosen string. Process-global; set once at startup. */\n private static svcVersion: string | undefined;\n\n /**\n * Identify this service. Call it at startup.\n *\n * LAST CALL WINS, deliberately. A real deployment identifies itself once, but an in-process test\n * can legitimately boot TWO services back-to-back (see the app-example e2e two-server flow), so a\n * \"one process = one service\" rule would reject a case that genuinely exists.\n *\n * @param name - this service's name, e.g. 'my-service'.\n * @param version - the opaque identifier of THIS build (git SHA, semver, CI build number).\n * @throws Error on a blank name or version — that is always a bug, never a use case.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static setInfo(name: string, version: string): void {\n if (!name || !name.trim()) {\n throw new Error('ServiceInfo.setInfo(...) requires a non-blank service name.');\n }\n if (!version || !version.trim()) {\n throw new Error(\n 'ServiceInfo.setInfo(...) requires a non-blank version. It is opaque to webpieces — ' +\n 'a git SHA, a semver tag, a CI build number — but every deployed build must be able ' +\n 'to say which build it is.',\n );\n }\n ServiceInfo.svcName = name;\n ServiceInfo.svcVersion = version;\n }\n\n /**\n * This service's name, or `undefined` when {@link setInfo} has not been called. Does NOT throw:\n * readers (logging backends, requestIdSource, outbound CLIENT_VERSION) simply omit the field when\n * it is missing, so a pre-`setInfo` log line still emits. Callers that REQUIRE identity call\n * {@link assertIdentified} at startup instead.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static getName(): string | undefined {\n return ServiceInfo.svcName;\n }\n\n /**\n * This build's version, or `undefined` when unset — same non-throwing contract as {@link getName}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static getVersion(): string | undefined {\n return ServiceInfo.svcVersion;\n }\n\n /**\n * FAIL FAST, AT STARTUP. Throws unless BOTH name and version are set. Whoever wants the \"a\n * deployed build must be able to say which build it is\" guarantee calls this while booting\n * (`setupRuntime` does) — so a forgotten `setInfo` kills the deploy (the revision never goes\n * healthy) instead of quietly shipping logs that cannot say which build emitted them. Nothing on\n * the REQUEST path calls this: a missing log field must never 500 live traffic.\n *\n * @throws Error if {@link setInfo} was never called.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static assertIdentified(): void {\n if (!ServiceInfo.svcName || !ServiceInfo.svcVersion) {\n throw new Error(ServiceInfo.notSetMessage());\n }\n }\n\n /** Reset — for tests, mirroring {@link ClientRegistry.clear}. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static clear(): void {\n ServiceInfo.svcName = undefined;\n ServiceInfo.svcVersion = undefined;\n }\n\n /**\n * The one actionable \"you forgot to call setInfo\" message, thrown by {@link assertIdentified}:\n * setInfo sets BOTH, so either being missing has the identical cause and the identical fix —\n * telling the caller only about the half they happened to read first would send them back for a\n * second round.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n private static notSetMessage(): string {\n return (\n 'ServiceInfo.setInfo(...) has not been called. Identify this service at startup:\\n' +\n \" ServiceInfo.setInfo('my-service', '2.1.0');\\n\" +\n 'The name+version stamp every log line (so you can tell WHICH BUILD emitted a line), ' +\n 'the name records which service minted a request id (requestIdSource), and the version ' +\n 'travels to downstream servers as clientVersion. The version is opaque — a git SHA, a ' +\n 'semver tag, a CI build number, whatever identifies your build.'\n );\n }\n}\n"]}
1
+ {"version":3,"file":"ServiceInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ServiceInfo.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAa,WAAW;IACpB,gEAAgE;IACxD,MAAM,CAAC,OAAO,CAAqB;IAE3C,+FAA+F;IACvF,MAAM,CAAC,UAAU,CAAqB;IAE9C;;;;;;;;;;OAUG;IACH,4JAA4J;IAC5J,MAAM,CAAC,OAAO,CAAC,IAAY,EAAE,OAAe;QACxC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACX,qFAAqF;gBACrF,qFAAqF;gBACrF,2BAA2B,CAC9B,CAAC;QACN,CAAC;QACD,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,WAAW,CAAC,UAAU,GAAG,OAAO,CAAC;IACrC,CAAC;IAED;;;;;;OAMG;IACH,4JAA4J;IAC5J,MAAM,CAAC,OAAO;QACV,OAAO,WAAW,CAAC,OAAO,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,4JAA4J;IAC5J,MAAM,CAAC,UAAU;QACb,OAAO,WAAW,CAAC,UAAU,CAAC;IAClC,CAAC;IAED,iEAAiE;IACjE,4JAA4J;IAC5J,MAAM,CAAC,KAAK;QACR,WAAW,CAAC,OAAO,GAAG,SAAS,CAAC;QAChC,WAAW,CAAC,UAAU,GAAG,SAAS,CAAC;IACvC,CAAC;CACJ;AA5DD,kCA4DC","sourcesContent":["/**\n * ServiceInfo - the ONE answer to \"what service am I, and which build of it?\", for every part of\n * webpieces that needs it.\n *\n * Configured like {@link HeaderRegistry} / {@link ClientRegistry} / LogManager — populated once at\n * startup, then globally accessible with NO DI wiring. Browser-safe (no `process.env`, no node-only\n * deps), which is why it lives in core-util beside {@link ClientRegistry}.\n *\n * ```ts\n * // startup, FIRST:\n * ServiceInfo.setInfo('my-service', '2.1.0');\n * ```\n *\n * WHY THIS EXISTS. Both facts used to be artifacts of WHICH LOGGING LIBRARY YOU PICKED rather than\n * facts about the service:\n * - the name lived on `BunyanFactoryOptions`, because bunyan REQUIRES a root-logger name\n * (`options.name (string) is required` — bunyan's own TypeError). winston has no such concept, so\n * a winston app shipped its logs UNNAMED.\n * - the version lived on `WinstonFactoryOptions` as `svcGitHash`, so a bunyan app could not stamp a\n * version AT ALL — and the name presumed a git SHA, which not every project deploys from.\n *\n * Switching backends silently changed which fields your logs carried. Several unrelated readers\n * need these same two facts, so they belong to the framework, not to a backend:\n * - the winston backend, to stamp `svcName` + `version`;\n * - the bunyan backend, to stamp `version` (its root-logger `name` is a fixed constant now);\n * - {@link WebpiecesCoreHeaders.REQUEST_ID_SOURCE}, to record WHO minted a request id;\n * - {@link WebpiecesCoreHeaders.CLIENT_VERSION}, so a downstream server can log which build called it.\n *\n * VERSION IS OPAQUE. It is whatever string identifies THIS build — a git SHA, a semver tag, a CI\n * build number. webpieces neither parses nor derives it; the app decides where it comes from (a\n * generated file, an env var, a Docker build arg) and passes it here.\n *\n * DOES NOT THROW ON READ. {@link getName} / {@link getVersion} return `undefined` when unset, so a\n * log line emitted before `setInfo` (early boot) still ships — it simply omits the version. Logging\n * never blocks on identity. The \"a deployed build MUST identify itself\" guarantee is enforced ONCE,\n * loudly, at startup by whoever calls {@link setInfo} — `setupRuntime` takes name+version as REQUIRED\n * inputs and calls it, so a blank value throws and kills the deploy — instead of every reader throwing.\n */\nexport class ServiceInfo {\n /** This service's name. Process-global; set once at startup. */\n private static svcName: string | undefined;\n\n /** This build's version — an opaque app-chosen string. Process-global; set once at startup. */\n private static svcVersion: string | undefined;\n\n /**\n * Identify this service. Call it at startup.\n *\n * LAST CALL WINS, deliberately. A real deployment identifies itself once, but an in-process test\n * can legitimately boot TWO services back-to-back (see the app-example e2e two-server flow), so a\n * \"one process = one service\" rule would reject a case that genuinely exists.\n *\n * @param name - this service's name, e.g. 'my-service'.\n * @param version - the opaque identifier of THIS build (git SHA, semver, CI build number).\n * @throws Error on a blank name or version — that is always a bug, never a use case.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static setInfo(name: string, version: string): void {\n if (!name || !name.trim()) {\n throw new Error('ServiceInfo.setInfo(...) requires a non-blank service name.');\n }\n if (!version || !version.trim()) {\n throw new Error(\n 'ServiceInfo.setInfo(...) requires a non-blank version. It is opaque to webpieces — ' +\n 'a git SHA, a semver tag, a CI build number — but every deployed build must be able ' +\n 'to say which build it is.',\n );\n }\n ServiceInfo.svcName = name;\n ServiceInfo.svcVersion = version;\n }\n\n /**\n * This service's name, or `undefined` when {@link setInfo} has not been called. Does NOT throw:\n * readers (logging backends, requestIdSource, outbound CLIENT_VERSION) simply omit the field when\n * it is missing, so a pre-`setInfo` log line still emits. Identity is REQUIRED where it matters —\n * `setupRuntime` takes name+version and calls {@link setInfo} at startup, so a real server is\n * always identified before it serves traffic.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static getName(): string | undefined {\n return ServiceInfo.svcName;\n }\n\n /**\n * This build's version, or `undefined` when unset — same non-throwing contract as {@link getName}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static getVersion(): string | undefined {\n return ServiceInfo.svcVersion;\n }\n\n /** Reset — for tests, mirroring {@link ClientRegistry.clear}. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static clear(): void {\n ServiceInfo.svcName = undefined;\n ServiceInfo.svcVersion = undefined;\n }\n}\n"]}
@@ -0,0 +1,35 @@
1
+ import { ApiMethodInfo } from './ApiMethodInfo';
2
+ import { FailureClassifier } from './FailureClassifier';
3
+ /**
4
+ * The webpieces built-in {@link FailureClassifier} — the terminal tier, used when no app default and
5
+ * no per-apiClass classifier claims an error. It is the SINGLE SOURCE OF TRUTH for the historical
6
+ * `LogApiCall.isUserError` behavior (which now delegates here), so the classification lives in one
7
+ * place.
8
+ *
9
+ * The question is "are things WORKING?", NOT "was it an HTTP 4xx vs 5xx" — the two differ (see 408).
10
+ * Classification is by portable Error TYPE, never by transport: this runs over in-process calls,
11
+ * pubsub/queue handlers, and HTTP through one code path and only ever sees a thrown Error. The Http*
12
+ * classes are portable error types that travel with the throw anywhere, so matching the TYPE works
13
+ * with or without any HTTP in the picture.
14
+ *
15
+ * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
16
+ * - HttpBadRequestError (400), HttpUnauthorizedError (401), HttpForbiddenError (403),
17
+ * HttpNotFoundError (404) → the server is fine, the caller erred → NON-failure.
18
+ * SERVER — something may actually be WRONG, so SURFACE it (failure):
19
+ * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
20
+ * absent below, so it counts as a failure. 500/502/504/598 and any non-Http Error: real failures.
21
+ *
22
+ * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake".
23
+ * CLIENT: receiving ANY error except 266 means the outbound call FAILED → failure.
24
+ */
25
+ export declare class WebpiecesDefaultFailureClassifier implements FailureClassifier {
26
+ /**
27
+ * Terminal tier — NEVER returns undefined (a definitive verdict is required so classification
28
+ * always resolves to a boolean).
29
+ *
30
+ * @returns true = failure, false = expected/non-failure
31
+ */
32
+ isFailure(error: Error, methodInfo: ApiMethodInfo): boolean;
33
+ }
34
+ /** Process-wide built-in instance — stateless, so one shared instance is enough. */
35
+ export declare const WEBPIECES_DEFAULT_FAILURE_CLASSIFIER: WebpiecesDefaultFailureClassifier;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER = exports.WebpiecesDefaultFailureClassifier = void 0;
4
+ const errors_1 = require("./errors");
5
+ /**
6
+ * The webpieces built-in {@link FailureClassifier} — the terminal tier, used when no app default and
7
+ * no per-apiClass classifier claims an error. It is the SINGLE SOURCE OF TRUTH for the historical
8
+ * `LogApiCall.isUserError` behavior (which now delegates here), so the classification lives in one
9
+ * place.
10
+ *
11
+ * The question is "are things WORKING?", NOT "was it an HTTP 4xx vs 5xx" — the two differ (see 408).
12
+ * Classification is by portable Error TYPE, never by transport: this runs over in-process calls,
13
+ * pubsub/queue handlers, and HTTP through one code path and only ever sees a thrown Error. The Http*
14
+ * classes are portable error types that travel with the throw anywhere, so matching the TYPE works
15
+ * with or without any HTTP in the picture.
16
+ *
17
+ * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:
18
+ * - HttpBadRequestError (400), HttpUnauthorizedError (401), HttpForbiddenError (403),
19
+ * HttpNotFoundError (404) → the server is fine, the caller erred → NON-failure.
20
+ * SERVER — something may actually be WRONG, so SURFACE it (failure):
21
+ * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately
22
+ * absent below, so it counts as a failure. 500/502/504/598 and any non-Http Error: real failures.
23
+ *
24
+ * HttpUserError (266): ALWAYS a non-failure, server OR client — an expected "user made a mistake".
25
+ * CLIENT: receiving ANY error except 266 means the outbound call FAILED → failure.
26
+ */
27
+ class WebpiecesDefaultFailureClassifier {
28
+ /**
29
+ * Terminal tier — NEVER returns undefined (a definitive verdict is required so classification
30
+ * always resolves to a boolean).
31
+ *
32
+ * @returns true = failure, false = expected/non-failure
33
+ */
34
+ isFailure(error, methodInfo) {
35
+ // 266 is the one error that is never a failure, on either side.
36
+ if (error instanceof errors_1.HttpUserError) {
37
+ return false;
38
+ }
39
+ // A client that RECEIVED any error (except the 266 above) made a failed call.
40
+ if (methodInfo.side !== 'server') {
41
+ return true;
42
+ }
43
+ // SERVER: only a healthy rejection of the caller's mistake is a non-failure. Note 408
44
+ // (HttpTimeoutError) is intentionally NOT here — the client may never have seen the response.
45
+ const healthyRejection = error instanceof errors_1.HttpBadRequestError ||
46
+ error instanceof errors_1.HttpUnauthorizedError ||
47
+ error instanceof errors_1.HttpForbiddenError ||
48
+ error instanceof errors_1.HttpNotFoundError;
49
+ return !healthyRejection;
50
+ }
51
+ }
52
+ exports.WebpiecesDefaultFailureClassifier = WebpiecesDefaultFailureClassifier;
53
+ /** Process-wide built-in instance — stateless, so one shared instance is enough. */
54
+ exports.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER = new WebpiecesDefaultFailureClassifier();
55
+ //# sourceMappingURL=WebpiecesDefaultFailureClassifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebpiecesDefaultFailureClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultFailureClassifier.ts"],"names":[],"mappings":";;;AAAA,qCAMkB;AAIlB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,iCAAiC;IAC1C;;;;;OAKG;IACH,SAAS,CAAC,KAAY,EAAE,UAAyB;QAC7C,gEAAgE;QAChE,IAAI,KAAK,YAAY,sBAAa,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,8EAA8E;QAC9E,IAAI,UAAU,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,sFAAsF;QACtF,8FAA8F;QAC9F,MAAM,gBAAgB,GAClB,KAAK,YAAY,4BAAmB;YACpC,KAAK,YAAY,8BAAqB;YACtC,KAAK,YAAY,2BAAkB;YACnC,KAAK,YAAY,0BAAiB,CAAC;QACvC,OAAO,CAAC,gBAAgB,CAAC;IAC7B,CAAC;CACJ;AAzBD,8EAyBC;AAED,oFAAoF;AACvE,QAAA,oCAAoC,GAAG,IAAI,iCAAiC,EAAE,CAAC","sourcesContent":["import {\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpNotFoundError,\n HttpUserError,\n} from './errors';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { FailureClassifier } from './FailureClassifier';\n\n/**\n * The webpieces built-in {@link FailureClassifier} — the terminal tier, used when no app default and\n * no per-apiClass classifier claims an error. It is the SINGLE SOURCE OF TRUTH for the historical\n * `LogApiCall.isUserError` behavior (which now delegates here), so the classification lives in one\n * place.\n *\n * The question is \"are things WORKING?\", NOT \"was it an HTTP 4xx vs 5xx\" — the two differ (see 408).\n * Classification is by portable Error TYPE, never by transport: this runs over in-process calls,\n * pubsub/queue handlers, and HTTP through one code path and only ever sees a thrown Error. The Http*\n * classes are portable error types that travel with the throw anywhere, so matching the TYPE works\n * with or without any HTTP in the picture.\n *\n * SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:\n * - HttpBadRequestError (400), HttpUnauthorizedError (401), HttpForbiddenError (403),\n * HttpNotFoundError (404) → the server is fine, the caller erred → NON-failure.\n * SERVER — something may actually be WRONG, so SURFACE it (failure):\n * - HttpTimeoutError (408): a 4xx, but the client may NEVER have seen the response — deliberately\n * absent below, so it counts as a failure. 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\".\n * CLIENT: receiving ANY error except 266 means the outbound call FAILED → failure.\n */\nexport class WebpiecesDefaultFailureClassifier implements FailureClassifier {\n /**\n * Terminal tier — NEVER returns undefined (a definitive verdict is required so classification\n * always resolves to a boolean).\n *\n * @returns true = failure, false = expected/non-failure\n */\n isFailure(error: Error, methodInfo: ApiMethodInfo): boolean {\n // 266 is the one error that is never a failure, on either side.\n if (error instanceof HttpUserError) {\n return false;\n }\n // A client that RECEIVED any error (except the 266 above) made a failed call.\n if (methodInfo.side !== 'server') {\n return true;\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 const healthyRejection =\n error instanceof HttpBadRequestError ||\n error instanceof HttpUnauthorizedError ||\n error instanceof HttpForbiddenError ||\n error instanceof HttpNotFoundError;\n return !healthyRejection;\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_FAILURE_CLASSIFIER = new WebpiecesDefaultFailureClassifier();\n"]}
package/src/index.d.ts CHANGED
@@ -28,6 +28,9 @@ export type { ServiceUrlDeriver } from './http/ClientRegistry';
28
28
  export { ServiceInfo } from './http/ServiceInfo';
29
29
  export { ErrorWireForm } from './http/ErrorTranslation';
30
30
  export type { ErrorTranslation } from './http/ErrorTranslation';
31
+ export type { FailureClassifier } from './http/FailureClassifier';
32
+ export { KeyedFailureClassifier } from './http/FailureClassifier';
33
+ export { WebpiecesDefaultFailureClassifier, WEBPIECES_DEFAULT_FAILURE_CLASSIFIER, } from './http/WebpiecesDefaultFailureClassifier';
31
34
  export { templateDeriver } from './http/templateDeriver';
32
35
  export { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';
33
36
  export { ContextReader } from './http/ContextReader';
package/src/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.HttpUnauthorizedError = exports.HttpBadRequestError = exports.EndpointNotFoundError = exports.HttpNotFoundError = exports.HttpError = exports.ProtocolError = exports.SECRETS = exports.Secrets = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.validateNoConflictingDecorators = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.isFormPost = exports.getEndpointOptions = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.AuthSharedSecret = exports.AuthOidc = exports.Auth = exports.AuthJwt = exports.Public = exports.AuthenticationConfig = exports.Authentication = exports.Endpoint = exports.ApiPath = exports.GCP_LOG_BUDGET_BYTES = exports.MAX_GCP_LOG_BYTES = exports.LogChunkInfo = exports.LogChunkerImpl = exports.LogChunker = exports.LogManager = exports.ConsoleLoggerFactory = exports.ConsoleLogger = exports.DESIGN_METADATA_KEYS = exports.isDocumentDesign = exports.DocumentDesign = exports.ContextTuple = exports.ContextKey = exports.toError = void 0;
12
- exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.ApiMethodInfo = exports.LOG_API_CALL_LOGGER_NAME = exports.ApiCallLogNameImpl = exports.ApiCallLogName = exports.ApiCallInfo = exports.LogApiCallImpl = exports.LogApiCall = exports.ContextMgr = exports.WebpiecesCoreHeaders = exports.templateDeriver = exports.ErrorWireForm = exports.ServiceInfo = exports.ClientRegistry = exports.HeaderRegistry = exports.DateTimeUtil = exports.TimeUtil = exports.DateUtil = exports.InstantUtil = exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.HttpUserError = exports.HttpVendorError = exports.HttpInternalServerError = exports.HttpGatewayTimeoutError = exports.HttpBadGatewayError = exports.HttpTimeoutError = exports.HttpForbiddenError = void 0;
12
+ exports.SerializedError = exports.SerializedMap = exports.RecordSerializer = exports.getDoNotRecordFields = exports.DoNotRecord = exports.RecordedTestCase = exports.RecordedError = exports.RecordedEndpoint = exports.RecorderKeys = exports.ApiCallContextHolder = exports.ApiMethodInfo = exports.LOG_API_CALL_LOGGER_NAME = exports.ApiCallLogNameImpl = exports.ApiCallLogName = exports.ApiCallInfo = exports.LogApiCallImpl = exports.LogApiCall = exports.ContextMgr = exports.WebpiecesCoreHeaders = exports.templateDeriver = exports.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER = exports.WebpiecesDefaultFailureClassifier = exports.KeyedFailureClassifier = exports.ErrorWireForm = exports.ServiceInfo = exports.ClientRegistry = exports.HeaderRegistry = exports.DateTimeUtil = exports.TimeUtil = exports.DateUtil = exports.InstantUtil = exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.HttpUserError = exports.HttpVendorError = exports.HttpInternalServerError = exports.HttpGatewayTimeoutError = exports.HttpBadGatewayError = exports.HttpTimeoutError = exports.HttpForbiddenError = 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");
@@ -119,6 +119,11 @@ Object.defineProperty(exports, "ServiceInfo", { enumerable: true, get: function
119
119
  // ClientRegistry at startup; consulted before the built-in webpieces mapping on BOTH sides.
120
120
  var ErrorTranslation_1 = require("./http/ErrorTranslation");
121
121
  Object.defineProperty(exports, "ErrorWireForm", { enumerable: true, get: function () { return ErrorTranslation_1.ErrorWireForm; } });
122
+ var FailureClassifier_1 = require("./http/FailureClassifier");
123
+ Object.defineProperty(exports, "KeyedFailureClassifier", { enumerable: true, get: function () { return FailureClassifier_1.KeyedFailureClassifier; } });
124
+ var WebpiecesDefaultFailureClassifier_1 = require("./http/WebpiecesDefaultFailureClassifier");
125
+ Object.defineProperty(exports, "WebpiecesDefaultFailureClassifier", { enumerable: true, get: function () { return WebpiecesDefaultFailureClassifier_1.WebpiecesDefaultFailureClassifier; } });
126
+ Object.defineProperty(exports, "WEBPIECES_DEFAULT_FAILURE_CLASSIFIER", { enumerable: true, get: function () { return WebpiecesDefaultFailureClassifier_1.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER; } });
122
127
  var templateDeriver_1 = require("./http/templateDeriver");
123
128
  Object.defineProperty(exports, "templateDeriver", { enumerable: true, get: function () { return templateDeriver_1.templateDeriver; } });
124
129
  var WebpiecesCoreHeaders_1 = require("./http/WebpiecesCoreHeaders");
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/core/core-util/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @webpieces/core-util\n *\n * Utility functions for WebPieces applications.\n * This package works in both browser and Node.js environments.\n *\n * @packageDocumentation\n */\n\nexport { toError } from './lib/errorUtils';\nexport { ContextKey } from './ContextKey';\nexport { ContextTuple } from './ContextTuple';\n\n// @DocumentDesign — DI-design-root marker. Applies to ANY project kind (server\n// controllers AND library impl classes), so it lives here (browser + Node) rather\n// than in a server-only routing package.\nexport { DocumentDesign, isDocumentDesign, DESIGN_METADATA_KEYS } from './DocumentDesign';\n\n// Logging (merged from former @webpieces/wp-logging).\n// Pluggable logging interface + a browser-safe console default; apps plug in\n// bunyan/winston/pino/etc. via LogManager.setFactory(...). Browser + Node.\nexport type { Logger, LogLevel } from './logging/Logger';\nexport type { LoggerFactory } from './logging/LoggerFactory';\nexport { ConsoleLogger } from './logging/ConsoleLogger';\nexport { ConsoleLoggerFactory } from './logging/ConsoleLoggerFactory';\nexport { LogManager } from './logging/LogManager';\nexport { LogChunker, LogChunkerImpl, LogChunkInfo, MAX_GCP_LOG_BYTES, GCP_LOG_BUDGET_BYTES } from './logging/LogChunker';\n\n// HTTP API contract (merged from former @webpieces/http-api).\n// Shared HTTP API definition consumed by both client and server: REST\n// decorators, the HttpError hierarchy, datetime DTOs, platform-header\n// registry/readers, ValidateImplementation, and the test-case recorder\n// contract. Pure definitions — express-free, browser + Node safe.\n\n// API definition decorators\nexport {\n ApiPath,\n Endpoint,\n Authentication,\n AuthenticationConfig,\n // Auth mode decorators (clean service-to-service + user JWT model)\n Public,\n AuthJwt,\n Auth,\n AuthOidc,\n AuthSharedSecret,\n // API kind (RPC vs PubSub/Cloud Tasks) + queue naming\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n isFormPost,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n validateNoConflictingDecorators,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n} from './http/decorators';\nexport type { AuthMode, ApiKind, JwtRequirement, EndpointOptions } from './http/decorators';\n// Client-side shared-secret store (the value THIS service sends per @AuthSharedSecret key).\nexport { Secrets, SECRETS } from './http/Secrets';\n\n// Type validators\nexport { ValidateImplementation } from './http/validators';\n\n// HTTP errors\nexport {\n ProtocolError,\n HttpError,\n HttpNotFoundError,\n EndpointNotFoundError,\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpTimeoutError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n HttpInternalServerError,\n HttpVendorError,\n HttpUserError,\n // Error subtype constants\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './http/errors';\n\n// Date/Time DTOs and Utilities (inspired by Java Time / JSR-310)\nexport {\n InstantDto,\n DateDto,\n TimeDto,\n DateTimeDto,\n InstantUtil,\n DateUtil,\n TimeUtil,\n DateTimeUtil,\n} from './http/datetime';\n\n// Context keys + registry (the global magic-context header system)\nexport { HeaderRegistry } from './http/HeaderRegistry';\nexport { ClientRegistry } from './http/ClientRegistry';\nexport type { ServiceUrlDeriver } from './http/ClientRegistry';\n\n// \"What service am I\" — set once at startup, read by the logging backends and by\n// RequestContextHeaders (to stamp requestIdSource on ids this service mints).\nexport { ServiceInfo } from './http/ServiceInfo';\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';\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';\n// Console-render bridge: turns LogApiCall's [LogApiCall] bracket into [API.{side}.{phase}] locally.\nexport { ApiCallLogName, ApiCallLogNameImpl, LOG_API_CALL_LOGGER_NAME } from './http/ApiCallLogName';\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"]}
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';\nexport { LogChunker, LogChunkerImpl, LogChunkInfo, MAX_GCP_LOG_BYTES, GCP_LOG_BUDGET_BYTES } from './logging/LogChunker';\n\n// HTTP API contract (merged from former @webpieces/http-api).\n// Shared HTTP API definition consumed by both client and server: REST\n// decorators, the HttpError hierarchy, datetime DTOs, platform-header\n// registry/readers, ValidateImplementation, and the test-case recorder\n// contract. Pure definitions — express-free, browser + Node safe.\n\n// API definition decorators\nexport {\n ApiPath,\n Endpoint,\n Authentication,\n AuthenticationConfig,\n // Auth mode decorators (clean service-to-service + user JWT model)\n Public,\n AuthJwt,\n Auth,\n AuthOidc,\n AuthSharedSecret,\n // API kind (RPC vs PubSub/Cloud Tasks) + queue naming\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n isFormPost,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n validateNoConflictingDecorators,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n} from './http/decorators';\nexport type { AuthMode, ApiKind, JwtRequirement, EndpointOptions } from './http/decorators';\n// Client-side shared-secret store (the value THIS service sends per @AuthSharedSecret key).\nexport { Secrets, SECRETS } from './http/Secrets';\n\n// Type validators\nexport { ValidateImplementation } from './http/validators';\n\n// HTTP errors\nexport {\n ProtocolError,\n HttpError,\n HttpNotFoundError,\n EndpointNotFoundError,\n HttpBadRequestError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpTimeoutError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n HttpInternalServerError,\n HttpVendorError,\n HttpUserError,\n // Error subtype constants\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './http/errors';\n\n// Date/Time DTOs and Utilities (inspired by Java Time / JSR-310)\nexport {\n InstantDto,\n DateDto,\n TimeDto,\n DateTimeDto,\n InstantUtil,\n DateUtil,\n TimeUtil,\n DateTimeUtil,\n} from './http/datetime';\n\n// Context keys + registry (the global magic-context header system)\nexport { HeaderRegistry } from './http/HeaderRegistry';\nexport { ClientRegistry } from './http/ClientRegistry';\nexport type { ServiceUrlDeriver } from './http/ClientRegistry';\n\n// \"What service am I\" — set once at startup, read by the logging backends and by\n// RequestContextHeaders (to stamp requestIdSource on ids this service mints).\nexport { ServiceInfo } from './http/ServiceInfo';\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';\n// Pluggable per-client failure classification (is a thrown API-call error a real failure or an\n// expected non-failure?). Registered on ClientRegistry at startup; consulted by LogApiCall.\nexport type { FailureClassifier } from './http/FailureClassifier';\nexport { KeyedFailureClassifier } from './http/FailureClassifier';\nexport {\n WebpiecesDefaultFailureClassifier,\n WEBPIECES_DEFAULT_FAILURE_CLASSIFIER,\n} from './http/WebpiecesDefaultFailureClassifier';\nexport { templateDeriver } from './http/templateDeriver';\nexport { WebpiecesCoreHeaders } from './http/WebpiecesCoreHeaders';\nexport { ContextReader } 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';\n// Console-render bridge: turns LogApiCall's [LogApiCall] bracket into [API.{side}.{phase}] locally.\nexport { ApiCallLogName, ApiCallLogNameImpl, LOG_API_CALL_LOGGER_NAME } from './http/ApiCallLogName';\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"]}