@webpieces/core-util 0.4.955 → 0.4.957

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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/src/http/ApiMethodInfo.d.ts +14 -0
  3. package/src/http/ApiMethodInfo.js +3 -0
  4. package/src/http/ApiMethodInfo.js.map +1 -1
  5. package/src/http/CallRegistry.d.ts +5 -10
  6. package/src/http/CallRegistry.js +21 -43
  7. package/src/http/CallRegistry.js.map +1 -1
  8. package/src/http/CallRegistryCompileAssertions.js +1 -1
  9. package/src/http/CallRegistryCompileAssertions.js.map +1 -1
  10. package/src/http/CallScope.d.ts +33 -0
  11. package/src/http/CallScope.js +70 -0
  12. package/src/http/CallScope.js.map +1 -0
  13. package/src/http/CallStrategy.d.ts +13 -1
  14. package/src/http/CallStrategy.js +4 -1
  15. package/src/http/CallStrategy.js.map +1 -1
  16. package/src/http/ClientRegistry.d.ts +54 -58
  17. package/src/http/ClientRegistry.js +72 -73
  18. package/src/http/ClientRegistry.js.map +1 -1
  19. package/src/http/ErrorClassifier.d.ts +20 -27
  20. package/src/http/ErrorClassifier.js +1 -15
  21. package/src/http/ErrorClassifier.js.map +1 -1
  22. package/src/http/ErrorLadderCompileAssertions.d.ts +1 -0
  23. package/src/http/ErrorLadderCompileAssertions.js +42 -0
  24. package/src/http/ErrorLadderCompileAssertions.js.map +1 -0
  25. package/src/http/ErrorTranslator.d.ts +8 -4
  26. package/src/http/ErrorTranslator.js.map +1 -1
  27. package/src/http/LogApiCall.js +2 -2
  28. package/src/http/LogApiCall.js.map +1 -1
  29. package/src/http/RetryStrategy.js +1 -1
  30. package/src/http/RetryStrategy.js.map +1 -1
  31. package/src/http/StreamErrorControl.d.ts +9 -1
  32. package/src/http/StreamErrorControl.js +14 -3
  33. package/src/http/StreamErrorControl.js.map +1 -1
  34. package/src/http/WebpiecesDefaultErrorClassifier.d.ts +2 -2
  35. package/src/http/WebpiecesDefaultErrorClassifier.js +2 -2
  36. package/src/http/WebpiecesDefaultErrorClassifier.js.map +1 -1
  37. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +2 -1
  38. package/src/http/WebpiecesDefaultErrorTranslator.js +2 -1
  39. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
  40. package/src/index.d.ts +3 -2
  41. package/src/index.js +3 -2
  42. package/src/index.js.map +1 -1
  43. package/src/ipc/IpcConnection.js +1 -1
  44. package/src/ipc/IpcConnection.js.map +1 -1
  45. package/src/ipc/IpcLogging.js +1 -1
  46. package/src/ipc/IpcLogging.js.map +1 -1
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ClientRegistry_1 = require("./ClientRegistry");
4
+ const ApiMethodInfo_1 = require("./ApiMethodInfo");
5
+ const CallStrategy_1 = require("./CallStrategy");
6
+ const HttpEndpointOptions_1 = require("./HttpEndpointOptions");
7
+ /**
8
+ * Compiled by tsc (spec files are not), never executed. Pins the ONE spelling of the error ladder:
9
+ * every registration states its `CallScope`, and the unscoped spellings it replaced do not compile.
10
+ * tsc fails the build with TS2578 if any of these ever starts compiling again.
11
+ */
12
+ class ErrorLadderCompileAssertions {
13
+ scopeIsRequired(translator, classifier) {
14
+ // @ts-expect-error setErrorTranslator(t) became setErrorTranslator(t, 'ALL') — the scope is required
15
+ ClientRegistry_1.ClientRegistry.setErrorTranslator(translator);
16
+ // @ts-expect-error setDefaultErrorClassifier(c) became setErrorClassifier(c, 'ALL')
17
+ ClientRegistry_1.ClientRegistry.setDefaultErrorClassifier(classifier);
18
+ // @ts-expect-error addErrorClassifier(new KeyedErrorClassifier('XApi', c)) became setErrorClassifier(c, XApi)
19
+ ClientRegistry_1.ClientRegistry.addErrorClassifier(classifier);
20
+ // @ts-expect-error the scope is required — setErrorClassifier(c, 'ALL')
21
+ ClientRegistry_1.ClientRegistry.setErrorClassifier(classifier);
22
+ // @ts-expect-error getErrorTranslator() became getErrorTranslator(api, methodName)
23
+ ClientRegistry_1.ClientRegistry.getErrorTranslator();
24
+ }
25
+ allHasNoMethod(translator, classifier) {
26
+ // @ts-expect-error ALL cannot select a method
27
+ ClientRegistry_1.ClientRegistry.setErrorTranslator(translator, 'ALL', 'work');
28
+ // @ts-expect-error ALL cannot select a method
29
+ ClientRegistry_1.ClientRegistry.setErrorClassifier(classifier, 'ALL', 'work');
30
+ }
31
+ callIdentityStatesItsContract() {
32
+ // @ts-expect-error ApiMethodInfo's contract CLASS is required (pass undefined only when there is none)
33
+ void new ApiMethodInfo_1.ApiMethodInfo('client', 'SaveApi', 'save');
34
+ // @ts-expect-error CallContext's contract CLASS is required (pass undefined only when there is none)
35
+ void new CallStrategy_1.CallContext('SaveApi', 'save', HttpEndpointOptions_1.WRITE);
36
+ }
37
+ keyedByClassNeverByName(classifier) {
38
+ // @ts-expect-error the ladder is keyed by the contract CLASS, never its name
39
+ ClientRegistry_1.ClientRegistry.setErrorClassifier(classifier, 'SaveApi');
40
+ }
41
+ }
42
+ //# sourceMappingURL=ErrorLadderCompileAssertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ErrorLadderCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorLadderCompileAssertions.ts"],"names":[],"mappings":";;AAAA,qDAAkD;AAGlD,mDAAgD;AAChD,iDAA6C;AAC7C,+DAA8C;AAE9C;;;;GAIG;AACH,MAAM,4BAA4B;IAC9B,eAAe,CAAC,UAA2B,EAAE,UAA2B;QACpE,qGAAqG;QACrG,+BAAc,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC9C,oFAAoF;QACpF,+BAAc,CAAC,yBAAyB,CAAC,UAAU,CAAC,CAAC;QACrD,8GAA8G;QAC9G,+BAAc,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC9C,wEAAwE;QACxE,+BAAc,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC9C,mFAAmF;QACnF,+BAAc,CAAC,kBAAkB,EAAE,CAAC;IACxC,CAAC;IAED,cAAc,CAAC,UAA2B,EAAE,UAA2B;QACnE,8CAA8C;QAC9C,+BAAc,CAAC,kBAAkB,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QAC7D,8CAA8C;QAC9C,+BAAc,CAAC,kBAAkB,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACjE,CAAC;IAED,6BAA6B;QACzB,uGAAuG;QACvG,KAAK,IAAI,6BAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QACpD,qGAAqG;QACrG,KAAK,IAAI,0BAAW,CAAC,SAAS,EAAE,MAAM,EAAE,2BAAK,CAAC,CAAC;IACnD,CAAC;IAED,uBAAuB,CAAC,UAA2B;QAC/C,6EAA6E;QAC7E,+BAAc,CAAC,kBAAkB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC7D,CAAC;CACJ","sourcesContent":["import { ClientRegistry } from './ClientRegistry';\nimport { ErrorClassifier } from './ErrorClassifier';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { CallContext } from './CallStrategy';\nimport { WRITE } from './HttpEndpointOptions';\n\n/**\n * Compiled by tsc (spec files are not), never executed. Pins the ONE spelling of the error ladder:\n * every registration states its `CallScope`, and the unscoped spellings it replaced do not compile.\n * tsc fails the build with TS2578 if any of these ever starts compiling again.\n */\nclass ErrorLadderCompileAssertions {\n scopeIsRequired(translator: ErrorTranslator, classifier: ErrorClassifier): void {\n // @ts-expect-error setErrorTranslator(t) became setErrorTranslator(t, 'ALL') — the scope is required\n ClientRegistry.setErrorTranslator(translator);\n // @ts-expect-error setDefaultErrorClassifier(c) became setErrorClassifier(c, 'ALL')\n ClientRegistry.setDefaultErrorClassifier(classifier);\n // @ts-expect-error addErrorClassifier(new KeyedErrorClassifier('XApi', c)) became setErrorClassifier(c, XApi)\n ClientRegistry.addErrorClassifier(classifier);\n // @ts-expect-error the scope is required — setErrorClassifier(c, 'ALL')\n ClientRegistry.setErrorClassifier(classifier);\n // @ts-expect-error getErrorTranslator() became getErrorTranslator(api, methodName)\n ClientRegistry.getErrorTranslator();\n }\n\n allHasNoMethod(translator: ErrorTranslator, classifier: ErrorClassifier): void {\n // @ts-expect-error ALL cannot select a method\n ClientRegistry.setErrorTranslator(translator, 'ALL', 'work');\n // @ts-expect-error ALL cannot select a method\n ClientRegistry.setErrorClassifier(classifier, 'ALL', 'work');\n }\n\n callIdentityStatesItsContract(): void {\n // @ts-expect-error ApiMethodInfo's contract CLASS is required (pass undefined only when there is none)\n void new ApiMethodInfo('client', 'SaveApi', 'save');\n // @ts-expect-error CallContext's contract CLASS is required (pass undefined only when there is none)\n void new CallContext('SaveApi', 'save', WRITE);\n }\n\n keyedByClassNeverByName(classifier: ErrorClassifier): void {\n // @ts-expect-error the ladder is keyed by the contract CLASS, never its name\n ClientRegistry.setErrorClassifier(classifier, 'SaveApi');\n }\n}\n"]}
@@ -4,8 +4,12 @@ import { HttpResponseDto } from './HttpResponseDto';
4
4
  * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over
5
5
  * the WHOLE response.
6
6
  *
7
- * An app implements this ONCE and registers it ONCE per process via
8
- * {@link ClientRegistry.setErrorTranslator} — on the server AND in the browser. Its `toWire` runs on
7
+ * An app implements this ONCE and registers it per process via
8
+ * `ClientRegistry.setErrorTranslator(t, 'ALL')` — on the server AND in the browser. A contract whose
9
+ * errors travel in a DIFFERENT wire format (a vendor's own 4xx body, or the error body an inbound
10
+ * vendor expects back) gets its own translator on the same `CallScope` ladder as `CallRegistry`'s
11
+ * strategy and timeout: `setErrorTranslator(t, VendorApi)` or `setErrorTranslator(t, VendorApi,
12
+ * 'method')`, resolved method -> API -> ALL -> webpieces default. Its `toWire` runs on
9
13
  * the SERVER (`ExpressWrapper.handleError`) and its `fromWire` runs on every CLIENT in that process
10
14
  * (`ClientErrorTranslator.throwIfFailure`, shared by `http-client-node` and `http-client-browser`).
11
15
  * The payoff is type symmetry across the wire: the server throws `OrderNotFoundError` and the caller
@@ -13,7 +17,7 @@ import { HttpResponseDto } from './HttpResponseDto';
13
17
  *
14
18
  * # THERE IS ALWAYS A TRANSLATOR
15
19
  *
16
- * {@link ClientRegistry.getErrorTranslator} is non-optional and starts life holding
20
+ * {@link ClientRegistry.getErrorTranslator} is non-optional: below every rung of the ladder sits
17
21
  * {@link WebpiecesDefaultErrorTranslator}. So no caller anywhere asks "did anyone register one" —
18
22
  * every call site is ONE unconditional line. A registered app translator REPLACES the default and
19
23
  * declines by DELEGATING to it:
@@ -89,7 +93,7 @@ import { HttpResponseDto } from './HttpResponseDto';
89
93
  * }
90
94
  *
91
95
  * // startup, ONCE per process (server AND browser)
92
- * ClientRegistry.setErrorTranslator(new OrderErrorTranslator());
96
+ * ClientRegistry.setErrorTranslator(new OrderErrorTranslator(), 'ALL');
93
97
  * ```
94
98
  */
95
99
  export interface ErrorTranslator {
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorTranslator.ts"],"names":[],"mappings":"","sourcesContent":["import { ClientRole } from '../errors/ClientRole';\nimport { HttpResponseDto } from './HttpResponseDto';\n\n/**\n * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over\n * the WHOLE response.\n *\n * An app implements this ONCE and registers it ONCE per process via\n * {@link ClientRegistry.setErrorTranslator} — on the server AND in the browser. Its `toWire` runs on\n * the SERVER (`ExpressWrapper.handleError`) and its `fromWire` runs on every CLIENT in that process\n * (`ClientErrorTranslator.throwIfFailure`, shared by `http-client-node` and `http-client-browser`).\n * The payoff is type symmetry across the wire: the server throws `OrderNotFoundError` and the caller\n * CATCHES `OrderNotFoundError`, instead of decoding a status code by hand at every call site.\n *\n * # THERE IS ALWAYS A TRANSLATOR\n *\n * {@link ClientRegistry.getErrorTranslator} is non-optional and starts life holding\n * {@link WebpiecesDefaultErrorTranslator}. So no caller anywhere asks \"did anyone register one\" —\n * every call site is ONE unconditional line. A registered app translator REPLACES the default and\n * declines by DELEGATING to it:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * That is one call, it is visible at the call site, and it composes: an app that wants to DECORATE\n * the default rather than replace it does so by calling it and editing the result. A `next` parameter\n * was considered and rejected — it buys nothing this does not, and it puts the framework back in the\n * business of sequencing somebody else's precedence.\n *\n * # Both halves speak {@link HttpResponseDto}, and that is the point\n *\n * `toWire` PRODUCES exactly what `fromWire` CONSUMES. Reading the two methods against each other in\n * one file is what makes a mistake visible, which is why this is one object rather than two\n * separately-registered functions.\n *\n * # The whole response, not a status plus a body\n *\n * The previous contract could express only `(statusCode, protocolError)`, so an app could not put its\n * own trace id, `Retry-After`, `WWW-Authenticate` or a cookie on an error response. A translator now\n * returns the status code, the reason phrase, the header LIST and the body — everything.\n *\n * # `toWire` RETURNS a response; `fromWire` THROWS\n *\n * They are mirrors, not twins. `toWire` is called inside a catch and its whole job is to produce a\n * value, so it returns one. `fromWire` is called on a response the caller is about to be handed, and\n * its whole job is to stop that from happening, so it THROWS and is typed `void`. Returning an\n * `Error` the caller then has to remember to throw was the previous shape and it is exactly the bug\n * class this deletes: a `void` return cannot be silently dropped.\n *\n * There is exactly ONE registered ErrorTranslator per process (a `set`, not an `add` to a list):\n * precedence between an app's own layers is the APP's to compose explicitly inside its `toWire`,\n * rather than something hidden in registration order inside the framework.\n *\n * # Not covered: 404 / unknown route\n *\n * A request that matches no route has no route context and never reaches this seam. That is\n * deliberate and out of scope.\n *\n * This is a business-logic contract (methods, not data), so it is an interface per the webpieces\n * guidelines.\n *\n * ```ts\n * class OrderErrorPayload {\n * constructor(public message: string, public errorCode: string) {}\n * }\n *\n * export class OrderErrorTranslator implements ErrorTranslator {\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n *\n * toWire(error: Error): HttpResponseDto {\n * if (!(error instanceof OrderNotFoundError)) {\n * return this.fallback.toWire(error); // not mine -> webpieces default\n * }\n * const body = new OrderErrorPayload(error.message, 'ORDER_NOT_FOUND');\n * return new HttpResponseDto(\n * new HttpResponseStatus(460, 'Order Not Found'),\n * [new HttpHeader('x-order-trace', error.traceId)],\n * body,\n * );\n * }\n *\n * fromWire(response: HttpResponseDto, role: ClientRole): void {\n * if (response.status.code !== 460) {\n * this.fallback.fromWire(response, role); // not mine -> webpieces default\n * return;\n * }\n * throw new OrderNotFoundError(String(response.body));\n * }\n * }\n *\n * // startup, ONCE per process (server AND browser)\n * ClientRegistry.setErrorTranslator(new OrderErrorTranslator());\n * ```\n */\nexport interface ErrorTranslator {\n /**\n * SERVER: exception -> the ENTIRE response (status code, reason phrase, headers, body). Always a\n * response, never `undefined`; decline by returning\n * `new WebpiecesDefaultErrorTranslator().toWire(error)`.\n */\n toWire(error: Error): HttpResponseDto;\n\n /**\n * CLIENT: the ENTIRE response -> a THROW. Receives the SAME shape `toWire` produces, normalised\n * from whichever transport read it.\n *\n * Called for EVERY response, including a 2xx — deliberately, so an app whose 200 body signals\n * failure can turn it into a throw. Returning normally means \"let this response through\"; the\n * webpieces default returns normally for any 2xx that is not 266 and throws otherwise. Decline by\n * calling `new WebpiecesDefaultErrorTranslator().fromWire(response, role)`.\n *\n * `role` is WHO is receiving ({@link ClientRole}): the end-user client or a server. The client\n * that read the response passes the role ITS setup declared (`ClientConfig`), so one translator\n * registered in a process serves every client there. It decides what a received 401 means; pass\n * it through unchanged when declining.\n *\n * A non-2xx response can never reach a typed caller even if an app translator forgets to throw:\n * `ClientErrorTranslator.throwIfFailure` applies the webpieces default behind it. That is bug\n * containment, not a \"was one registered\" branch.\n */\n fromWire(response: HttpResponseDto, role: ClientRole): void;\n}\n"]}
1
+ {"version":3,"file":"ErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorTranslator.ts"],"names":[],"mappings":"","sourcesContent":["import { ClientRole } from '../errors/ClientRole';\nimport { HttpResponseDto } from './HttpResponseDto';\n\n/**\n * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over\n * the WHOLE response.\n *\n * An app implements this ONCE and registers it per process via\n * `ClientRegistry.setErrorTranslator(t, 'ALL')` — on the server AND in the browser. A contract whose\n * errors travel in a DIFFERENT wire format (a vendor's own 4xx body, or the error body an inbound\n * vendor expects back) gets its own translator on the same `CallScope` ladder as `CallRegistry`'s\n * strategy and timeout: `setErrorTranslator(t, VendorApi)` or `setErrorTranslator(t, VendorApi,\n * 'method')`, resolved method -> API -> ALL -> webpieces default. Its `toWire` runs on\n * the SERVER (`ExpressWrapper.handleError`) and its `fromWire` runs on every CLIENT in that process\n * (`ClientErrorTranslator.throwIfFailure`, shared by `http-client-node` and `http-client-browser`).\n * The payoff is type symmetry across the wire: the server throws `OrderNotFoundError` and the caller\n * CATCHES `OrderNotFoundError`, instead of decoding a status code by hand at every call site.\n *\n * # THERE IS ALWAYS A TRANSLATOR\n *\n * {@link ClientRegistry.getErrorTranslator} is non-optional: below every rung of the ladder sits\n * {@link WebpiecesDefaultErrorTranslator}. So no caller anywhere asks \"did anyone register one\" —\n * every call site is ONE unconditional line. A registered app translator REPLACES the default and\n * declines by DELEGATING to it:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * That is one call, it is visible at the call site, and it composes: an app that wants to DECORATE\n * the default rather than replace it does so by calling it and editing the result. A `next` parameter\n * was considered and rejected — it buys nothing this does not, and it puts the framework back in the\n * business of sequencing somebody else's precedence.\n *\n * # Both halves speak {@link HttpResponseDto}, and that is the point\n *\n * `toWire` PRODUCES exactly what `fromWire` CONSUMES. Reading the two methods against each other in\n * one file is what makes a mistake visible, which is why this is one object rather than two\n * separately-registered functions.\n *\n * # The whole response, not a status plus a body\n *\n * The previous contract could express only `(statusCode, protocolError)`, so an app could not put its\n * own trace id, `Retry-After`, `WWW-Authenticate` or a cookie on an error response. A translator now\n * returns the status code, the reason phrase, the header LIST and the body — everything.\n *\n * # `toWire` RETURNS a response; `fromWire` THROWS\n *\n * They are mirrors, not twins. `toWire` is called inside a catch and its whole job is to produce a\n * value, so it returns one. `fromWire` is called on a response the caller is about to be handed, and\n * its whole job is to stop that from happening, so it THROWS and is typed `void`. Returning an\n * `Error` the caller then has to remember to throw was the previous shape and it is exactly the bug\n * class this deletes: a `void` return cannot be silently dropped.\n *\n * There is exactly ONE registered ErrorTranslator per process (a `set`, not an `add` to a list):\n * precedence between an app's own layers is the APP's to compose explicitly inside its `toWire`,\n * rather than something hidden in registration order inside the framework.\n *\n * # Not covered: 404 / unknown route\n *\n * A request that matches no route has no route context and never reaches this seam. That is\n * deliberate and out of scope.\n *\n * This is a business-logic contract (methods, not data), so it is an interface per the webpieces\n * guidelines.\n *\n * ```ts\n * class OrderErrorPayload {\n * constructor(public message: string, public errorCode: string) {}\n * }\n *\n * export class OrderErrorTranslator implements ErrorTranslator {\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n *\n * toWire(error: Error): HttpResponseDto {\n * if (!(error instanceof OrderNotFoundError)) {\n * return this.fallback.toWire(error); // not mine -> webpieces default\n * }\n * const body = new OrderErrorPayload(error.message, 'ORDER_NOT_FOUND');\n * return new HttpResponseDto(\n * new HttpResponseStatus(460, 'Order Not Found'),\n * [new HttpHeader('x-order-trace', error.traceId)],\n * body,\n * );\n * }\n *\n * fromWire(response: HttpResponseDto, role: ClientRole): void {\n * if (response.status.code !== 460) {\n * this.fallback.fromWire(response, role); // not mine -> webpieces default\n * return;\n * }\n * throw new OrderNotFoundError(String(response.body));\n * }\n * }\n *\n * // startup, ONCE per process (server AND browser)\n * ClientRegistry.setErrorTranslator(new OrderErrorTranslator(), 'ALL');\n * ```\n */\nexport interface ErrorTranslator {\n /**\n * SERVER: exception -> the ENTIRE response (status code, reason phrase, headers, body). Always a\n * response, never `undefined`; decline by returning\n * `new WebpiecesDefaultErrorTranslator().toWire(error)`.\n */\n toWire(error: Error): HttpResponseDto;\n\n /**\n * CLIENT: the ENTIRE response -> a THROW. Receives the SAME shape `toWire` produces, normalised\n * from whichever transport read it.\n *\n * Called for EVERY response, including a 2xx — deliberately, so an app whose 200 body signals\n * failure can turn it into a throw. Returning normally means \"let this response through\"; the\n * webpieces default returns normally for any 2xx that is not 266 and throws otherwise. Decline by\n * calling `new WebpiecesDefaultErrorTranslator().fromWire(response, role)`.\n *\n * `role` is WHO is receiving ({@link ClientRole}): the end-user client or a server. The client\n * that read the response passes the role ITS setup declared (`ClientConfig`), so one translator\n * registered in a process serves every client there. It decides what a received 401 means; pass\n * it through unchanged when declining.\n *\n * A non-2xx response can never reach a typed caller even if an app translator forgets to throw:\n * `ClientErrorTranslator.throwIfFailure` applies the webpieces default behind it. That is bug\n * containment, not a \"was one registered\" branch.\n */\n fromWire(response: HttpResponseDto, role: ClientRole): void;\n}\n"]}
@@ -345,8 +345,8 @@ class LogApiCallImpl {
345
345
  // per failure now that `ApiErrorBoundary` writes none. An operator error message is never
346
346
  // published to a caller (see `ApiErrorCodec.publicMessage`), so the log is the one place it
347
347
  // exists at all — dropping it on the caller-error branch would lose it outright.
348
- // Pluggable classification (ClientRegistry): a per-apiClass EXTERNAL-client classifier wins,
349
- // else the app default, else the webpieces built-in — which is side-dependent (a 4xx the SERVER
348
+ // Pluggable classification (ClientRegistry): the CallScope ladder of the call's contract class
349
+ // (method, then API, then ALL), else the webpieces built-in — which is side-dependent (a 4xx the SERVER
350
350
  // raised is a handled non-failure; the same 4xx a CLIENT receives means its call FAILED; 266 is
351
351
  // never a failure either side). This line is FINAL BY POSITION: it only sees what escaped every
352
352
  // retry, so a TRANSIENT_FAILURE here was never retried (no strategy, or a WRITE) or gave up —
@@ -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 { toError } from '../lib/errorUtils';\nimport { LogManager } from '../logging/LogManager';\nimport { ApiCallInfo } from './ApiCallInfo';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { ApiCallContext } from './ApiCallContext';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\nimport { LOG_API_CALL_LOGGER_NAME } from './ApiCallLogName';\nimport { ClientRegistry } from './ClientRegistry';\nimport { CallOutcome } from './ErrorClassifier';\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/** The slice of node's `process.versions` that runtime detection reads (core-util has no node types). */\nclass NodeVersionsProbe {\n node?: string;\n}\n\n/** The slice of node's `process` that runtime detection reads. */\nclass NodeProcessProbe {\n versions?: NodeVersionsProbe;\n}\n\n/** `globalThis`, as seen by runtime detection: a node `process` may or may not exist. */\nclass NodeGlobalProbe {\n process?: NodeProcessProbe;\n}\n\n/**\n * The startup configuration for {@link LogApiCall.configure}.\n *\n * A class rather than an object literal (CLAUDE.md: data-only structures are classes), so the one call\n * site reads `LogApiCall.configure(new LogApiCallConfig(2000))`.\n */\nexport class LogApiCallConfig {\n /**\n * @param maxLoggedBodyChars - the most characters of a serialized request/response body that one\n * `[API-*]` line may carry. `undefined` keeps the runtime default\n * ({@link LogApiCall.BROWSER_DEFAULT_MAX_BODY_CHARS} / {@link LogApiCall.NODE_DEFAULT_MAX_BODY_CHARS});\n * `0` or `Infinity` mean NO cap, for local debugging.\n */\n constructor(readonly maxLoggedBodyChars?: number) {}\n}\n\n/**\n * Process-wide settings for the `[API-*]` lines {@link LogApiCallImpl} writes — today, how much of a\n * body one line may carry (#1300).\n *\n * WHY a cap: the size of a body is already on the same line as a structured tag\n * (`api.requestSize` / `api.responseSize` / `api.responseCount`, computed on the FULL body), so the body\n * text adds no measurement, only bytes. Unbounded, it blew a consumer's 4,000-char browser message\n * trimmer (a Sentry report per line) and on a server it risks Cloud Logging's 256 KB per-entry limit,\n * where an oversize entry is LOST, not shortened.\n *\n * WHY the default depends on the RUNTIME, not on `methodInfo.side`: a node server is also a CLIENT when\n * it calls another service, and those lines go to Cloud Logging too. The runtime is detected ONCE, at\n * module load, by `process.versions.node` only — never `typeof window`, which React Native defines and\n * some SSR setups fake.\n *\n * A static, process-wide setting in the style of {@link ClientRegistry} / `HeaderRegistry`: set once at\n * startup (an app's `main.ts`, a server's bootstrap), never DI-injected, because the browser-side\n * {@link LogApiCallImpl} is constructed by a transport with no DI container.\n */\nexport class LogApiCall {\n /**\n * Browser and React Native default: well under a 4,000-char shipped-log message cap, leaving room\n * for the `[API-…] Class.method response=` prefix and the truncation marker.\n */\n static readonly BROWSER_DEFAULT_MAX_BODY_CHARS = 2000;\n\n /**\n * Node default: even at a worst-case 4 UTF-8 bytes per char this is ~200 KB, under Cloud Logging's\n * 256 KB per-entry limit with room for the entry's other fields.\n */\n static readonly NODE_DEFAULT_MAX_BODY_CHARS = 50000;\n\n /** The default for the runtime this module was loaded in, decided once. */\n static readonly RUNTIME_DEFAULT_MAX_BODY_CHARS = LogApiCall.defaultMaxBodyChars(\n LogApiCall.isNodeRuntime(),\n );\n\n private static configuredMaxBodyChars: number | undefined;\n\n /**\n * Override the runtime default, once at startup. A config whose `maxLoggedBodyChars` is\n * `undefined` restores the runtime default; `0` or `Infinity` turn the cap off.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); set once at startup, never DI-injected\n static configure(config: LogApiCallConfig): void {\n const max = config.maxLoggedBodyChars;\n if (max !== undefined && (Number.isNaN(max) || max < 0)) {\n throw new Error(\n `LogApiCall.configure: maxLoggedBodyChars must be a non-negative number, 0 or Infinity (no cap), or undefined (runtime default); got ${max}`,\n );\n }\n LogApiCall.configuredMaxBodyChars = max;\n }\n\n /** The cap in force: the configured value, else the runtime default. 0 or Infinity mean no cap. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); set once at startup, never DI-injected\n static maxLoggedBodyChars(): number {\n return LogApiCall.configuredMaxBodyChars ?? LogApiCall.RUNTIME_DEFAULT_MAX_BODY_CHARS;\n }\n\n /**\n * The default for a runtime: {@link NODE_DEFAULT_MAX_BODY_CHARS} on node, else\n * {@link BROWSER_DEFAULT_MAX_BODY_CHARS} (browser and React Native alike).\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static defaultMaxBodyChars(isNode: boolean): number {\n return isNode\n ? LogApiCall.NODE_DEFAULT_MAX_BODY_CHARS\n : LogApiCall.BROWSER_DEFAULT_MAX_BODY_CHARS;\n }\n\n /**\n * True only when a node `process` with `versions.node` exists. Read through `globalThis` because\n * this browser-safe package compiles with no node types.\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static isNodeRuntime(): boolean {\n const g = globalThis as NodeGlobalProbe;\n return typeof g.process !== 'undefined' && !!g.process.versions?.node;\n }\n\n /**\n * Cut a serialized body to its first `max` chars and append\n * `…[truncated: logged <kept> of <total> chars]`. Run on the ALREADY-MASKED string, so a cut can\n * never reveal a masked field. Never splits a surrogate pair: when the cut would land between the\n * two halves of one code point, that code point is dropped whole. A body within the cap, an\n * undefined body, and a cap of 0 / Infinity are returned unchanged.\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static truncateForLog(body: string | undefined, max: number): string | undefined {\n if (body === undefined || max === 0 || !Number.isFinite(max) || body.length <= max) {\n return body;\n }\n let end = Math.floor(max);\n const last = body.charCodeAt(end - 1);\n if (last >= 0xd800 && last <= 0xdbff) {\n end -= 1; // a high surrogate whose low half would be cut off — drop the pair\n }\n return `${body.slice(0, end)}…[truncated: logged ${end} of ${body.length} chars]`;\n }\n}\n\n/**\n * LogApiCallImpl - 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 ApiCallContext} 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, and takes that seam as a REQUIRED\n * CONSTRUCTOR ARGUMENT — there is no process-global holder to install and none to forget. Each\n * environment-specific package constructs its own:\n *\n * LogApiFilter (@webpieces/http-routing) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * NodeProxyClient (@webpieces/http-client-node) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * TaskProxyClient (@webpieces/cloudtasks-client) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * BrowserProxyClient (@webpieces/http-client-browser) -> new LogApiCallImpl(new BrowserApiCallContext())\n *\n * NOT a singleton, deliberately: a shared instance would need a shared context, which is the global\n * this constructor replaced. Construct one where you know which environment you are in.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * (request and response bodies are capped at {@link LogApiCall.maxLoggedBodyChars} chars, #1300)\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} error={...} (CallOutcome.EXPECTED)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (TRANSIENT_FAILURE or FAILURE — final by position; each retried attempt is logged by CallRegistry as [API-client-attempt-TRANSIENT])\n */\nexport class LogApiCallImpl {\n /**\n * @param ctx - the environment's {@link ApiCallContext}. REQUIRED, with no default: that is what\n * turns \"nobody bootstrapped the context\" into a compile error instead of a throw on the first\n * real call in production.\n */\n constructor(private readonly ctx: ApiCallContext) {}\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<Q, R>(\n methodInfo: ApiMethodInfo,\n requestDto: Q,\n method: (dto: Q) => Promise<R>,\n responseCount?: (response: R) => number | undefined,\n ): Promise<R> {\n if (methodInfo.noLogging) {\n return this.executeUnlogged(methodInfo, requestDto, method);\n }\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const stamp = this.stamper(this.activeContext());\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 // Only take the field-masking hit when this call declared sensitive fields; otherwise the plain\n // JSON.stringify fast path, unchanged for every existing caller.\n const requestBody = this.serialize(requestDto, methodInfo);\n const requestSize = this.byteSize(requestBody);\n // The size above is on the FULL body; only the log TEXT is capped (#1300), and only after\n // serialize() has masked it.\n const maxBodyChars = LogApiCall.maxLoggedBodyChars();\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(\n `[API-${side}-req] ${id} request=${LogApiCall.truncateForLog(requestBody, maxBodyChars)}`,\n ),\n );\n\n if (!requestDto) 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 = this.serialize(response, methodInfo);\n const count = this.selectResponseCount(response, responseCount, side, id);\n stamp(\n new ApiCallInfo(\n methodInfo,\n 'response',\n 'success',\n durationMs,\n requestSize,\n this.byteSize(responseBody),\n count,\n ),\n () =>\n log.info(\n `[API-${side}-resp-SUCCESS] ${id} response=${LogApiCall.truncateForLog(responseBody, maxBodyChars)}`,\n ),\n );\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 err;\n }\n }\n\n /**\n * Build the tag-and-emit span used around every `[API-*]` line.\n *\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\n * call. `ctx` is passed in (rather than read from `this`) so the caller has already proven it is\n * ACTIVE before anything is stamped.\n */\n private stamper(ctx: ApiCallContext): (info: ApiCallInfo, emit: () => void) => void {\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n return (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n // webpieces-disable no-unmanaged-exceptions -- cleanup must run when a logging backend throws\n try {\n emit();\n } finally {\n ctx.remove(key);\n }\n };\n }\n\n /**\n * The `@Endpoint(..., { noLogging: true })` path: run the call and say NOTHING about it.\n *\n * No `[API-*]` line, no {@link ApiCallInfo} stamp, and no serialization of either DTO — a\n * noLogging route is a log shipper or a log-ingest endpoint, so a line describing the call IS the\n * thing being shipped, and emitting it feeds the next batch its own payload (#976).\n *\n * It deliberately does NOT call {@link activeContext}: that guard exists because a call with\n * nowhere to STAMP a tag is a bug, and this path stamps nothing. A noLogging call is therefore\n * legal outside a RequestContext scope, which is exactly where a flusher tends to run.\n *\n * The null-request check is kept, because it is a contract error and not observability — a\n * noLogging route deserves the same error as every other route, and the throw propagates to the caller\n * unlogged here, exactly as the logged path re-throws.\n *\n * `responseCount` is not taken: it feeds only the `api.responseCount` tag, which is not emitted.\n */\n private async executeUnlogged<Q, R>(\n methodInfo: ApiMethodInfo,\n requestDto: Q,\n method: (dto: Q) => Promise<R>,\n ): Promise<R> {\n if (!requestDto) {\n throw new Error(\n `Request cannot be null and was from ${methodInfo.apiClass}.${methodInfo.methodName}`,\n );\n }\n return method(requestDto);\n }\n\n /**\n * Evaluate an opt-in logical-item count without allowing observability code to affect the call.\n * Warnings deliberately contain neither request nor response bodies (nor the invalid value).\n */\n private selectResponseCount<R>(\n response: R,\n selector: ((response: R) => number | undefined) | undefined,\n side: string,\n id: string,\n ): number | undefined {\n if (!selector) {\n return undefined;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an observability callback must not fail the API call\n try {\n const count = selector(response);\n if (\n count === undefined ||\n (Number.isFinite(count) && Number.isInteger(count) && count >= 0)\n ) {\n return count;\n }\n this.warnInvalidResponseCount(side, id, 'returned an invalid value');\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n this.warnInvalidResponseCount(side, id, 'threw');\n }\n return undefined;\n }\n\n private warnInvalidResponseCount(side: string, id: string, reason: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a warning backend must not replace a successful API response\n try {\n log.warn(\n `[API-${side}-resp-COUNT-WARN] ${id} responseCount selector ${reason}; omitting responseCount`,\n );\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n // The API response is authoritative; observability failures are intentionally ignored here.\n }\n }\n\n /**\n * Serialize a DTO for the LOG LINE ONLY. With no mask on the call, this is a plain JSON.stringify\n * (byte-for-byte the old behavior, no walk) so existing callers pay nothing. With a mask, it runs\n * {@link MaskSpec.stringify}, which produces a masked STRING without ever mutating the DTO — so the\n * object handed to the transport, and thus the value ON THE WIRE, is unchanged.\n */\n private serialize(\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches execute)\n dto: unknown,\n methodInfo: ApiMethodInfo,\n ): string | undefined {\n return methodInfo.mask ? methodInfo.mask.stringify(dto) : JSON.stringify(dto);\n }\n\n /**\n * The ApiCallContext to stamp into. It cannot be MISSING (it is a constructor argument), but it\n * can be INACTIVE — a Node context used outside any `RequestContext.run(...)` scope. That throws:\n * an api call with nowhere to tag is a bug.\n */\n private activeContext(): ApiCallContext {\n const ctx = this.ctx;\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside a ' +\n 'RequestContext.run(...) scope — a server filter opens one per request, and a ' +\n 'non-webpieces host must open one around the work that calls a webpieces client. ' +\n '(A BrowserApiCallContext is always active, so this can only be the Node side.)',\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 // The MESSAGE is on BOTH branches, and that is load-bearing: this is the ONLY operator line\n // per failure now that `ApiErrorBoundary` writes none. An operator error message is never\n // published to a caller (see `ApiErrorCodec.publicMessage`), so the log is the one place it\n // exists at all — dropping it on the caller-error branch would lose it outright.\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). This line is FINAL BY POSITION: it only sees what escaped every\n // retry, so a TRANSIENT_FAILURE here was never retried (no strategy, or a WRITE) or gave up —\n // it logs FAIL exactly like a FAILURE. Only EXPECTED is \"OTHER / result:'success'\".\n const isUser = ClientRegistry.classify(error, methodInfo) === CallOutcome.EXPECTED;\n\n stamp(\n new ApiCallInfo(\n methodInfo,\n 'response',\n isUser ? 'success' : 'failure',\n durationMs,\n requestSize,\n ),\n () =>\n isUser\n ? log.warn(\n `[API-${side}-resp-OTHER] ${id} errorType=${errorType} error=${error.message}`,\n )\n : log.error(\n `[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`,\n ),\n );\n }\n\n /**\n * UTF-8 byte size without platform-specific encoding globals: 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 let bytes = 0;\n for (const character of serialized) {\n const code = character.codePointAt(0)!;\n bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;\n }\n return bytes;\n }\n}\n"]}
1
+ {"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":"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","sourcesContent":["import { toError } from '../lib/errorUtils';\nimport { LogManager } from '../logging/LogManager';\nimport { ApiCallInfo } from './ApiCallInfo';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { ApiCallContext } from './ApiCallContext';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\nimport { LOG_API_CALL_LOGGER_NAME } from './ApiCallLogName';\nimport { ClientRegistry } from './ClientRegistry';\nimport { CallOutcome } from './ErrorClassifier';\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/** The slice of node's `process.versions` that runtime detection reads (core-util has no node types). */\nclass NodeVersionsProbe {\n node?: string;\n}\n\n/** The slice of node's `process` that runtime detection reads. */\nclass NodeProcessProbe {\n versions?: NodeVersionsProbe;\n}\n\n/** `globalThis`, as seen by runtime detection: a node `process` may or may not exist. */\nclass NodeGlobalProbe {\n process?: NodeProcessProbe;\n}\n\n/**\n * The startup configuration for {@link LogApiCall.configure}.\n *\n * A class rather than an object literal (CLAUDE.md: data-only structures are classes), so the one call\n * site reads `LogApiCall.configure(new LogApiCallConfig(2000))`.\n */\nexport class LogApiCallConfig {\n /**\n * @param maxLoggedBodyChars - the most characters of a serialized request/response body that one\n * `[API-*]` line may carry. `undefined` keeps the runtime default\n * ({@link LogApiCall.BROWSER_DEFAULT_MAX_BODY_CHARS} / {@link LogApiCall.NODE_DEFAULT_MAX_BODY_CHARS});\n * `0` or `Infinity` mean NO cap, for local debugging.\n */\n constructor(readonly maxLoggedBodyChars?: number) {}\n}\n\n/**\n * Process-wide settings for the `[API-*]` lines {@link LogApiCallImpl} writes — today, how much of a\n * body one line may carry (#1300).\n *\n * WHY a cap: the size of a body is already on the same line as a structured tag\n * (`api.requestSize` / `api.responseSize` / `api.responseCount`, computed on the FULL body), so the body\n * text adds no measurement, only bytes. Unbounded, it blew a consumer's 4,000-char browser message\n * trimmer (a Sentry report per line) and on a server it risks Cloud Logging's 256 KB per-entry limit,\n * where an oversize entry is LOST, not shortened.\n *\n * WHY the default depends on the RUNTIME, not on `methodInfo.side`: a node server is also a CLIENT when\n * it calls another service, and those lines go to Cloud Logging too. The runtime is detected ONCE, at\n * module load, by `process.versions.node` only — never `typeof window`, which React Native defines and\n * some SSR setups fake.\n *\n * A static, process-wide setting in the style of {@link ClientRegistry} / `HeaderRegistry`: set once at\n * startup (an app's `main.ts`, a server's bootstrap), never DI-injected, because the browser-side\n * {@link LogApiCallImpl} is constructed by a transport with no DI container.\n */\nexport class LogApiCall {\n /**\n * Browser and React Native default: well under a 4,000-char shipped-log message cap, leaving room\n * for the `[API-…] Class.method response=` prefix and the truncation marker.\n */\n static readonly BROWSER_DEFAULT_MAX_BODY_CHARS = 2000;\n\n /**\n * Node default: even at a worst-case 4 UTF-8 bytes per char this is ~200 KB, under Cloud Logging's\n * 256 KB per-entry limit with room for the entry's other fields.\n */\n static readonly NODE_DEFAULT_MAX_BODY_CHARS = 50000;\n\n /** The default for the runtime this module was loaded in, decided once. */\n static readonly RUNTIME_DEFAULT_MAX_BODY_CHARS = LogApiCall.defaultMaxBodyChars(\n LogApiCall.isNodeRuntime(),\n );\n\n private static configuredMaxBodyChars: number | undefined;\n\n /**\n * Override the runtime default, once at startup. A config whose `maxLoggedBodyChars` is\n * `undefined` restores the runtime default; `0` or `Infinity` turn the cap off.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); set once at startup, never DI-injected\n static configure(config: LogApiCallConfig): void {\n const max = config.maxLoggedBodyChars;\n if (max !== undefined && (Number.isNaN(max) || max < 0)) {\n throw new Error(\n `LogApiCall.configure: maxLoggedBodyChars must be a non-negative number, 0 or Infinity (no cap), or undefined (runtime default); got ${max}`,\n );\n }\n LogApiCall.configuredMaxBodyChars = max;\n }\n\n /** The cap in force: the configured value, else the runtime default. 0 or Infinity mean no cap. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); set once at startup, never DI-injected\n static maxLoggedBodyChars(): number {\n return LogApiCall.configuredMaxBodyChars ?? LogApiCall.RUNTIME_DEFAULT_MAX_BODY_CHARS;\n }\n\n /**\n * The default for a runtime: {@link NODE_DEFAULT_MAX_BODY_CHARS} on node, else\n * {@link BROWSER_DEFAULT_MAX_BODY_CHARS} (browser and React Native alike).\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static defaultMaxBodyChars(isNode: boolean): number {\n return isNode\n ? LogApiCall.NODE_DEFAULT_MAX_BODY_CHARS\n : LogApiCall.BROWSER_DEFAULT_MAX_BODY_CHARS;\n }\n\n /**\n * True only when a node `process` with `versions.node` exists. Read through `globalThis` because\n * this browser-safe package compiles with no node types.\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static isNodeRuntime(): boolean {\n const g = globalThis as NodeGlobalProbe;\n return typeof g.process !== 'undefined' && !!g.process.versions?.node;\n }\n\n /**\n * Cut a serialized body to its first `max` chars and append\n * `…[truncated: logged <kept> of <total> chars]`. Run on the ALREADY-MASKED string, so a cut can\n * never reveal a masked field. Never splits a surrogate pair: when the cut would land between the\n * two halves of one code point, that code point is dropped whole. A body within the cap, an\n * undefined body, and a cap of 0 / Infinity are returned unchanged.\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static truncateForLog(body: string | undefined, max: number): string | undefined {\n if (body === undefined || max === 0 || !Number.isFinite(max) || body.length <= max) {\n return body;\n }\n let end = Math.floor(max);\n const last = body.charCodeAt(end - 1);\n if (last >= 0xd800 && last <= 0xdbff) {\n end -= 1; // a high surrogate whose low half would be cut off — drop the pair\n }\n return `${body.slice(0, end)}…[truncated: logged ${end} of ${body.length} chars]`;\n }\n}\n\n/**\n * LogApiCallImpl - 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 ApiCallContext} 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, and takes that seam as a REQUIRED\n * CONSTRUCTOR ARGUMENT — there is no process-global holder to install and none to forget. Each\n * environment-specific package constructs its own:\n *\n * LogApiFilter (@webpieces/http-routing) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * NodeProxyClient (@webpieces/http-client-node) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * TaskProxyClient (@webpieces/cloudtasks-client) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * BrowserProxyClient (@webpieces/http-client-browser) -> new LogApiCallImpl(new BrowserApiCallContext())\n *\n * NOT a singleton, deliberately: a shared instance would need a shared context, which is the global\n * this constructor replaced. Construct one where you know which environment you are in.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * (request and response bodies are capped at {@link LogApiCall.maxLoggedBodyChars} chars, #1300)\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} error={...} (CallOutcome.EXPECTED)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (TRANSIENT_FAILURE or FAILURE — final by position; each retried attempt is logged by CallRegistry as [API-client-attempt-TRANSIENT])\n */\nexport class LogApiCallImpl {\n /**\n * @param ctx - the environment's {@link ApiCallContext}. REQUIRED, with no default: that is what\n * turns \"nobody bootstrapped the context\" into a compile error instead of a throw on the first\n * real call in production.\n */\n constructor(private readonly ctx: ApiCallContext) {}\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<Q, R>(\n methodInfo: ApiMethodInfo,\n requestDto: Q,\n method: (dto: Q) => Promise<R>,\n responseCount?: (response: R) => number | undefined,\n ): Promise<R> {\n if (methodInfo.noLogging) {\n return this.executeUnlogged(methodInfo, requestDto, method);\n }\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const stamp = this.stamper(this.activeContext());\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 // Only take the field-masking hit when this call declared sensitive fields; otherwise the plain\n // JSON.stringify fast path, unchanged for every existing caller.\n const requestBody = this.serialize(requestDto, methodInfo);\n const requestSize = this.byteSize(requestBody);\n // The size above is on the FULL body; only the log TEXT is capped (#1300), and only after\n // serialize() has masked it.\n const maxBodyChars = LogApiCall.maxLoggedBodyChars();\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(\n `[API-${side}-req] ${id} request=${LogApiCall.truncateForLog(requestBody, maxBodyChars)}`,\n ),\n );\n\n if (!requestDto) 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 = this.serialize(response, methodInfo);\n const count = this.selectResponseCount(response, responseCount, side, id);\n stamp(\n new ApiCallInfo(\n methodInfo,\n 'response',\n 'success',\n durationMs,\n requestSize,\n this.byteSize(responseBody),\n count,\n ),\n () =>\n log.info(\n `[API-${side}-resp-SUCCESS] ${id} response=${LogApiCall.truncateForLog(responseBody, maxBodyChars)}`,\n ),\n );\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 err;\n }\n }\n\n /**\n * Build the tag-and-emit span used around every `[API-*]` line.\n *\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\n * call. `ctx` is passed in (rather than read from `this`) so the caller has already proven it is\n * ACTIVE before anything is stamped.\n */\n private stamper(ctx: ApiCallContext): (info: ApiCallInfo, emit: () => void) => void {\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n return (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n // webpieces-disable no-unmanaged-exceptions -- cleanup must run when a logging backend throws\n try {\n emit();\n } finally {\n ctx.remove(key);\n }\n };\n }\n\n /**\n * The `@Endpoint(..., { noLogging: true })` path: run the call and say NOTHING about it.\n *\n * No `[API-*]` line, no {@link ApiCallInfo} stamp, and no serialization of either DTO — a\n * noLogging route is a log shipper or a log-ingest endpoint, so a line describing the call IS the\n * thing being shipped, and emitting it feeds the next batch its own payload (#976).\n *\n * It deliberately does NOT call {@link activeContext}: that guard exists because a call with\n * nowhere to STAMP a tag is a bug, and this path stamps nothing. A noLogging call is therefore\n * legal outside a RequestContext scope, which is exactly where a flusher tends to run.\n *\n * The null-request check is kept, because it is a contract error and not observability — a\n * noLogging route deserves the same error as every other route, and the throw propagates to the caller\n * unlogged here, exactly as the logged path re-throws.\n *\n * `responseCount` is not taken: it feeds only the `api.responseCount` tag, which is not emitted.\n */\n private async executeUnlogged<Q, R>(\n methodInfo: ApiMethodInfo,\n requestDto: Q,\n method: (dto: Q) => Promise<R>,\n ): Promise<R> {\n if (!requestDto) {\n throw new Error(\n `Request cannot be null and was from ${methodInfo.apiClass}.${methodInfo.methodName}`,\n );\n }\n return method(requestDto);\n }\n\n /**\n * Evaluate an opt-in logical-item count without allowing observability code to affect the call.\n * Warnings deliberately contain neither request nor response bodies (nor the invalid value).\n */\n private selectResponseCount<R>(\n response: R,\n selector: ((response: R) => number | undefined) | undefined,\n side: string,\n id: string,\n ): number | undefined {\n if (!selector) {\n return undefined;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an observability callback must not fail the API call\n try {\n const count = selector(response);\n if (\n count === undefined ||\n (Number.isFinite(count) && Number.isInteger(count) && count >= 0)\n ) {\n return count;\n }\n this.warnInvalidResponseCount(side, id, 'returned an invalid value');\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n this.warnInvalidResponseCount(side, id, 'threw');\n }\n return undefined;\n }\n\n private warnInvalidResponseCount(side: string, id: string, reason: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a warning backend must not replace a successful API response\n try {\n log.warn(\n `[API-${side}-resp-COUNT-WARN] ${id} responseCount selector ${reason}; omitting responseCount`,\n );\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n // The API response is authoritative; observability failures are intentionally ignored here.\n }\n }\n\n /**\n * Serialize a DTO for the LOG LINE ONLY. With no mask on the call, this is a plain JSON.stringify\n * (byte-for-byte the old behavior, no walk) so existing callers pay nothing. With a mask, it runs\n * {@link MaskSpec.stringify}, which produces a masked STRING without ever mutating the DTO — so the\n * object handed to the transport, and thus the value ON THE WIRE, is unchanged.\n */\n private serialize(\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches execute)\n dto: unknown,\n methodInfo: ApiMethodInfo,\n ): string | undefined {\n return methodInfo.mask ? methodInfo.mask.stringify(dto) : JSON.stringify(dto);\n }\n\n /**\n * The ApiCallContext to stamp into. It cannot be MISSING (it is a constructor argument), but it\n * can be INACTIVE — a Node context used outside any `RequestContext.run(...)` scope. That throws:\n * an api call with nowhere to tag is a bug.\n */\n private activeContext(): ApiCallContext {\n const ctx = this.ctx;\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside a ' +\n 'RequestContext.run(...) scope — a server filter opens one per request, and a ' +\n 'non-webpieces host must open one around the work that calls a webpieces client. ' +\n '(A BrowserApiCallContext is always active, so this can only be the Node side.)',\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 // The MESSAGE is on BOTH branches, and that is load-bearing: this is the ONLY operator line\n // per failure now that `ApiErrorBoundary` writes none. An operator error message is never\n // published to a caller (see `ApiErrorCodec.publicMessage`), so the log is the one place it\n // exists at all — dropping it on the caller-error branch would lose it outright.\n // Pluggable classification (ClientRegistry): the CallScope ladder of the call's contract class\n // (method, then API, then ALL), 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). This line is FINAL BY POSITION: it only sees what escaped every\n // retry, so a TRANSIENT_FAILURE here was never retried (no strategy, or a WRITE) or gave up —\n // it logs FAIL exactly like a FAILURE. Only EXPECTED is \"OTHER / result:'success'\".\n const isUser = ClientRegistry.classify(error, methodInfo) === CallOutcome.EXPECTED;\n\n stamp(\n new ApiCallInfo(\n methodInfo,\n 'response',\n isUser ? 'success' : 'failure',\n durationMs,\n requestSize,\n ),\n () =>\n isUser\n ? log.warn(\n `[API-${side}-resp-OTHER] ${id} errorType=${errorType} error=${error.message}`,\n )\n : log.error(\n `[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`,\n ),\n );\n }\n\n /**\n * UTF-8 byte size without platform-specific encoding globals: 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 let bytes = 0;\n for (const character of serialized) {\n const code = character.codePointAt(0)!;\n bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;\n }\n return bytes;\n }\n}\n"]}
@@ -53,7 +53,7 @@ class RetryStrategy {
53
53
  }
54
54
  // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs
55
55
  async execute(call, ctx) {
56
- const methodInfo = new ApiMethodInfo_1.ApiMethodInfo('client', ctx.apiName, ctx.methodName);
56
+ const methodInfo = new ApiMethodInfo_1.ApiMethodInfo('client', ctx.apiName, ctx.methodName, ctx.contract);
57
57
  let delayMs = this.initialDelayMs;
58
58
  for (let attempt = 1;; attempt++) {
59
59
  // webpieces-disable no-unmanaged-exceptions -- a strategy decides per failure whether to retry
@@ -1 +1 @@
1
- {"version":3,"file":"RetryStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RetryStrategy.ts"],"names":[],"mappings":"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","sourcesContent":["import { Attempt, CallContext, CallStrategy } from './CallStrategy';\nimport { CallDeadline } from './CallDeadline';\nimport { ClientRegistry } from './ClientRegistry';\nimport { CallOutcome } from './ErrorClassifier';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { WRITE } from './HttpEndpointOptions';\nimport { ApiRetriesExhaustedError } from '../errors/ApiError';\nimport { toError } from '../lib/errorUtils';\n\n/**\n * THE webpieces retry strategy, shared by every transport — register its {@link RetryStrategy.run}\n * with `CallRegistry.setStrategy(...)`, or hand it to a `TransportDefault`.\n *\n * Two questions, two owners, and it asks both:\n *\n * - **the ERROR** — `ClientRegistry.classify(error, …)`. Only `CallOutcome.TRANSIENT_FAILURE` is\n * retried; an EXPECTED or FAILURE outcome is a definitive answer and is rethrown unchanged.\n * - **the ENDPOINT** — `CallContext.operation`. A `WRITE` is NEVER retried, even when this strategy is\n * registered at ALL: a transient error on a non-idempotent write may already have happened.\n *\n * Each attempt gets its own `attemptTimeoutMs` deadline. Between attempts it waits\n * `initialDelayMs`, then multiplies by `delayMultiplier`, capped at `maxDelayMs`.\n *\n * When it RETRIED and ran out, it throws {@link ApiRetriesExhaustedError} (`cause` = the last\n * attempt's error), which is classified FAILURE so no layer above retries again. A call that made a\n * single attempt — `maxRetries` 0, a `WRITE`, a non-transient error — rethrows its own error unchanged.\n */\nexport class RetryStrategy {\n /** The {@link CallStrategy} to register. A bound field, so `strategy.run` can be passed around. */\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n readonly run: CallStrategy<unknown>;\n\n constructor(\n readonly attemptTimeoutMs: number,\n readonly maxRetries: number,\n readonly initialDelayMs: number,\n readonly delayMultiplier: number,\n readonly maxDelayMs: number,\n ) {\n CallDeadline.validate(attemptTimeoutMs);\n RetryStrategy.requireWhole('maxRetries', maxRetries);\n RetryStrategy.requireWhole('initialDelayMs', initialDelayMs);\n RetryStrategy.requireWhole('maxDelayMs', maxDelayMs);\n if (!Number.isFinite(delayMultiplier) || delayMultiplier < 1) {\n throw new RangeError('delayMultiplier must be finite and at least 1');\n }\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n this.run = (call: Attempt<unknown>, ctx: CallContext): Promise<unknown> => this.execute(call, ctx);\n }\n\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n private async execute(call: Attempt<unknown>, ctx: CallContext): Promise<unknown> {\n const methodInfo = new ApiMethodInfo('client', ctx.apiName, ctx.methodName);\n let delayMs = this.initialDelayMs;\n for (let attempt = 1; ; attempt++) {\n // webpieces-disable no-unmanaged-exceptions -- a strategy decides per failure whether to retry\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await call(this.attemptTimeoutMs);\n } catch (err: unknown) {\n const error = toError(err);\n const retryable =\n ctx.operation !== WRITE &&\n ClientRegistry.classify(error, methodInfo) === CallOutcome.TRANSIENT_FAILURE;\n if (!retryable) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a definitive answer is rethrown unchanged\n throw err;\n }\n if (attempt > this.maxRetries) {\n if (attempt === 1) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- never retried, so nothing was exhausted\n throw err;\n }\n throw new ApiRetriesExhaustedError(attempt, error);\n }\n await RetryStrategy.sleep(delayMs);\n delayMs = Math.min(delayMs * this.delayMultiplier, this.maxDelayMs);\n }\n }\n }\n\n // webpieces-disable no-function-outside-class -- constructor argument guard\n private static requireWhole(name: string, value: number): void {\n if (!Number.isInteger(value) || value < 0) {\n throw new RangeError(`${name} must be a non-negative integer`);\n }\n }\n\n // webpieces-disable no-function-outside-class -- backoff timer\n private static sleep(ms: number): Promise<void> {\n return new Promise<void>((resolve: () => void) => setTimeout(resolve, ms));\n }\n}\n"]}
1
+ {"version":3,"file":"RetryStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RetryStrategy.ts"],"names":[],"mappings":"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","sourcesContent":["import { Attempt, CallContext, CallStrategy } from './CallStrategy';\nimport { CallDeadline } from './CallDeadline';\nimport { ClientRegistry } from './ClientRegistry';\nimport { CallOutcome } from './ErrorClassifier';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { WRITE } from './HttpEndpointOptions';\nimport { ApiRetriesExhaustedError } from '../errors/ApiError';\nimport { toError } from '../lib/errorUtils';\n\n/**\n * THE webpieces retry strategy, shared by every transport — register its {@link RetryStrategy.run}\n * with `CallRegistry.setStrategy(...)`, or hand it to a `TransportDefault`.\n *\n * Two questions, two owners, and it asks both:\n *\n * - **the ERROR** — `ClientRegistry.classify(error, …)`. Only `CallOutcome.TRANSIENT_FAILURE` is\n * retried; an EXPECTED or FAILURE outcome is a definitive answer and is rethrown unchanged.\n * - **the ENDPOINT** — `CallContext.operation`. A `WRITE` is NEVER retried, even when this strategy is\n * registered at ALL: a transient error on a non-idempotent write may already have happened.\n *\n * Each attempt gets its own `attemptTimeoutMs` deadline. Between attempts it waits\n * `initialDelayMs`, then multiplies by `delayMultiplier`, capped at `maxDelayMs`.\n *\n * When it RETRIED and ran out, it throws {@link ApiRetriesExhaustedError} (`cause` = the last\n * attempt's error), which is classified FAILURE so no layer above retries again. A call that made a\n * single attempt — `maxRetries` 0, a `WRITE`, a non-transient error — rethrows its own error unchanged.\n */\nexport class RetryStrategy {\n /** The {@link CallStrategy} to register. A bound field, so `strategy.run` can be passed around. */\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n readonly run: CallStrategy<unknown>;\n\n constructor(\n readonly attemptTimeoutMs: number,\n readonly maxRetries: number,\n readonly initialDelayMs: number,\n readonly delayMultiplier: number,\n readonly maxDelayMs: number,\n ) {\n CallDeadline.validate(attemptTimeoutMs);\n RetryStrategy.requireWhole('maxRetries', maxRetries);\n RetryStrategy.requireWhole('initialDelayMs', initialDelayMs);\n RetryStrategy.requireWhole('maxDelayMs', maxDelayMs);\n if (!Number.isFinite(delayMultiplier) || delayMultiplier < 1) {\n throw new RangeError('delayMultiplier must be finite and at least 1');\n }\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n this.run = (call: Attempt<unknown>, ctx: CallContext): Promise<unknown> => this.execute(call, ctx);\n }\n\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n private async execute(call: Attempt<unknown>, ctx: CallContext): Promise<unknown> {\n const methodInfo = new ApiMethodInfo(\n 'client',\n ctx.apiName,\n ctx.methodName,\n ctx.contract,\n );\n let delayMs = this.initialDelayMs;\n for (let attempt = 1; ; attempt++) {\n // webpieces-disable no-unmanaged-exceptions -- a strategy decides per failure whether to retry\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await call(this.attemptTimeoutMs);\n } catch (err: unknown) {\n const error = toError(err);\n const retryable =\n ctx.operation !== WRITE &&\n ClientRegistry.classify(error, methodInfo) === CallOutcome.TRANSIENT_FAILURE;\n if (!retryable) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a definitive answer is rethrown unchanged\n throw err;\n }\n if (attempt > this.maxRetries) {\n if (attempt === 1) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- never retried, so nothing was exhausted\n throw err;\n }\n throw new ApiRetriesExhaustedError(attempt, error);\n }\n await RetryStrategy.sleep(delayMs);\n delayMs = Math.min(delayMs * this.delayMultiplier, this.maxDelayMs);\n }\n }\n }\n\n // webpieces-disable no-function-outside-class -- constructor argument guard\n private static requireWhole(name: string, value: number): void {\n if (!Number.isInteger(value) || value < 0) {\n throw new RangeError(`${name} must be a non-negative integer`);\n }\n }\n\n // webpieces-disable no-function-outside-class -- backoff timer\n private static sleep(ms: number): Promise<void> {\n return new Promise<void>((resolve: () => void) => setTimeout(resolve, ms));\n }\n}\n"]}
@@ -1,13 +1,21 @@
1
1
  import { ClientRole } from '../errors/ClientRole';
2
2
  import { StreamTransportError } from './StreamingContract';
3
+ import { RouteMetadata } from './RouteMetadata';
3
4
  export declare class StreamProtocolError extends StreamTransportError {
4
5
  }
5
6
  export declare class StreamDisconnectedError extends StreamTransportError {
6
7
  }
7
- /** Reserved ASCII-RS controls share exactly the unary ErrorTranslator seam in both directions. */
8
+ /**
9
+ * Reserved ASCII-RS controls share exactly the unary ErrorTranslator seam in both directions —
10
+ * including its `CallScope` ladder: the translator is resolved for the streaming ROUTE's contract
11
+ * class and method (method -> API -> ALL -> webpieces default), exactly as a unary call's is.
12
+ */
8
13
  export declare class StreamErrorControl {
14
+ private readonly route;
15
+ constructor(route: RouteMetadata);
9
16
  encode(error: Error): string;
10
17
  /** @param role - who is receiving the record, declared at the reading side's setup. */
11
18
  decode(line: string, role: ClientRole): Error;
19
+ private translator;
12
20
  private response;
13
21
  }
@@ -29,10 +29,18 @@ class ControlRecord {
29
29
  control;
30
30
  response;
31
31
  }
32
- /** Reserved ASCII-RS controls share exactly the unary ErrorTranslator seam in both directions. */
32
+ /**
33
+ * Reserved ASCII-RS controls share exactly the unary ErrorTranslator seam in both directions —
34
+ * including its `CallScope` ladder: the translator is resolved for the streaming ROUTE's contract
35
+ * class and method (method -> API -> ALL -> webpieces default), exactly as a unary call's is.
36
+ */
33
37
  class StreamErrorControl {
38
+ route;
39
+ constructor(route) {
40
+ this.route = route;
41
+ }
34
42
  encode(error) {
35
- const response = ClientRegistry_1.ClientRegistry.getErrorTranslator().toWire(error);
43
+ const response = this.translator().toWire(error);
36
44
  return `${String.fromCharCode(0x1e)}${JSON.stringify({ control: 'error', response })}\n`;
37
45
  }
38
46
  /** @param role - who is receiving the record, declared at the reading side's setup. */
@@ -40,7 +48,7 @@ class StreamErrorControl {
40
48
  const response = this.response(line);
41
49
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- fromWire returns its typed result by throwing
42
50
  try {
43
- ClientRegistry_1.ClientRegistry.getErrorTranslator().fromWire(response, role);
51
+ this.translator().fromWire(response, role);
44
52
  WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR.fromWire(response, role);
45
53
  }
46
54
  catch (err) {
@@ -49,6 +57,9 @@ class StreamErrorControl {
49
57
  }
50
58
  return new StreamProtocolError('Error translator returned for a JSONL error control record.');
51
59
  }
60
+ translator() {
61
+ return ClientRegistry_1.ClientRegistry.getErrorTranslator(this.route.apiClass, this.route.methodName);
62
+ }
52
63
  response(line) {
53
64
  let control;
54
65
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted control boundary
@@ -1 +1 @@
1
- {"version":3,"file":"StreamErrorControl.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/StreamErrorControl.ts"],"names":[],"mappings":"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","sourcesContent":["import { ClientRole } from '../errors/ClientRole';\nimport { ClientRegistry } from './ClientRegistry';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { DtoValue } from '../mcp/DtoSchema';\nimport { toError } from '../lib/errorUtils';\nimport { StreamTransportError } from './StreamingContract';\n\nexport class StreamProtocolError extends StreamTransportError {}\n\nexport class StreamDisconnectedError extends StreamTransportError {}\n\nclass ControlStatus {\n code?: number;\n reason?: string;\n}\n\nclass ControlHeader {\n name?: string;\n value?: string;\n}\n\nclass ControlResponse {\n status?: ControlStatus;\n headers?: ControlHeader[];\n body?: DtoValue;\n}\n\nclass ControlRecord {\n control?: string;\n response?: ControlResponse;\n}\n\n/** Reserved ASCII-RS controls share exactly the unary ErrorTranslator seam in both directions. */\nexport class StreamErrorControl {\n encode(error: Error): string {\n const response = ClientRegistry.getErrorTranslator().toWire(error);\n return `${String.fromCharCode(0x1e)}${JSON.stringify({ control: 'error', response })}\\n`;\n }\n\n /** @param role - who is receiving the record, declared at the reading side's setup. */\n decode(line: string, role: ClientRole): Error {\n const response = this.response(line);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- fromWire returns its typed result by throwing\n try {\n ClientRegistry.getErrorTranslator().fromWire(response, role);\n WEBPIECES_DEFAULT_ERROR_TRANSLATOR.fromWire(response, role);\n } catch (err: unknown) {\n const error = toError(err);\n return error;\n }\n return new StreamProtocolError(\n 'Error translator returned for a JSONL error control record.',\n );\n }\n\n private response(line: string): HttpResponseDto {\n let control: ControlRecord;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted control boundary\n try {\n control = JSON.parse(line.slice(1)) as ControlRecord;\n } catch (err: unknown) {\n const error = toError(err);\n throw new StreamProtocolError(\n 'Malformed JSONL error control record.',\n undefined,\n undefined,\n error,\n );\n }\n const status = control?.response?.status;\n const headers = control?.response?.headers;\n if (\n control?.control !== 'error' ||\n typeof status?.code !== 'number' ||\n typeof status.reason !== 'string' ||\n !Array.isArray(headers)\n )\n throw new StreamProtocolError('Malformed JSONL error control record.');\n const mapped = headers.map((header: ControlHeader): HttpHeader => {\n if (typeof header?.name !== 'string' || typeof header.value !== 'string') {\n throw new StreamProtocolError('Malformed JSONL error control header.');\n }\n return new HttpHeader(header.name, header.value);\n });\n return new HttpResponseDto(\n new HttpResponseStatus(status.code, status.reason),\n mapped,\n control.response?.body,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"StreamErrorControl.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/StreamErrorControl.ts"],"names":[],"mappings":"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","sourcesContent":["import { ClientRole } from '../errors/ClientRole';\nimport { ClientRegistry } from './ClientRegistry';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { DtoValue } from '../mcp/DtoSchema';\nimport { toError } from '../lib/errorUtils';\nimport { StreamTransportError } from './StreamingContract';\nimport { RouteMetadata } from './RouteMetadata';\nimport { ErrorTranslator } from './ErrorTranslator';\n\nexport class StreamProtocolError extends StreamTransportError {}\n\nexport class StreamDisconnectedError extends StreamTransportError {}\n\nclass ControlStatus {\n code?: number;\n reason?: string;\n}\n\nclass ControlHeader {\n name?: string;\n value?: string;\n}\n\nclass ControlResponse {\n status?: ControlStatus;\n headers?: ControlHeader[];\n body?: DtoValue;\n}\n\nclass ControlRecord {\n control?: string;\n response?: ControlResponse;\n}\n\n/**\n * Reserved ASCII-RS controls share exactly the unary ErrorTranslator seam in both directions —\n * including its `CallScope` ladder: the translator is resolved for the streaming ROUTE's contract\n * class and method (method -> API -> ALL -> webpieces default), exactly as a unary call's is.\n */\nexport class StreamErrorControl {\n constructor(private readonly route: RouteMetadata) {}\n\n encode(error: Error): string {\n const response = this.translator().toWire(error);\n return `${String.fromCharCode(0x1e)}${JSON.stringify({ control: 'error', response })}\\n`;\n }\n\n /** @param role - who is receiving the record, declared at the reading side's setup. */\n decode(line: string, role: ClientRole): Error {\n const response = this.response(line);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- fromWire returns its typed result by throwing\n try {\n this.translator().fromWire(response, role);\n WEBPIECES_DEFAULT_ERROR_TRANSLATOR.fromWire(response, role);\n } catch (err: unknown) {\n const error = toError(err);\n return error;\n }\n return new StreamProtocolError(\n 'Error translator returned for a JSONL error control record.',\n );\n }\n\n private translator(): ErrorTranslator {\n return ClientRegistry.getErrorTranslator(this.route.apiClass, this.route.methodName);\n }\n\n private response(line: string): HttpResponseDto {\n let control: ControlRecord;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted control boundary\n try {\n control = JSON.parse(line.slice(1)) as ControlRecord;\n } catch (err: unknown) {\n const error = toError(err);\n throw new StreamProtocolError(\n 'Malformed JSONL error control record.',\n undefined,\n undefined,\n error,\n );\n }\n const status = control?.response?.status;\n const headers = control?.response?.headers;\n if (\n control?.control !== 'error' ||\n typeof status?.code !== 'number' ||\n typeof status.reason !== 'string' ||\n !Array.isArray(headers)\n )\n throw new StreamProtocolError('Malformed JSONL error control record.');\n const mapped = headers.map((header: ControlHeader): HttpHeader => {\n if (typeof header?.name !== 'string' || typeof header.value !== 'string') {\n throw new StreamProtocolError('Malformed JSONL error control header.');\n }\n return new HttpHeader(header.name, header.value);\n });\n return new HttpResponseDto(\n new HttpResponseStatus(status.code, status.reason),\n mapped,\n control.response?.body,\n );\n }\n}\n"]}
@@ -1,8 +1,8 @@
1
1
  import { ApiMethodInfo } from './ApiMethodInfo';
2
2
  import { CallOutcome, ErrorClassifier } from './ErrorClassifier';
3
3
  /**
4
- * The webpieces built-in {@link ErrorClassifier} — the terminal tier, used when no per-apiClass
5
- * classifier and no app default claims an error. Two tables, one per question:
4
+ * The webpieces built-in {@link ErrorClassifier} — the terminal rung, used when no classifier on the
5
+ * `CallScope` ladder (method, API, ALL) claims an error. Two tables, one per question:
6
6
  *
7
7
  * # 1. EXPECTED or not — "are things WORKING?"
8
8
  *
@@ -5,8 +5,8 @@ const ApiError_1 = require("../errors/ApiError");
5
5
  const OAuthProtocolError_1 = require("../oauth/OAuthProtocolError");
6
6
  const ErrorClassifier_1 = require("./ErrorClassifier");
7
7
  /**
8
- * The webpieces built-in {@link ErrorClassifier} — the terminal tier, used when no per-apiClass
9
- * classifier and no app default claims an error. Two tables, one per question:
8
+ * The webpieces built-in {@link ErrorClassifier} — the terminal rung, used when no classifier on the
9
+ * `CallScope` ladder (method, API, ALL) claims an error. Two tables, one per question:
10
10
  *
11
11
  * # 1. EXPECTED or not — "are things WORKING?"
12
12
  *
@@ -1 +1 @@
1
- {"version":3,"file":"WebpiecesDefaultErrorClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorClassifier.ts"],"names":[],"mappings":";;;AAAA,iDAmB4B;AAC5B,oEAAiE;AAEjE,uDAAiE;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAa,+BAA+B;IACxC,+CAA+C;IAC/C,QAAQ,CAAC,KAAY,EAAE,UAAyB;QAC5C,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;YACrC,OAAO,6BAAW,CAAC,QAAQ,CAAC;QAChC,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,6BAAW,CAAC,iBAAiB,CAAC,CAAC,CAAC,6BAAW,CAAC,OAAO,CAAC;IACzF,CAAC;IAEO,UAAU,CAAC,KAAY,EAAE,UAAyB;QACtD,gEAAgE;QAChE,IAAI,KAAK,YAAY,0BAAe,EAAE,CAAC;YACnC,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,6DAA6D;QAC7D,IAAI,UAAU,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC/B,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,OAAO,CACH,KAAK,YAAY,6BAAkB;YACnC,KAAK,YAAY,+BAAoB;YACrC,KAAK,YAAY,4BAAiB;YAClC,KAAK,YAAY,2BAAgB;YACjC,KAAK,YAAY,2BAAgB;YACjC,KAAK,YAAY,gCAAqB;YACtC,KAAK,YAAY,qCAA0B;YAC3C,KAAK,YAAY,uCAA4B;YAC7C,CAAC,KAAK,YAAY,wBAAa,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;YACzD,CAAC,KAAK,YAAY,uCAAkB,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC,CACjE,CAAC;IACN,CAAC;IAEO,WAAW,CAAC,KAAY;QAC5B,2FAA2F;QAC3F,IAAI,KAAK,YAAY,mCAAwB,EAAE,CAAC;YAC5C,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,IAAI,KAAK,YAAY,wBAAa,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC,UAAU,KAAK,GAAG,IAAI,KAAK,CAAC,UAAU,KAAK,GAAG,CAAC;QAChE,CAAC;QACD,OAAO,CACH,KAAK,YAAY,iCAAsB;YACvC,KAAK,YAAY,oCAAyB;YAC1C,KAAK,YAAY,6BAAkB;YACnC,KAAK,YAAY,8BAAmB;YACpC,KAAK,YAAY,8BAAmB;YACpC,KAAK,YAAY,oCAAyB;YAC1C,KAAK,YAAY,6BAAkB,CACtC,CAAC;IACN,CAAC;CACJ;AAlDD,0EAkDC;AAED,oFAAoF;AACvE,QAAA,kCAAkC,GAAG,IAAI,+BAA+B,EAAE,CAAC","sourcesContent":["import {\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiForbiddenError,\n ApiNotFoundError,\n ApiEndUserError,\n ApiConflictError,\n ApiUnprocessableError,\n ApiPreconditionFailedError,\n ApiUnsupportedMediaTypeError,\n ApiCodedError,\n ApiRetriesExhaustedError,\n ApiRequestTimeoutError,\n ApiDependencyTimeoutError,\n ApiBadGatewayError,\n ApiUnavailableError,\n ApiRateLimitedError,\n ApiDependencyBackoffError,\n ApiConnectionError,\n} from '../errors/ApiError';\nimport { OAuthProtocolError } from '../oauth/OAuthProtocolError';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { CallOutcome, ErrorClassifier } from './ErrorClassifier';\n\n/**\n * The webpieces built-in {@link ErrorClassifier} — the terminal tier, used when no per-apiClass\n * classifier and no app default claims an error. Two tables, one per question:\n *\n * # 1. EXPECTED or not — \"are things WORKING?\"\n *\n * NOT \"was it an HTTP 4xx vs 5xx\" — the two differ (see 408). Classification is by portable Error\n * TYPE, never by transport: this runs over in-process calls, queue handlers and HTTP through one code\n * path and only ever sees a thrown Error.\n *\n * - ApiEndUserError (266): ALWAYS expected, server OR client — an expected \"user made a mistake\".\n * - SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:\n * ApiBadRequestError (400), ApiUnauthorizedError (401), ApiForbiddenError (403), ApiNotFoundError\n * (404), ApiConflictError (409), ApiPreconditionFailedError (412), ApiUnsupportedMediaTypeError\n * (415), ApiUnprocessableError (422), an ApiCodedError whose `isCallerError()` is true (below 500,\n * except 408/429), and an OAuthProtocolError below 500 (a refused grant is the OAuth protocol\n * working, #1176). ApiRequestTimeoutError (408) is deliberately absent: the client may NEVER have\n * seen the response.\n * - CLIENT: receiving ANY error except 266 means the outbound call failed — never expected here.\n * (An external client whose 404 means \"miss\" registers its own classifier for that.)\n *\n * # 2. TRANSIENT or FAILURE — \"may repeating it work?\"\n *\n * The kind table that used to live inside the MCP renderer:\n *\n * - TRANSIENT: request-timeout (408), dependency-timeout (504, incl. a client's own\n * `ApiCallTimeoutError`), bad-gateway (502 proxy), unavailable (503), rate-limited (429),\n * dependency-backoff, connection, and an ApiCodedError of 408/429.\n * - FAILURE: everything else — a received 4xx (our bug), implementation, not-implemented (501), a\n * generic dependency defect, and {@link ApiRetriesExhaustedError}, which is checked FIRST: it is an\n * `ApiDependencyError`, and classifying it transient would let every layer above retry again.\n */\nexport class WebpiecesDefaultErrorClassifier implements ErrorClassifier {\n /** Terminal tier — NEVER returns undefined. */\n classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome {\n if (this.isExpected(error, methodInfo)) {\n return CallOutcome.EXPECTED;\n }\n return this.isTransient(error) ? CallOutcome.TRANSIENT_FAILURE : CallOutcome.FAILURE;\n }\n\n private isExpected(error: Error, methodInfo: ApiMethodInfo): boolean {\n // 266 is the one error that is never a failure, on either side.\n if (error instanceof ApiEndUserError) {\n return true;\n }\n // A client that RECEIVED any other error made a failed call.\n if (methodInfo.side !== 'server') {\n return false;\n }\n return (\n error instanceof ApiBadRequestError ||\n error instanceof ApiUnauthorizedError ||\n error instanceof ApiForbiddenError ||\n error instanceof ApiNotFoundError ||\n error instanceof ApiConflictError ||\n error instanceof ApiUnprocessableError ||\n error instanceof ApiPreconditionFailedError ||\n error instanceof ApiUnsupportedMediaTypeError ||\n (error instanceof ApiCodedError && error.isCallerError()) ||\n (error instanceof OAuthProtocolError && error.isCallerError())\n );\n }\n\n private isTransient(error: Error): boolean {\n // FIRST: a lower layer already retried. Never transient, or every layer above retries too.\n if (error instanceof ApiRetriesExhaustedError) {\n return false;\n }\n if (error instanceof ApiCodedError) {\n return error.statusCode === 408 || error.statusCode === 429;\n }\n return (\n error instanceof ApiRequestTimeoutError ||\n error instanceof ApiDependencyTimeoutError ||\n error instanceof ApiBadGatewayError ||\n error instanceof ApiUnavailableError ||\n error instanceof ApiRateLimitedError ||\n error instanceof ApiDependencyBackoffError ||\n error instanceof ApiConnectionError\n );\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_CLASSIFIER = new WebpiecesDefaultErrorClassifier();\n"]}
1
+ {"version":3,"file":"WebpiecesDefaultErrorClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorClassifier.ts"],"names":[],"mappings":";;;AAAA,iDAmB4B;AAC5B,oEAAiE;AAEjE,uDAAiE;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAa,+BAA+B;IACxC,+CAA+C;IAC/C,QAAQ,CAAC,KAAY,EAAE,UAAyB;QAC5C,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;YACrC,OAAO,6BAAW,CAAC,QAAQ,CAAC;QAChC,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,6BAAW,CAAC,iBAAiB,CAAC,CAAC,CAAC,6BAAW,CAAC,OAAO,CAAC;IACzF,CAAC;IAEO,UAAU,CAAC,KAAY,EAAE,UAAyB;QACtD,gEAAgE;QAChE,IAAI,KAAK,YAAY,0BAAe,EAAE,CAAC;YACnC,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,6DAA6D;QAC7D,IAAI,UAAU,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC/B,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,OAAO,CACH,KAAK,YAAY,6BAAkB;YACnC,KAAK,YAAY,+BAAoB;YACrC,KAAK,YAAY,4BAAiB;YAClC,KAAK,YAAY,2BAAgB;YACjC,KAAK,YAAY,2BAAgB;YACjC,KAAK,YAAY,gCAAqB;YACtC,KAAK,YAAY,qCAA0B;YAC3C,KAAK,YAAY,uCAA4B;YAC7C,CAAC,KAAK,YAAY,wBAAa,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;YACzD,CAAC,KAAK,YAAY,uCAAkB,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC,CACjE,CAAC;IACN,CAAC;IAEO,WAAW,CAAC,KAAY;QAC5B,2FAA2F;QAC3F,IAAI,KAAK,YAAY,mCAAwB,EAAE,CAAC;YAC5C,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,IAAI,KAAK,YAAY,wBAAa,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC,UAAU,KAAK,GAAG,IAAI,KAAK,CAAC,UAAU,KAAK,GAAG,CAAC;QAChE,CAAC;QACD,OAAO,CACH,KAAK,YAAY,iCAAsB;YACvC,KAAK,YAAY,oCAAyB;YAC1C,KAAK,YAAY,6BAAkB;YACnC,KAAK,YAAY,8BAAmB;YACpC,KAAK,YAAY,8BAAmB;YACpC,KAAK,YAAY,oCAAyB;YAC1C,KAAK,YAAY,6BAAkB,CACtC,CAAC;IACN,CAAC;CACJ;AAlDD,0EAkDC;AAED,oFAAoF;AACvE,QAAA,kCAAkC,GAAG,IAAI,+BAA+B,EAAE,CAAC","sourcesContent":["import {\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiForbiddenError,\n ApiNotFoundError,\n ApiEndUserError,\n ApiConflictError,\n ApiUnprocessableError,\n ApiPreconditionFailedError,\n ApiUnsupportedMediaTypeError,\n ApiCodedError,\n ApiRetriesExhaustedError,\n ApiRequestTimeoutError,\n ApiDependencyTimeoutError,\n ApiBadGatewayError,\n ApiUnavailableError,\n ApiRateLimitedError,\n ApiDependencyBackoffError,\n ApiConnectionError,\n} from '../errors/ApiError';\nimport { OAuthProtocolError } from '../oauth/OAuthProtocolError';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { CallOutcome, ErrorClassifier } from './ErrorClassifier';\n\n/**\n * The webpieces built-in {@link ErrorClassifier} — the terminal rung, used when no classifier on the\n * `CallScope` ladder (method, API, ALL) claims an error. Two tables, one per question:\n *\n * # 1. EXPECTED or not — \"are things WORKING?\"\n *\n * NOT \"was it an HTTP 4xx vs 5xx\" — the two differ (see 408). Classification is by portable Error\n * TYPE, never by transport: this runs over in-process calls, queue handlers and HTTP through one code\n * path and only ever sees a thrown Error.\n *\n * - ApiEndUserError (266): ALWAYS expected, server OR client — an expected \"user made a mistake\".\n * - SERVER — a healthy server correctly rejecting a CLIENT'S mistake is metrics NOISE, not a failure:\n * ApiBadRequestError (400), ApiUnauthorizedError (401), ApiForbiddenError (403), ApiNotFoundError\n * (404), ApiConflictError (409), ApiPreconditionFailedError (412), ApiUnsupportedMediaTypeError\n * (415), ApiUnprocessableError (422), an ApiCodedError whose `isCallerError()` is true (below 500,\n * except 408/429), and an OAuthProtocolError below 500 (a refused grant is the OAuth protocol\n * working, #1176). ApiRequestTimeoutError (408) is deliberately absent: the client may NEVER have\n * seen the response.\n * - CLIENT: receiving ANY error except 266 means the outbound call failed — never expected here.\n * (An external client whose 404 means \"miss\" registers its own classifier for that.)\n *\n * # 2. TRANSIENT or FAILURE — \"may repeating it work?\"\n *\n * The kind table that used to live inside the MCP renderer:\n *\n * - TRANSIENT: request-timeout (408), dependency-timeout (504, incl. a client's own\n * `ApiCallTimeoutError`), bad-gateway (502 proxy), unavailable (503), rate-limited (429),\n * dependency-backoff, connection, and an ApiCodedError of 408/429.\n * - FAILURE: everything else — a received 4xx (our bug), implementation, not-implemented (501), a\n * generic dependency defect, and {@link ApiRetriesExhaustedError}, which is checked FIRST: it is an\n * `ApiDependencyError`, and classifying it transient would let every layer above retry again.\n */\nexport class WebpiecesDefaultErrorClassifier implements ErrorClassifier {\n /** Terminal tier — NEVER returns undefined. */\n classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome {\n if (this.isExpected(error, methodInfo)) {\n return CallOutcome.EXPECTED;\n }\n return this.isTransient(error) ? CallOutcome.TRANSIENT_FAILURE : CallOutcome.FAILURE;\n }\n\n private isExpected(error: Error, methodInfo: ApiMethodInfo): boolean {\n // 266 is the one error that is never a failure, on either side.\n if (error instanceof ApiEndUserError) {\n return true;\n }\n // A client that RECEIVED any other error made a failed call.\n if (methodInfo.side !== 'server') {\n return false;\n }\n return (\n error instanceof ApiBadRequestError ||\n error instanceof ApiUnauthorizedError ||\n error instanceof ApiForbiddenError ||\n error instanceof ApiNotFoundError ||\n error instanceof ApiConflictError ||\n error instanceof ApiUnprocessableError ||\n error instanceof ApiPreconditionFailedError ||\n error instanceof ApiUnsupportedMediaTypeError ||\n (error instanceof ApiCodedError && error.isCallerError()) ||\n (error instanceof OAuthProtocolError && error.isCallerError())\n );\n }\n\n private isTransient(error: Error): boolean {\n // FIRST: a lower layer already retried. Never transient, or every layer above retries too.\n if (error instanceof ApiRetriesExhaustedError) {\n return false;\n }\n if (error instanceof ApiCodedError) {\n return error.statusCode === 408 || error.statusCode === 429;\n }\n return (\n error instanceof ApiRequestTimeoutError ||\n error instanceof ApiDependencyTimeoutError ||\n error instanceof ApiBadGatewayError ||\n error instanceof ApiUnavailableError ||\n error instanceof ApiRateLimitedError ||\n error instanceof ApiDependencyBackoffError ||\n error instanceof ApiConnectionError\n );\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_CLASSIFIER = new WebpiecesDefaultErrorClassifier();\n"]}
@@ -5,7 +5,8 @@ import { HttpResponseDto } from './HttpResponseDto';
5
5
  * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by
6
6
  * every node client and by the browser bundle alike.
7
7
  *
8
- * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is
8
+ * It is what {@link ClientRegistry.getErrorTranslator} answers for a call no `CallScope` rung
9
+ * (method, API, ALL) has a translator for, and it is
9
10
  * what an app's own translator calls to DECLINE an error:
10
11
  *
11
12
  * ```ts
@@ -14,7 +14,8 @@ const HttpResponseDto_1 = require("./HttpResponseDto");
14
14
  * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by
15
15
  * every node client and by the browser bundle alike.
16
16
  *
17
- * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is
17
+ * It is what {@link ClientRegistry.getErrorTranslator} answers for a call no `CallScope` rung
18
+ * (method, API, ALL) has a translator for, and it is
18
19
  * what an app's own translator calls to DECLINE an error:
19
20
  *
20
21
  * ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ClientRole } from '../errors/ClientRole';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { OAuthErrorResponse } from '../oauth/OAuthErrorResponse';\nimport { OAuthProtocolError } from '../oauth/OAuthProtocolError';\nimport { ApiCallTimeoutError } from './ApiCallTimeoutError';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * It holds no ROLE: who is receiving ({@link ClientRole}) is an argument of `fromWire`, supplied by\n * the client that read the response from its own setup, so one stateless instance serves a server's\n * clients and a browser's alike.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n /** Reason phrase for a client deadline; see {@link ApiCallTimeoutError}. Never 'Gateway Timeout'. */\n static readonly CALL_TIMEOUT_REASON = 'Network Timeout';\n\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [426, 'Upgrade Required'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * The one exception is an {@link OAuthProtocolError}: its caller is an OAuth client, so it is\n * answered in the RFC shape by `OAuthErrorResponse`, not as a payload.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n // An OAuth refusal is answered to an OAuth CLIENT, which parses the RFC 6749/6750 body and\n // never the ApiError envelope (#1176). Same renderer as every other OAuth boundary.\n if (error instanceof OAuthProtocolError) {\n return OAuthErrorResponse.toResponse(error, undefined);\n }\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.reasonPhrase(error, status)),\n headers,\n payload,\n );\n }\n\n /**\n * The reason phrase for `error`. An {@link ApiCallTimeoutError} is THIS process's own client\n * deadline firing on an outbound call: no gateway was involved, so it is never titled\n * 'Gateway Timeout' (issue #1170). It keeps its 504 status and `dependency-timeout` payload —\n * retry semantics are unchanged — and only the human framing differs.\n */\n private reasonPhrase(error: Error, status: number): string {\n if (error instanceof ApiCallTimeoutError) {\n return WebpiecesDefaultErrorTranslator.CALL_TIMEOUT_REASON;\n }\n return this.genericMessage(status);\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY\n * bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/\n * backoff/unavailable meaning, 266 is the end user's own answer, a 426 `client-too-old` stays\n * {@link ApiClientTooOldError}, and a 401 depends on `role` (see {@link ClientRole}).\n *\n * @param role - who is receiving, declared at the client's setup. No default.\n */\n fromWire(response: HttpResponseDto, role: ClientRole): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message =\n decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(\n role,\n code,\n message,\n decoded,\n this.retryAfterSeconds(response.headers),\n );\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n\n /** Parse either legal Retry-After form and bound untrusted guidance to one day. */\n private retryAfterSeconds(headers: readonly HttpHeader[]): number | undefined {\n const raw = headers.find(\n (header: HttpHeader) => header.name.toLowerCase() === 'retry-after',\n )?.value;\n if (raw === undefined) return undefined;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.min(Math.ceil(seconds), 86_400);\n const at = Date.parse(raw);\n if (!Number.isFinite(at)) return undefined;\n return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}
1
+ {"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":";;;AAAA,iEAA8D;AAC9D,2DAAwD;AACxD,iDAA6D;AAE7D,yEAAsE;AACtE,oEAAiE;AACjE,oEAAiE;AACjE,+DAA4D;AAC5D,6DAAyE;AAEzE,uDAAoF;AAEpF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAa,+BAA+B;IACxC,qGAAqG;IACrG,MAAM,CAAU,mBAAmB,GAAG,iBAAiB,CAAC;IAEvC,QAAQ,GAAG,IAAI,mCAAgB,EAAE,CAAC;IAElC,eAAe,GAAwB,IAAI,GAAG,CAAiB;QAC5E,CAAC,GAAG,EAAE,gBAAgB,CAAC;QACvB,CAAC,GAAG,EAAE,aAAa,CAAC;QACpB,CAAC,GAAG,EAAE,cAAc,CAAC;QACrB,CAAC,GAAG,EAAE,WAAW,CAAC;QAClB,CAAC,GAAG,EAAE,WAAW,CAAC;QAClB,CAAC,GAAG,EAAE,iBAAiB,CAAC;QACxB,CAAC,GAAG,EAAE,UAAU,CAAC;QACjB,CAAC,GAAG,EAAE,qBAAqB,CAAC;QAC5B,CAAC,GAAG,EAAE,wBAAwB,CAAC;QAC/B,CAAC,GAAG,EAAE,uBAAuB,CAAC;QAC9B,CAAC,GAAG,EAAE,kBAAkB,CAAC;QACzB,CAAC,GAAG,EAAE,mBAAmB,CAAC;QAC1B,CAAC,GAAG,EAAE,uBAAuB,CAAC;QAC9B,CAAC,GAAG,EAAE,iBAAiB,CAAC;QACxB,CAAC,GAAG,EAAE,aAAa,CAAC;QACpB,CAAC,GAAG,EAAE,qBAAqB,CAAC;QAC5B,CAAC,GAAG,EAAE,iBAAiB,CAAC;KAC3B,CAAC,CAAC;IAEH;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,KAAY;QACf,2FAA2F;QAC3F,oFAAoF;QACpF,IAAI,KAAK,YAAY,uCAAkB,EAAE,CAAC;YACtC,OAAO,uCAAkB,CAAC,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAC3D,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5C,2FAA2F;QAC3F,mEAAmE;QACnE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAqB,CAAC;QAC3C,MAAM,MAAM,GAAG,uCAAkB,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QACpE,MAAM,OAAO,GACT,OAAO,CAAC,iBAAiB,KAAK,SAAS;YACnC,CAAC,CAAC,CAAC,IAAI,4BAAU,CAAC,aAAa,EAAE,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC;YACpE,CAAC,CAAC,EAAE,CAAC;QACb,OAAO,IAAI,iCAAe,CACtB,IAAI,oCAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,EAChE,OAAO,EACP,OAAO,CACV,CAAC;IACN,CAAC;IAED;;;;;OAKG;IACK,YAAY,CAAC,KAAY,EAAE,MAAc;QAC7C,IAAI,KAAK,YAAY,yCAAmB,EAAE,CAAC;YACvC,OAAO,+BAA+B,CAAC,mBAAmB,CAAC;QAC/D,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,QAAyB,EAAE,IAAgB;QAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC;QAClC,IAAI,IAAI,IAAI,GAAG,IAAI,IAAI,GAAG,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YAC5C,OAAO;QACX,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,OAAO,GACT,OAAO,EAAE,OAAO,IAAI,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpF,MAAM,2CAAoB,CAAC,KAAK,CAC5B,IAAI,EACJ,IAAI,EACJ,OAAO,EACP,OAAO,EACP,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAC3C,CAAC;IACN,CAAC;IAED,0EAA0E;IAC1E,cAAc,CAAC,IAAY;QACvB,OAAO,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,gBAAgB,CAAC;IAC9D,CAAC;IAED;;;;OAIG;IACK,UAAU,CAAC,QAAyB;QACxC,IAAI,CAAC,6BAAa,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,MAAM,OAAO,GAAG,6BAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,uCAAkB,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YACvC,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,6FAA6F;QAC7F,MAAM,UAAU,GAAG,OAAO,YAAY,wBAAa,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;QACrF,MAAM,QAAQ,GAAG,uCAAkB,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACtE,OAAO,QAAQ,KAAK,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACnE,CAAC;IAED,kHAAkH;IAC1G,eAAe,CAAC,IAAa,EAAE,MAAc;QACjD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,wBAAwB,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,KAAK,CAAC;YACxE,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACzF,CAAC;QACD,OAAO,MAAM,IAAI,gBAAgB,CAAC;IACtC,CAAC;IAED,mFAAmF;IAC3E,iBAAiB,CAAC,OAA8B;QACpD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CACpB,CAAC,MAAkB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,aAAa,CACtE,EAAE,KAAK,CAAC;QACT,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACxC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACnC,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1F,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,OAAO,SAAS,CAAC;QAC3C,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC/E,CAAC;;AA9JL,0EA+JC;AAED,oFAAoF;AACvE,QAAA,kCAAkC,GAAG,IAAI,+BAA+B,EAAE,CAAC","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ClientRole } from '../errors/ClientRole';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { OAuthErrorResponse } from '../oauth/OAuthErrorResponse';\nimport { OAuthProtocolError } from '../oauth/OAuthProtocolError';\nimport { ApiCallTimeoutError } from './ApiCallTimeoutError';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} answers for a call no `CallScope` rung\n * (method, API, ALL) has a translator for, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * It holds no ROLE: who is receiving ({@link ClientRole}) is an argument of `fromWire`, supplied by\n * the client that read the response from its own setup, so one stateless instance serves a server's\n * clients and a browser's alike.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n /** Reason phrase for a client deadline; see {@link ApiCallTimeoutError}. Never 'Gateway Timeout'. */\n static readonly CALL_TIMEOUT_REASON = 'Network Timeout';\n\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [426, 'Upgrade Required'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * The one exception is an {@link OAuthProtocolError}: its caller is an OAuth client, so it is\n * answered in the RFC shape by `OAuthErrorResponse`, not as a payload.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n // An OAuth refusal is answered to an OAuth CLIENT, which parses the RFC 6749/6750 body and\n // never the ApiError envelope (#1176). Same renderer as every other OAuth boundary.\n if (error instanceof OAuthProtocolError) {\n return OAuthErrorResponse.toResponse(error, undefined);\n }\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.reasonPhrase(error, status)),\n headers,\n payload,\n );\n }\n\n /**\n * The reason phrase for `error`. An {@link ApiCallTimeoutError} is THIS process's own client\n * deadline firing on an outbound call: no gateway was involved, so it is never titled\n * 'Gateway Timeout' (issue #1170). It keeps its 504 status and `dependency-timeout` payload —\n * retry semantics are unchanged — and only the human framing differs.\n */\n private reasonPhrase(error: Error, status: number): string {\n if (error instanceof ApiCallTimeoutError) {\n return WebpiecesDefaultErrorTranslator.CALL_TIMEOUT_REASON;\n }\n return this.genericMessage(status);\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY\n * bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/\n * backoff/unavailable meaning, 266 is the end user's own answer, a 426 `client-too-old` stays\n * {@link ApiClientTooOldError}, and a 401 depends on `role` (see {@link ClientRole}).\n *\n * @param role - who is receiving, declared at the client's setup. No default.\n */\n fromWire(response: HttpResponseDto, role: ClientRole): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message =\n decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(\n role,\n code,\n message,\n decoded,\n this.retryAfterSeconds(response.headers),\n );\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n\n /** Parse either legal Retry-After form and bound untrusted guidance to one day. */\n private retryAfterSeconds(headers: readonly HttpHeader[]): number | undefined {\n const raw = headers.find(\n (header: HttpHeader) => header.name.toLowerCase() === 'retry-after',\n )?.value;\n if (raw === undefined) return undefined;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.min(Math.ceil(seconds), 86_400);\n const at = Date.parse(raw);\n if (!Number.isFinite(at)) return undefined;\n return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}