@webpieces/core-util 0.4.790 → 0.4.792

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 (50) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/errors/ReceivedApiErrorRule.d.ts +43 -0
  4. package/src/errors/ReceivedApiErrorRule.js +65 -0
  5. package/src/errors/ReceivedApiErrorRule.js.map +1 -0
  6. package/src/errors/index.d.ts +1 -0
  7. package/src/errors/index.js +3 -1
  8. package/src/errors/index.js.map +1 -1
  9. package/src/http/ClientRegistry.d.ts +29 -18
  10. package/src/http/ClientRegistry.js +37 -27
  11. package/src/http/ClientRegistry.js.map +1 -1
  12. package/src/http/ErrorTranslator.d.ts +115 -0
  13. package/src/http/{ErrorTranslators.js → ErrorTranslator.js} +1 -1
  14. package/src/http/ErrorTranslator.js.map +1 -0
  15. package/src/http/FailureClassifier.d.ts +1 -1
  16. package/src/http/FailureClassifier.js.map +1 -1
  17. package/src/http/HttpResponseDto.d.ts +1 -1
  18. package/src/http/HttpResponseDto.js +1 -1
  19. package/src/http/HttpResponseDto.js.map +1 -1
  20. package/src/http/ServiceInfo.d.ts +1 -1
  21. package/src/http/ServiceInfo.js +1 -1
  22. package/src/http/ServiceInfo.js.map +1 -1
  23. package/src/http/Surface.d.ts +39 -0
  24. package/src/http/Surface.js +28 -0
  25. package/src/http/Surface.js.map +1 -0
  26. package/src/http/WebpiecesCoreHeaders.d.ts +18 -0
  27. package/src/http/WebpiecesCoreHeaders.js +18 -0
  28. package/src/http/WebpiecesCoreHeaders.js.map +1 -1
  29. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +70 -0
  30. package/src/http/WebpiecesDefaultErrorTranslator.js +136 -0
  31. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -0
  32. package/src/index.d.ts +5 -1
  33. package/src/index.js +15 -3
  34. package/src/index.js.map +1 -1
  35. package/src/ipc/IpcConnection.js +6 -8
  36. package/src/ipc/IpcConnection.js.map +1 -1
  37. package/src/ipc/IpcErrorTranslator.d.ts +39 -0
  38. package/src/ipc/IpcErrorTranslator.js +3 -0
  39. package/src/ipc/IpcErrorTranslator.js.map +1 -0
  40. package/src/ipc/IpcRegistry.d.ts +55 -0
  41. package/src/ipc/IpcRegistry.js +73 -0
  42. package/src/ipc/IpcRegistry.js.map +1 -0
  43. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.d.ts +28 -0
  44. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js +49 -0
  45. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js.map +1 -0
  46. package/src/ipc/index.d.ts +4 -0
  47. package/src/ipc/index.js +9 -1
  48. package/src/ipc/index.js.map +1 -1
  49. package/src/http/ErrorTranslators.d.ts +0 -106
  50. package/src/http/ErrorTranslators.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorTranslator.ts"],"names":[],"mappings":"","sourcesContent":["import { 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): void {\n * if (response.status.code !== 460) {\n * this.fallback.fromWire(response); // 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)`.\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): void;\n}\n"]}
@@ -9,7 +9,7 @@ import { ApiMethodInfo } from './ApiMethodInfo';
9
9
  * rather than a bare function — so "find usages" in an IDE lands on every implementation.
10
10
  *
11
11
  * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once
12
- * singleton that owns URL mappings and {@link ErrorTranslators}. TWO tiers, resolved most-specific
12
+ * singleton that owns URL mappings and {@link ErrorTranslator}. TWO tiers, resolved most-specific
13
13
  * first (see {@link ClientRegistry.classifyFailure}):
14
14
  *
15
15
  * 1. `ClientRegistry.setDefaultFailureClassifier(c)` — ONE per app/company. It reads
@@ -1 +1 @@
1
- {"version":3,"file":"FailureClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/FailureClassifier.ts"],"names":[],"mappings":";;;AAuCA;;;;;GAKG;AACH,MAAa,sBAAsB;IAEX;IACA;IAFpB,YACoB,QAAgB,EAChB,UAA6B;QAD7B,aAAQ,GAAR,QAAQ,CAAQ;QAChB,eAAU,GAAV,UAAU,CAAmB;IAC9C,CAAC;CACP;AALD,wDAKC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Decides whether a thrown API-call error is a real FAILURE — the process not working, SURFACE it\n * (LogApiCall logs `[API-*-resp-FAIL]`, `jsonPayload.api.result='failure'` — what dashboards/alerts\n * count) — or an EXPECTED non-failure — the process working correctly, a handled condition\n * (`[API-*-resp-OTHER]`, `result='success'`).\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once\n * singleton that owns URL mappings and {@link ErrorTranslators}. TWO tiers, resolved most-specific\n * first (see {@link ClientRegistry.classifyFailure}):\n *\n * 1. `ClientRegistry.setDefaultFailureClassifier(c)` — ONE per app/company. It reads\n * {@link ApiMethodInfo.side}, so a single strategy covers the SERVER router AND all INTERNAL\n * clients (webpieces http client, cloud tasks). Optional: if unset, webpieces uses\n * {@link WebpiecesDefaultFailureClassifier} (server 4xx = non-failure; client non-266 = failure).\n * 2. `ClientRegistry.addFailureClassifier(apiClass, c)` — per EXTERNAL client, keyed by\n * {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi', 'TwilioApi'). Each external\n * API qualifies errors differently (a Firestore 404 miss, a Twilio 429 retry are EXPECTED, not\n * failures), so it overrides the default for that apiClass only.\n *\n * Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal;\n * they fall through to tier 1. An external client that forgets to register is FAIL-SAFE: it also\n * falls to tier 1 (client side ⇒ every non-266 = failure) until it opts in.\n */\nexport interface FailureClassifier {\n /**\n * @param error - the already-normalized thrown error (callers pass `toError(err)`)\n * @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client\n * @returns `true` = real failure; `false` = expected/non-failure; `undefined` = DEFER\n * (a per-apiClass classifier defers to the app default; the app default defers to the\n * webpieces built-in). Deferring is what makes classifiers additive AND override-capable.\n */\n isFailure(error: Error, methodInfo: ApiMethodInfo): boolean | undefined;\n}\n\n/**\n * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the\n * {@link FailureClassifier} to apply for it. A data-only structure, so it is a class (not an inline\n * object literal), per CLAUDE.md — lets startup wiring carry a list of these and hand each to\n * {@link ClientRegistry.addFailureClassifier}.\n */\nexport class KeyedFailureClassifier {\n constructor(\n public readonly apiClass: string,\n public readonly classifier: FailureClassifier,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"FailureClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/FailureClassifier.ts"],"names":[],"mappings":";;;AAuCA;;;;;GAKG;AACH,MAAa,sBAAsB;IAEX;IACA;IAFpB,YACoB,QAAgB,EAChB,UAA6B;QAD7B,aAAQ,GAAR,QAAQ,CAAQ;QAChB,eAAU,GAAV,UAAU,CAAmB;IAC9C,CAAC;CACP;AALD,wDAKC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Decides whether a thrown API-call error is a real FAILURE — the process not working, SURFACE it\n * (LogApiCall logs `[API-*-resp-FAIL]`, `jsonPayload.api.result='failure'` — what dashboards/alerts\n * count) — or an EXPECTED non-failure — the process working correctly, a handled condition\n * (`[API-*-resp-OTHER]`, `result='success'`).\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once\n * singleton that owns URL mappings and {@link ErrorTranslator}. TWO tiers, resolved most-specific\n * first (see {@link ClientRegistry.classifyFailure}):\n *\n * 1. `ClientRegistry.setDefaultFailureClassifier(c)` — ONE per app/company. It reads\n * {@link ApiMethodInfo.side}, so a single strategy covers the SERVER router AND all INTERNAL\n * clients (webpieces http client, cloud tasks). Optional: if unset, webpieces uses\n * {@link WebpiecesDefaultFailureClassifier} (server 4xx = non-failure; client non-266 = failure).\n * 2. `ClientRegistry.addFailureClassifier(apiClass, c)` — per EXTERNAL client, keyed by\n * {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi', 'TwilioApi'). Each external\n * API qualifies errors differently (a Firestore 404 miss, a Twilio 429 retry are EXPECTED, not\n * failures), so it overrides the default for that apiClass only.\n *\n * Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal;\n * they fall through to tier 1. An external client that forgets to register is FAIL-SAFE: it also\n * falls to tier 1 (client side ⇒ every non-266 = failure) until it opts in.\n */\nexport interface FailureClassifier {\n /**\n * @param error - the already-normalized thrown error (callers pass `toError(err)`)\n * @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client\n * @returns `true` = real failure; `false` = expected/non-failure; `undefined` = DEFER\n * (a per-apiClass classifier defers to the app default; the app default defers to the\n * webpieces built-in). Deferring is what makes classifiers additive AND override-capable.\n */\n isFailure(error: Error, methodInfo: ApiMethodInfo): boolean | undefined;\n}\n\n/**\n * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the\n * {@link FailureClassifier} to apply for it. A data-only structure, so it is a class (not an inline\n * object literal), per CLAUDE.md — lets startup wiring carry a list of these and hand each to\n * {@link ClientRegistry.addFailureClassifier}.\n */\nexport class KeyedFailureClassifier {\n constructor(\n public readonly apiClass: string,\n public readonly classifier: FailureClassifier,\n ) {}\n}\n"]}
@@ -8,7 +8,7 @@
8
8
  * # Why a DTO at all, instead of handing an app express's `res` or fetch's `Response`
9
9
  *
10
10
  * Node/express and browser fetch model a response completely differently, and an
11
- * {@link ErrorTranslators} implementation is registered ONCE and serves BOTH — the server writing a
11
+ * {@link ErrorTranslator} implementation is registered ONCE and serves BOTH — the server writing a
12
12
  * response and every client in the process reading one. So neither transport's object can be the
13
13
  * currency. webpieces normalises both into this DTO at its own boundary, and the app only ever sees
14
14
  * this. One form, both transports, both directions.
@@ -11,7 +11,7 @@ exports.HttpResponseDto = exports.HttpResponseStatus = exports.HttpHeader = void
11
11
  * # Why a DTO at all, instead of handing an app express's `res` or fetch's `Response`
12
12
  *
13
13
  * Node/express and browser fetch model a response completely differently, and an
14
- * {@link ErrorTranslators} implementation is registered ONCE and serves BOTH — the server writing a
14
+ * {@link ErrorTranslator} implementation is registered ONCE and serves BOTH — the server writing a
15
15
  * response and every client in the process reading one. So neither transport's object can be the
16
16
  * currency. webpieces normalises both into this DTO at its own boundary, and the app only ever sees
17
17
  * this. One form, both transports, both directions.
@@ -1 +1 @@
1
- {"version":3,"file":"HttpResponseDto.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpResponseDto.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAa,UAAU;IAEC;IACA;IAFpB,YACoB,IAAY,EACZ,KAAa;QADb,SAAI,GAAJ,IAAI,CAAQ;QACZ,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AALD,gCAKC;AAED,4FAA4F;AAC5F,MAAa,kBAAkB;IAEP;IACA;IAFpB,YACoB,IAAY,EACZ,MAAc;QADd,SAAI,GAAJ,IAAI,CAAQ;QACZ,WAAM,GAAN,MAAM,CAAQ;IAC/B,CAAC;CACP;AALD,gDAKC;AAED;;;;;;GAMG;AACH,gFAAgF;AAChF,MAAa,eAAe;IAEJ;IACA;IAEA;IAJpB,YACoB,MAA0B,EAC1B,OAA8B;IAC9C,mLAAmL;IACnK,IAAW;QAHX,WAAM,GAAN,MAAM,CAAoB;QAC1B,YAAO,GAAP,OAAO,CAAuB;QAE9B,SAAI,GAAJ,IAAI,CAAO;IAC5B,CAAC;CACP;AAPD,0CAOC","sourcesContent":["/**\n * HttpResponseDto - the ENTIRE HTTP response, as pure data, in the ONE form webpieces speaks.\n *\n * Modelled on java webpieces' `http/http1_1-parser/.../api/dto/` (`HttpPayload` -> `HttpMessage` ->\n * `HttpResponse` -> `HttpResponseStatusLine` -> `HttpResponseStatus`), flattened to the KISS subset\n * this framework actually needs.\n *\n * # Why a DTO at all, instead of handing an app express's `res` or fetch's `Response`\n *\n * Node/express and browser fetch model a response completely differently, and an\n * {@link ErrorTranslators} implementation is registered ONCE and serves BOTH — the server writing a\n * response and every client in the process reading one. So neither transport's object can be the\n * currency. webpieces normalises both into this DTO at its own boundary, and the app only ever sees\n * this. One form, both transports, both directions.\n *\n * # The two properties copied from java deliberately\n *\n * - **Headers are a LIST of `{name, value}`, not a Map.** HTTP permits repeats (`Set-Cookie` is the\n * everyday one) and a Map silently drops all but the last. The REQUEST side of webpieces-ts\n * already respects this (`readExpressHeaders` returns `Map<string, string[]>`), so the response\n * side must not be the half that loses data.\n * - **Status is `{ code, reason }`, not a bare number.** The reason phrase is part of the response\n * and an app may want its own ('Order Not Found' beside a 460).\n *\n * `HttpVersion` from the java DTO is deliberately OMITTED: express and fetch each own the version,\n * and no app decision depends on it.\n *\n * Pure data, so these are CLASSES with explicit constructors (webpieces guideline: data => classes),\n * and they live in core-util so the identical types reach a node server and a browser bundle.\n */\nexport class HttpHeader {\n constructor(\n public readonly name: string,\n public readonly value: string,\n ) {}\n}\n\n/** The status line's status: the numeric code AND the reason phrase that goes beside it. */\nexport class HttpResponseStatus {\n constructor(\n public readonly code: number,\n public readonly reason: string,\n ) {}\n}\n\n/**\n * The whole response: status (code + reason), the header LIST, and the body.\n *\n * `body` is `unknown` because it is whatever the app chose to publish. webpieces' OWN default puts a\n * {@link ApiErrorPayload} there, and the built-in client mapping reads it back as one — but an app that\n * owns the whole response owns the body shape too, so the framework does not constrain it.\n */\n// webpieces-disable no-any-unknown -- the app owns the full-response body shape\nexport class HttpResponseDto<TBody = unknown> {\n constructor(\n public readonly status: HttpResponseStatus,\n public readonly headers: readonly HttpHeader[],\n // webpieces-disable no-any-unknown -- the app owns the body shape when it owns the response; webpieces' own default puts a ApiErrorPayload here, an app puts whatever it publishes\n public readonly body: TBody,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"HttpResponseDto.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpResponseDto.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAa,UAAU;IAEC;IACA;IAFpB,YACoB,IAAY,EACZ,KAAa;QADb,SAAI,GAAJ,IAAI,CAAQ;QACZ,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AALD,gCAKC;AAED,4FAA4F;AAC5F,MAAa,kBAAkB;IAEP;IACA;IAFpB,YACoB,IAAY,EACZ,MAAc;QADd,SAAI,GAAJ,IAAI,CAAQ;QACZ,WAAM,GAAN,MAAM,CAAQ;IAC/B,CAAC;CACP;AALD,gDAKC;AAED;;;;;;GAMG;AACH,gFAAgF;AAChF,MAAa,eAAe;IAEJ;IACA;IAEA;IAJpB,YACoB,MAA0B,EAC1B,OAA8B;IAC9C,mLAAmL;IACnK,IAAW;QAHX,WAAM,GAAN,MAAM,CAAoB;QAC1B,YAAO,GAAP,OAAO,CAAuB;QAE9B,SAAI,GAAJ,IAAI,CAAO;IAC5B,CAAC;CACP;AAPD,0CAOC","sourcesContent":["/**\n * HttpResponseDto - the ENTIRE HTTP response, as pure data, in the ONE form webpieces speaks.\n *\n * Modelled on java webpieces' `http/http1_1-parser/.../api/dto/` (`HttpPayload` -> `HttpMessage` ->\n * `HttpResponse` -> `HttpResponseStatusLine` -> `HttpResponseStatus`), flattened to the KISS subset\n * this framework actually needs.\n *\n * # Why a DTO at all, instead of handing an app express's `res` or fetch's `Response`\n *\n * Node/express and browser fetch model a response completely differently, and an\n * {@link ErrorTranslator} implementation is registered ONCE and serves BOTH — the server writing a\n * response and every client in the process reading one. So neither transport's object can be the\n * currency. webpieces normalises both into this DTO at its own boundary, and the app only ever sees\n * this. One form, both transports, both directions.\n *\n * # The two properties copied from java deliberately\n *\n * - **Headers are a LIST of `{name, value}`, not a Map.** HTTP permits repeats (`Set-Cookie` is the\n * everyday one) and a Map silently drops all but the last. The REQUEST side of webpieces-ts\n * already respects this (`readExpressHeaders` returns `Map<string, string[]>`), so the response\n * side must not be the half that loses data.\n * - **Status is `{ code, reason }`, not a bare number.** The reason phrase is part of the response\n * and an app may want its own ('Order Not Found' beside a 460).\n *\n * `HttpVersion` from the java DTO is deliberately OMITTED: express and fetch each own the version,\n * and no app decision depends on it.\n *\n * Pure data, so these are CLASSES with explicit constructors (webpieces guideline: data => classes),\n * and they live in core-util so the identical types reach a node server and a browser bundle.\n */\nexport class HttpHeader {\n constructor(\n public readonly name: string,\n public readonly value: string,\n ) {}\n}\n\n/** The status line's status: the numeric code AND the reason phrase that goes beside it. */\nexport class HttpResponseStatus {\n constructor(\n public readonly code: number,\n public readonly reason: string,\n ) {}\n}\n\n/**\n * The whole response: status (code + reason), the header LIST, and the body.\n *\n * `body` is `unknown` because it is whatever the app chose to publish. webpieces' OWN default puts a\n * {@link ApiErrorPayload} there, and the built-in client mapping reads it back as one — but an app that\n * owns the whole response owns the body shape too, so the framework does not constrain it.\n */\n// webpieces-disable no-any-unknown -- the app owns the full-response body shape\nexport class HttpResponseDto<TBody = unknown> {\n constructor(\n public readonly status: HttpResponseStatus,\n public readonly headers: readonly HttpHeader[],\n // webpieces-disable no-any-unknown -- the app owns the body shape when it owns the response; webpieces' own default puts a ApiErrorPayload here, an app puts whatever it publishes\n public readonly body: TBody,\n ) {}\n}\n"]}
@@ -65,6 +65,6 @@ export declare class ServiceInfo {
65
65
  * This build's version, or `undefined` when unset — same non-throwing contract as {@link getName}.
66
66
  */
67
67
  static getVersion(): string | undefined;
68
- /** Reset — for tests, mirroring {@link ClientRegistry.clear}. */
68
+ /** Reset — for tests, mirroring {@link ClientRegistry.resetForTests}. */
69
69
  static clear(): void;
70
70
  }
@@ -86,7 +86,7 @@ class ServiceInfo {
86
86
  static getVersion() {
87
87
  return ServiceInfo.svcVersion;
88
88
  }
89
- /** Reset — for tests, mirroring {@link ClientRegistry.clear}. */
89
+ /** Reset — for tests, mirroring {@link ClientRegistry.resetForTests}. */
90
90
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected
91
91
  static clear() {
92
92
  ServiceInfo.svcName = undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"ServiceInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ServiceInfo.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAa,WAAW;IACpB,gEAAgE;IACxD,MAAM,CAAC,OAAO,CAAqB;IAE3C,+FAA+F;IACvF,MAAM,CAAC,UAAU,CAAqB;IAE9C;;;;;;;;;;OAUG;IACH,4JAA4J;IAC5J,MAAM,CAAC,OAAO,CAAC,IAAY,EAAE,OAAe;QACxC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACX,qFAAqF;gBACrF,qFAAqF;gBACrF,2BAA2B,CAC9B,CAAC;QACN,CAAC;QACD,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,WAAW,CAAC,UAAU,GAAG,OAAO,CAAC;IACrC,CAAC;IAED;;;;;;OAMG;IACH,4JAA4J;IAC5J,MAAM,CAAC,OAAO;QACV,OAAO,WAAW,CAAC,OAAO,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,4JAA4J;IAC5J,MAAM,CAAC,UAAU;QACb,OAAO,WAAW,CAAC,UAAU,CAAC;IAClC,CAAC;IAED,iEAAiE;IACjE,4JAA4J;IAC5J,MAAM,CAAC,KAAK;QACR,WAAW,CAAC,OAAO,GAAG,SAAS,CAAC;QAChC,WAAW,CAAC,UAAU,GAAG,SAAS,CAAC;IACvC,CAAC;CACJ;AA5DD,kCA4DC","sourcesContent":["/**\n * ServiceInfo - the ONE answer to \"what service am I, and which build of it?\", for every part of\n * webpieces that needs it.\n *\n * Configured like {@link HeaderRegistry} / {@link ClientRegistry} / LogManager — populated once at\n * startup, then globally accessible with NO DI wiring. Browser-safe (no `process.env`, no node-only\n * deps), which is why it lives in core-util beside {@link ClientRegistry}.\n *\n * ```ts\n * // startup, FIRST:\n * ServiceInfo.setInfo('my-service', '2.1.0');\n * ```\n *\n * WHY THIS EXISTS. Both facts used to be artifacts of WHICH LOGGING LIBRARY YOU PICKED rather than\n * facts about the service:\n * - the name lived on `BunyanFactoryOptions`, because bunyan REQUIRES a root-logger name\n * (`options.name (string) is required` — bunyan's own TypeError). winston has no such concept, so\n * a winston app shipped its logs UNNAMED.\n * - the version lived on `WinstonFactoryOptions` as `svcGitHash`, so a bunyan app could not stamp a\n * version AT ALL — and the name presumed a git SHA, which not every project deploys from.\n *\n * Switching backends silently changed which fields your logs carried. Several unrelated readers\n * need these same two facts, so they belong to the framework, not to a backend:\n * - the winston backend, to stamp `svcName` + `version`;\n * - the bunyan backend, to stamp `version` (its root-logger `name` is a fixed constant now);\n * - {@link WebpiecesCoreHeaders.REQUEST_ID_SOURCE}, to record WHO minted a request id;\n * - {@link WebpiecesCoreHeaders.CLIENT_VERSION}, so a downstream server can log which build called it.\n *\n * VERSION IS OPAQUE. It is whatever string identifies THIS build — a git SHA, a semver tag, a CI\n * build number. webpieces neither parses nor derives it; the app decides where it comes from (a\n * generated file, an env var, a Docker build arg) and passes it here.\n *\n * DOES NOT THROW ON READ. {@link getName} / {@link getVersion} return `undefined` when unset, so a\n * log line emitted before `setInfo` (early boot) still ships — it simply omits the version. Logging\n * never blocks on identity. The \"a deployed build MUST identify itself\" guarantee is enforced ONCE,\n * loudly, at startup by whoever calls {@link setInfo} — `setupRuntime` takes name+version as REQUIRED\n * inputs and calls it, so a blank value throws and kills the deploy — instead of every reader throwing.\n */\nexport class ServiceInfo {\n /** This service's name. Process-global; set once at startup. */\n private static svcName: string | undefined;\n\n /** This build's version — an opaque app-chosen string. Process-global; set once at startup. */\n private static svcVersion: string | undefined;\n\n /**\n * Identify this service. Call it at startup.\n *\n * LAST CALL WINS, deliberately. A real deployment identifies itself once, but an in-process test\n * can legitimately boot TWO services back-to-back (see the app-example e2e two-server flow), so a\n * \"one process = one service\" rule would reject a case that genuinely exists.\n *\n * @param name - this service's name, e.g. 'my-service'.\n * @param version - the opaque identifier of THIS build (git SHA, semver, CI build number).\n * @throws Error on a blank name or version — that is always a bug, never a use case.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static setInfo(name: string, version: string): void {\n if (!name || !name.trim()) {\n throw new Error('ServiceInfo.setInfo(...) requires a non-blank service name.');\n }\n if (!version || !version.trim()) {\n throw new Error(\n 'ServiceInfo.setInfo(...) requires a non-blank version. It is opaque to webpieces — ' +\n 'a git SHA, a semver tag, a CI build number — but every deployed build must be able ' +\n 'to say which build it is.',\n );\n }\n ServiceInfo.svcName = name;\n ServiceInfo.svcVersion = version;\n }\n\n /**\n * This service's name, or `undefined` when {@link setInfo} has not been called. Does NOT throw:\n * readers (logging backends, requestIdSource, outbound CLIENT_VERSION) simply omit the field when\n * it is missing, so a pre-`setInfo` log line still emits. Identity is REQUIRED where it matters —\n * `setupRuntime` takes name+version and calls {@link setInfo} at startup, so a real server is\n * always identified before it serves traffic.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static getName(): string | undefined {\n return ServiceInfo.svcName;\n }\n\n /**\n * This build's version, or `undefined` when unset — same non-throwing contract as {@link getName}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static getVersion(): string | undefined {\n return ServiceInfo.svcVersion;\n }\n\n /** Reset — for tests, mirroring {@link ClientRegistry.clear}. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static clear(): void {\n ServiceInfo.svcName = undefined;\n ServiceInfo.svcVersion = undefined;\n }\n}\n"]}
1
+ {"version":3,"file":"ServiceInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ServiceInfo.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAa,WAAW;IACpB,gEAAgE;IACxD,MAAM,CAAC,OAAO,CAAqB;IAE3C,+FAA+F;IACvF,MAAM,CAAC,UAAU,CAAqB;IAE9C;;;;;;;;;;OAUG;IACH,4JAA4J;IAC5J,MAAM,CAAC,OAAO,CAAC,IAAY,EAAE,OAAe;QACxC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACX,qFAAqF;gBACrF,qFAAqF;gBACrF,2BAA2B,CAC9B,CAAC;QACN,CAAC;QACD,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,WAAW,CAAC,UAAU,GAAG,OAAO,CAAC;IACrC,CAAC;IAED;;;;;;OAMG;IACH,4JAA4J;IAC5J,MAAM,CAAC,OAAO;QACV,OAAO,WAAW,CAAC,OAAO,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,4JAA4J;IAC5J,MAAM,CAAC,UAAU;QACb,OAAO,WAAW,CAAC,UAAU,CAAC;IAClC,CAAC;IAED,yEAAyE;IACzE,4JAA4J;IAC5J,MAAM,CAAC,KAAK;QACR,WAAW,CAAC,OAAO,GAAG,SAAS,CAAC;QAChC,WAAW,CAAC,UAAU,GAAG,SAAS,CAAC;IACvC,CAAC;CACJ;AA5DD,kCA4DC","sourcesContent":["/**\n * ServiceInfo - the ONE answer to \"what service am I, and which build of it?\", for every part of\n * webpieces that needs it.\n *\n * Configured like {@link HeaderRegistry} / {@link ClientRegistry} / LogManager — populated once at\n * startup, then globally accessible with NO DI wiring. Browser-safe (no `process.env`, no node-only\n * deps), which is why it lives in core-util beside {@link ClientRegistry}.\n *\n * ```ts\n * // startup, FIRST:\n * ServiceInfo.setInfo('my-service', '2.1.0');\n * ```\n *\n * WHY THIS EXISTS. Both facts used to be artifacts of WHICH LOGGING LIBRARY YOU PICKED rather than\n * facts about the service:\n * - the name lived on `BunyanFactoryOptions`, because bunyan REQUIRES a root-logger name\n * (`options.name (string) is required` — bunyan's own TypeError). winston has no such concept, so\n * a winston app shipped its logs UNNAMED.\n * - the version lived on `WinstonFactoryOptions` as `svcGitHash`, so a bunyan app could not stamp a\n * version AT ALL — and the name presumed a git SHA, which not every project deploys from.\n *\n * Switching backends silently changed which fields your logs carried. Several unrelated readers\n * need these same two facts, so they belong to the framework, not to a backend:\n * - the winston backend, to stamp `svcName` + `version`;\n * - the bunyan backend, to stamp `version` (its root-logger `name` is a fixed constant now);\n * - {@link WebpiecesCoreHeaders.REQUEST_ID_SOURCE}, to record WHO minted a request id;\n * - {@link WebpiecesCoreHeaders.CLIENT_VERSION}, so a downstream server can log which build called it.\n *\n * VERSION IS OPAQUE. It is whatever string identifies THIS build — a git SHA, a semver tag, a CI\n * build number. webpieces neither parses nor derives it; the app decides where it comes from (a\n * generated file, an env var, a Docker build arg) and passes it here.\n *\n * DOES NOT THROW ON READ. {@link getName} / {@link getVersion} return `undefined` when unset, so a\n * log line emitted before `setInfo` (early boot) still ships — it simply omits the version. Logging\n * never blocks on identity. The \"a deployed build MUST identify itself\" guarantee is enforced ONCE,\n * loudly, at startup by whoever calls {@link setInfo} — `setupRuntime` takes name+version as REQUIRED\n * inputs and calls it, so a blank value throws and kills the deploy — instead of every reader throwing.\n */\nexport class ServiceInfo {\n /** This service's name. Process-global; set once at startup. */\n private static svcName: string | undefined;\n\n /** This build's version — an opaque app-chosen string. Process-global; set once at startup. */\n private static svcVersion: string | undefined;\n\n /**\n * Identify this service. Call it at startup.\n *\n * LAST CALL WINS, deliberately. A real deployment identifies itself once, but an in-process test\n * can legitimately boot TWO services back-to-back (see the app-example e2e two-server flow), so a\n * \"one process = one service\" rule would reject a case that genuinely exists.\n *\n * @param name - this service's name, e.g. 'my-service'.\n * @param version - the opaque identifier of THIS build (git SHA, semver, CI build number).\n * @throws Error on a blank name or version — that is always a bug, never a use case.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static setInfo(name: string, version: string): void {\n if (!name || !name.trim()) {\n throw new Error('ServiceInfo.setInfo(...) requires a non-blank service name.');\n }\n if (!version || !version.trim()) {\n throw new Error(\n 'ServiceInfo.setInfo(...) requires a non-blank version. It is opaque to webpieces — ' +\n 'a git SHA, a semver tag, a CI build number — but every deployed build must be able ' +\n 'to say which build it is.',\n );\n }\n ServiceInfo.svcName = name;\n ServiceInfo.svcVersion = version;\n }\n\n /**\n * This service's name, or `undefined` when {@link setInfo} has not been called. Does NOT throw:\n * readers (logging backends, requestIdSource, outbound CLIENT_VERSION) simply omit the field when\n * it is missing, so a pre-`setInfo` log line still emits. Identity is REQUIRED where it matters —\n * `setupRuntime` takes name+version and calls {@link setInfo} at startup, so a real server is\n * always identified before it serves traffic.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static getName(): string | undefined {\n return ServiceInfo.svcName;\n }\n\n /**\n * This build's version, or `undefined` when unset — same non-throwing contract as {@link getName}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static getVersion(): string | undefined {\n return ServiceInfo.svcVersion;\n }\n\n /** Reset — for tests, mirroring {@link ClientRegistry.resetForTests}. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static clear(): void {\n ServiceInfo.svcName = undefined;\n ServiceInfo.svcVersion = undefined;\n }\n}\n"]}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * WHICH KIND OF CALLER is driving this request, all the way from the edge.
3
+ *
4
+ * The same endpoint can be reached by a browser GUI, by an LLM through the MCP bridge, and by an
5
+ * external partner against a published REST contract. "Does an end-user error render as 266 or as a
6
+ * real 4xx" is therefore a property of the CALLER, not of the server — which is why the per-router
7
+ * `setEndUserStatus` this replaced could never express it: one router serves all three.
8
+ *
9
+ * webpieces already knows which, from the auth mode that matched the incoming request:
10
+ *
11
+ * | how the request authenticated | surface |
12
+ * |-------------------------------|--------------|
13
+ * | `@WpAuthJwt` | `gui` |
14
+ * | `@WpMcpAuthJwt` (MCP bridge) | `llm` |
15
+ * | `@WpAuthApiKey` | `public-api` |
16
+ *
17
+ * It is set at the EDGE ONLY and then PROPAGATES: the surface is the ORIGINAL caller's, so a hop that
18
+ * already received one INHERITS it unchanged. `@WpAuthOidc` is an internal service-to-service hop, not
19
+ * a surface of its own, and must never overwrite a propagated value — `GUI -> api1 -> api2` leaves
20
+ * api2 seeing `gui`, which is the whole point of carrying it.
21
+ */
22
+ export type Surface = 'gui' | 'llm' | 'public-api';
23
+ /**
24
+ * The ONE derivation of "what HTTP status does an `ApiEndUserError` publish on this request" — the
25
+ * per-request replacement for the deleted per-router `EndUserStatus`.
26
+ *
27
+ * - `gui` and `llm` -> **266**. Both are webpieces clients that DECODE the body: a browser client
28
+ * reconstructs the typed `ApiEndUserError`, and the MCP bridge calls through `ApiFactory` and
29
+ * renders the result for the model itself. A real 4xx would throw away the message either one is
30
+ * there to show.
31
+ * - `public-api` -> the status the THROW SITE published (`ApiEndUserError.edgeHttpStatus`, else 400),
32
+ * because a partner's REST contract promises real statuses and has no webpieces client to decode
33
+ * anything.
34
+ * - **absent** -> 266, the historical default, so a public endpoint or a non-webpieces caller behaves
35
+ * exactly as it did before this key existed.
36
+ */
37
+ export declare class SurfaceEndUserStatus {
38
+ static statusFor(surface: Surface | undefined, edgeHttpStatus: number | undefined): number;
39
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SurfaceEndUserStatus = void 0;
4
+ /**
5
+ * The ONE derivation of "what HTTP status does an `ApiEndUserError` publish on this request" — the
6
+ * per-request replacement for the deleted per-router `EndUserStatus`.
7
+ *
8
+ * - `gui` and `llm` -> **266**. Both are webpieces clients that DECODE the body: a browser client
9
+ * reconstructs the typed `ApiEndUserError`, and the MCP bridge calls through `ApiFactory` and
10
+ * renders the result for the model itself. A real 4xx would throw away the message either one is
11
+ * there to show.
12
+ * - `public-api` -> the status the THROW SITE published (`ApiEndUserError.edgeHttpStatus`, else 400),
13
+ * because a partner's REST contract promises real statuses and has no webpieces client to decode
14
+ * anything.
15
+ * - **absent** -> 266, the historical default, so a public endpoint or a non-webpieces caller behaves
16
+ * exactly as it did before this key existed.
17
+ */
18
+ class SurfaceEndUserStatus {
19
+ // webpieces-disable no-function-outside-class -- stateless per-request derivation, no state to hold
20
+ static statusFor(surface, edgeHttpStatus) {
21
+ if (surface === 'public-api') {
22
+ return edgeHttpStatus ?? 400;
23
+ }
24
+ return 266;
25
+ }
26
+ }
27
+ exports.SurfaceEndUserStatus = SurfaceEndUserStatus;
28
+ //# sourceMappingURL=Surface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Surface.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/Surface.ts"],"names":[],"mappings":";;;AAuBA;;;;;;;;;;;;;GAaG;AACH,MAAa,oBAAoB;IAC7B,oGAAoG;IACpG,MAAM,CAAC,SAAS,CAAC,OAA4B,EAAE,cAAkC;QAC7E,IAAI,OAAO,KAAK,YAAY,EAAE,CAAC;YAC3B,OAAO,cAAc,IAAI,GAAG,CAAC;QACjC,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;CACJ;AARD,oDAQC","sourcesContent":["/**\n * WHICH KIND OF CALLER is driving this request, all the way from the edge.\n *\n * The same endpoint can be reached by a browser GUI, by an LLM through the MCP bridge, and by an\n * external partner against a published REST contract. \"Does an end-user error render as 266 or as a\n * real 4xx\" is therefore a property of the CALLER, not of the server — which is why the per-router\n * `setEndUserStatus` this replaced could never express it: one router serves all three.\n *\n * webpieces already knows which, from the auth mode that matched the incoming request:\n *\n * | how the request authenticated | surface |\n * |-------------------------------|--------------|\n * | `@WpAuthJwt` | `gui` |\n * | `@WpMcpAuthJwt` (MCP bridge) | `llm` |\n * | `@WpAuthApiKey` | `public-api` |\n *\n * It is set at the EDGE ONLY and then PROPAGATES: the surface is the ORIGINAL caller's, so a hop that\n * already received one INHERITS it unchanged. `@WpAuthOidc` is an internal service-to-service hop, not\n * a surface of its own, and must never overwrite a propagated value — `GUI -> api1 -> api2` leaves\n * api2 seeing `gui`, which is the whole point of carrying it.\n */\nexport type Surface = 'gui' | 'llm' | 'public-api';\n\n/**\n * The ONE derivation of \"what HTTP status does an `ApiEndUserError` publish on this request\" — the\n * per-request replacement for the deleted per-router `EndUserStatus`.\n *\n * - `gui` and `llm` -> **266**. Both are webpieces clients that DECODE the body: a browser client\n * reconstructs the typed `ApiEndUserError`, and the MCP bridge calls through `ApiFactory` and\n * renders the result for the model itself. A real 4xx would throw away the message either one is\n * there to show.\n * - `public-api` -> the status the THROW SITE published (`ApiEndUserError.edgeHttpStatus`, else 400),\n * because a partner's REST contract promises real statuses and has no webpieces client to decode\n * anything.\n * - **absent** -> 266, the historical default, so a public endpoint or a non-webpieces caller behaves\n * exactly as it did before this key existed.\n */\nexport class SurfaceEndUserStatus {\n // webpieces-disable no-function-outside-class -- stateless per-request derivation, no state to hold\n static statusFor(surface: Surface | undefined, edgeHttpStatus: number | undefined): number {\n if (surface === 'public-api') {\n return edgeHttpStatus ?? 400;\n }\n return 266;\n }\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { ContextKey, AnyContextKey } from '../ContextKey';
2
2
  import { ApiCallInfo } from './ApiCallInfo';
3
+ import { Surface } from './Surface';
3
4
  /**
4
5
  * Core framework context keys — the minimum the WebPieces framework needs to correlate one
5
6
  * request across every service it touches, and across every log line each of them writes.
@@ -97,6 +98,23 @@ export declare class WebpiecesCoreHeaders {
97
98
  static readonly ORG_ID: ContextKey<string, "trusted">;
98
99
  static readonly USER_ID: ContextKey<string, "trusted">;
99
100
  static readonly USER_ROLES: ContextKey<string, "trusted">;
101
+ /**
102
+ * WHICH KIND OF CALLER is driving this request — `gui`, `llm` or `public-api`. See
103
+ * {@link Surface} for the table and for why it is a property of the CALLER and not of the server.
104
+ *
105
+ * TRUSTED, and that is load-bearing twice over. It is what makes an app safe to branch on it in
106
+ * its OWN authorization logic ("this endpoint is not exposed to partners"), and it is what makes
107
+ * the value in {@link SurfaceEndUserStatus} believable: webpieces sets it itself, from the auth
108
+ * mode that matched, never from caller input. A caller that simply SENDS `x-wp-surface` at an
109
+ * endpoint whose auth did not establish one is rejected by `AuthFilter.reconcileWireTrust`, like
110
+ * every other unvouched trusted header.
111
+ *
112
+ * It keeps its `httpHeader` because PROPAGATION is the point: the surface belongs to the ORIGINAL
113
+ * caller, so `GUI -> api1 -> api2` must leave api2 seeing `gui`. An internal `@WpAuthOidc` hop
114
+ * verifies its caller, so the propagated value is admitted verbatim — and the auth filter only
115
+ * ever SETS a surface when none arrived, so an inherited one is never overwritten.
116
+ */
117
+ static readonly SURFACE: ContextKey<Surface, "trusted">;
100
118
  /**
101
119
  * Turns on test-case recording for this request (Java: x-webpieces-recording).
102
120
  * Transferred so recording follows the request across service hops.
@@ -100,6 +100,23 @@ class WebpiecesCoreHeaders {
100
100
  static ORG_ID = ContextKey_1.ContextKey.trusted('orgId', 'derived from a verified credential by an app-bound JwtHook (a ContextTuple in AuthenticatedCaller)', 'x-org-id');
101
101
  static USER_ID = ContextKey_1.ContextKey.trusted('userId', 'derived from a verified credential by an app-bound JwtHook (a ContextTuple in AuthenticatedCaller)', 'x-user-id');
102
102
  static USER_ROLES = ContextKey_1.ContextKey.trusted('roles', 'derived from a verified credential by an app-bound JwtHook (a ContextTuple in AuthenticatedCaller)', 'x-webpieces-roles');
103
+ /**
104
+ * WHICH KIND OF CALLER is driving this request — `gui`, `llm` or `public-api`. See
105
+ * {@link Surface} for the table and for why it is a property of the CALLER and not of the server.
106
+ *
107
+ * TRUSTED, and that is load-bearing twice over. It is what makes an app safe to branch on it in
108
+ * its OWN authorization logic ("this endpoint is not exposed to partners"), and it is what makes
109
+ * the value in {@link SurfaceEndUserStatus} believable: webpieces sets it itself, from the auth
110
+ * mode that matched, never from caller input. A caller that simply SENDS `x-wp-surface` at an
111
+ * endpoint whose auth did not establish one is rejected by `AuthFilter.reconcileWireTrust`, like
112
+ * every other unvouched trusted header.
113
+ *
114
+ * It keeps its `httpHeader` because PROPAGATION is the point: the surface belongs to the ORIGINAL
115
+ * caller, so `GUI -> api1 -> api2` must leave api2 seeing `gui`. An internal `@WpAuthOidc` hop
116
+ * verifies its caller, so the propagated value is admitted verbatim — and the auth filter only
117
+ * ever SETS a surface when none arrived, so an inherited one is never overwritten.
118
+ */
119
+ static SURFACE = ContextKey_1.ContextKey.trusted('surface', 'set by the webpieces AuthFilter from the auth mode that matched the incoming request, or inherited from the edge hop that did', 'x-wp-surface');
103
120
  /**
104
121
  * Turns on test-case recording for this request (Java: x-webpieces-recording).
105
122
  * Transferred so recording follows the request across service hops.
@@ -258,6 +275,7 @@ class WebpiecesCoreHeaders {
258
275
  WebpiecesCoreHeaders.USER_ID,
259
276
  WebpiecesCoreHeaders.ORG_ID,
260
277
  WebpiecesCoreHeaders.USER_ROLES,
278
+ WebpiecesCoreHeaders.SURFACE,
261
279
  WebpiecesCoreHeaders.RECORDING,
262
280
  WebpiecesCoreHeaders.API_CALL_INFO,
263
281
  WebpiecesCoreHeaders.HTTP_METHOD,
@@ -1 +1 @@
1
- {"version":3,"file":"WebpiecesCoreHeaders.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesCoreHeaders.ts"],"names":[],"mappings":"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","sourcesContent":["import { ContextKey, AnyContextKey } from '../ContextKey';\nimport { ApiCallInfo } from './ApiCallInfo';\n\n/**\n * Core framework context keys — the minimum the WebPieces framework needs to correlate one\n * request across every service it touches, and across every log line each of them writes.\n *\n * ONE id, propagated unchanged. The first service to see a request without an `x-request-id`\n * generates one (RequestContextHeaders.fillFromRequest); every hop copies it onward verbatim. Grep that id and you\n * have the whole call tree. There is no per-hop id and no parent pointer: a chain of ids you must\n * stitch back together buys nothing a single shared id does not already give you.\n *\n * Lives in core-util (browser-safe) so both the http clients and http-server can reference it.\n *\n * Exposed as {@link HeaderRegistry.DEFAULT_HEADERS} — a service opts into these by\n * passing `platformHeaders=true` to `HeaderRegistry.configure(...)`.\n *\n * Each key's `name` is the logical/log name; `httpHeader` is the wire name.\n */\nexport class WebpiecesCoreHeaders {\n /**\n * The id that correlates every hop of one request, and every log line of every hop.\n * Generated by the first service to see a request without one; propagated unchanged after that.\n */\n static readonly REQUEST_ID = ContextKey.untrusted<string>('requestId', 'x-request-id');\n\n /**\n * WHICH SERVICE MINTED {@link REQUEST_ID} — the name from {@link ServiceInfo}, stamped by\n * `RequestContextHeaders.fillFromRequest` ONLY on the branch that generates a new id (i.e. when\n * the inbound request carried no `x-request-id`). It answers the question the id alone cannot:\n * \"this trace starts here — is that right?\" An id appearing with no source means it came from\n * outside; an id sourced by a service that should never be an entry point is a routing bug.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire, and that is the WHOLE POINT. If it\n * travelled, hop 2 would inherit it, hop 3 would inherit it, and \"who started this trace\"\n * would be indistinguishable from \"who passed it along\" — the origin, the one fact this key\n * carries, would be lost. It is absent on every hop that did NOT mint the id, which is exactly\n * the signal: present == I am the origin.\n * - `isLogged` TRUE → emitted as a plain string at `jsonPayload.requestIdSource`.\n */\n static readonly REQUEST_ID_SOURCE = ContextKey.untrusted<string>(\n 'requestIdSource',\n /*httpHeader*/ undefined,\n );\n\n /**\n * The CALLER's build version — so a downstream server's logs record which build of the client\n * called it (surfaces as `jsonPayload.clientVersion`). Distinct from the log line's own `version`\n * (this service's build): `version` answers \"which build wrote this line?\", `clientVersion`\n * answers \"which build asked us to?\".\n *\n * - `httpHeader` SET → transferred over the wire, BUT unlike a normal transferred key it is NOT\n * copied from the context onward. Each hop OVERWRITES it with its OWN `ServiceInfo.getVersion()`\n * as it becomes the client to the next hop (see `buildOutboundHeaders`), so on any given server\n * `clientVersion` is always the IMMEDIATE caller's version, never a stale grand-caller's.\n * - `isLogged` TRUE → the inbound value lands in the context and flows through the normal log\n * field map; no backend change needed.\n */\n static readonly CLIENT_VERSION = ContextKey.untrusted<string>(\n 'clientVersion',\n 'x-webpieces-client-version',\n );\n\n /**\n * A frontend/app-minted correlation id that groups every request triggered by ONE user ACTION.\n *\n * An \"action\" is a single thing the user did in the GUI — a CLICK on a button/link, or TYPING in a\n * field — or a background poller tick: anything that may fan out into MULTIPLE remote calls. That one\n * action fires 1..N browser HTTP calls, each of which gets its own framework-minted {@link REQUEST_ID}\n * (one per HTTP call, shared within that call's server→server subtree). `actionId` sits ABOVE\n * `requestId` and is what stitches those N requests back to the single action that caused them:\n *\n * actionId (app-minted, ONE per user action, rides EVERY call of that action)\n * └── 1..N requestId (framework-minted, ONE per HTTP call)\n *\n * Grep one `actionId` in the logs → every `requestId` it spawned, and every log line of the whole\n * action. Minted and refreshed by the app (a UI concern), carried under `x-webpieces-actionid`.\n *\n * Browser/app-minted ONLY: unlike {@link REQUEST_ID}, the framework transfers and logs it but must\n * NOT auto-mint one server-side. Absent `actionId` ⇒ a non-action flow (system / cron / task), which\n * is the correct signal.\n *\n * - `httpHeader` SET → transferred: copied off the inbound request into context and re-emitted on\n * outbound hops, so the id follows the action across services.\n * - `isLogged` TRUE → emitted as a plain string on every log line of the request.\n */\n static readonly ACTION_ID = ContextKey.untrusted<string>('actionId', 'x-webpieces-actionid');\n\n /**\n * WHO the request is acting as, and WHAT they may do. All three are TRUSTED keys: they are the\n * inputs to authorization decisions, so a reader must be able to tell \"the framework proved\n * this\" from \"the caller typed this\" — see the trust section of the {@link ContextKey} doc.\n *\n * They keep their `httpHeader`, because propagating a verified identity to the next internal\n * service is the point. What makes that safe is not the header being absent, it is WHO is\n * allowed to have set it: an inbound value is held PENDING by\n * `RequestContextHeaders.fillFromRequest` and admitted by `AuthFilter` only on a route that\n * verified its CALLER (`@WpAuthOidc` / `@WpAuthSharedSecret`). On a browser-reachable route\n * (`@WpAuthJwt` / public) the value must match what the authenticator itself derived, or the\n * request is rejected.\n *\n * `provenance` says \"an app-bound JwtHook\" rather than naming one hook, because the framework\n * default ({@link DefaultJwtHook}) stamps NO context entries at all — an app supplies a hook that\n * returns {@link ContextTuple}s for the keys it can vouch for. Any of these three that an app's\n * hook does NOT stamp will be rejected when a caller supplies it, which is the correct and loud\n * outcome: nothing is vouching for it.\n */\n static readonly ORG_ID = ContextKey.trusted<string>(\n 'orgId',\n 'derived from a verified credential by an app-bound JwtHook (a ContextTuple in AuthenticatedCaller)',\n 'x-org-id',\n );\n\n static readonly USER_ID = ContextKey.trusted<string>(\n 'userId',\n 'derived from a verified credential by an app-bound JwtHook (a ContextTuple in AuthenticatedCaller)',\n 'x-user-id',\n );\n\n static readonly USER_ROLES = ContextKey.trusted<string>(\n 'roles',\n 'derived from a verified credential by an app-bound JwtHook (a ContextTuple in AuthenticatedCaller)',\n 'x-webpieces-roles',\n );\n\n /**\n * Turns on test-case recording for this request (Java: x-webpieces-recording).\n * Transferred so recording follows the request across service hops.\n */\n static readonly RECORDING = ContextKey.untrusted<string>('recording', 'x-webpieces-recording');\n\n /**\n * The structured API-call tag ({@link ApiCallInfo}) stamped by {@link LogApiCallImpl} around every\n * outbound (client) / inbound (server) call. It rides the magic context so EVERY log line emitted\n * during the call inherits a filterable `api` object, surfacing in GCP as nested\n * `jsonPayload.api.{side,type,result,path,method}`.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire. Per-hop only: each server/client hop\n * stamps its own tag, so a downstream server records `side:'server'`, never the caller's `side:'client'`.\n * - `isLogged` TRUE → emitted by the logging backends. It carries an OBJECT value, so the backends\n * read it via {@link HeaderRegistry.buildStructuredLogFields} (object-aware); the flat\n * `buildLogFields()` string map deliberately skips it (typeof-string guard).\n */\n static readonly API_CALL_INFO = ContextKey.untrusted<ApiCallInfo>(\n 'api',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * The inbound request's HTTP method and path, stamped ONCE from the {@link HttpRequest} by\n * `RequestContextHeaders.fillFromRequest` (the atomic inbound choke point every transport funnels\n * through). They surface as top-level `jsonPayload.httpMethod` / `jsonPayload.requestPath` so every\n * log line of the request carries them — they used to ride inside {@link ApiCallInfo} (`api.path` /\n * `api.method`) but that coupled a per-CALL logger to the per-REQUEST transport shape.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire: a downstream hop stamps its OWN inbound\n * method/path, never the caller's. Outbound client calls never set these (no inbound path).\n * - `isLogged` TRUE → emitted by the logging backends as plain strings.\n */\n static readonly HTTP_METHOD = ContextKey.untrusted<string>(\n 'httpMethod',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n static readonly REQUEST_PATH = ContextKey.untrusted<string>(\n 'requestPath',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * The routed endpoint's IMPLEMENTATION identity: the concrete controller class name\n * ({@link RouteMetadata.controllerClassName}, e.g. `LoginController`) and the handler method NAME\n * ({@link RouteMetadata.methodName}, e.g. `login`), stamped once per request by {@link LogApiFilter}\n * after route matching so every subsequent log line of the request carries them.\n *\n * These say WHICH CODE ran, which is what you actually grep for — far more useful than the raw\n * `requestPath`. They are the top-level, filterable twin of what previously only lived nested in\n * {@link ApiCallInfo} (`api.method.controllerName` / `api.method.methodName`). The local console\n * formatters render them together as a compact `[Controller.method]` bracket; GCP keeps them as two\n * separate `jsonPayload.controller` / `jsonPayload.method` fields.\n *\n * NOTE: `method` here is the CODE method name (e.g. `login`), NOT the HTTP verb — that is\n * {@link HTTP_METHOD} (`httpMethod`).\n *\n * - `httpHeader` UNDEFINED → NOT transferred: each hop stamps its OWN routed controller/method.\n * - `isLogged` TRUE → emitted by the logging backends as plain strings.\n */\n static readonly CONTROLLER = ContextKey.untrusted<string>(\n 'controller',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n static readonly METHOD = ContextKey.untrusted<string>(\n 'method',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * The base URL ONE outbound call should go to, overriding whatever the client's `ClientConfig`\n * bound at construction — the answer to \"POST our published contract to a URL the PARTNER\n * registered at runtime\" (an `OrganizationWebhook.url` column, an OAuth callback, a per-tenant\n * or self-hosted host). The destination is DATA, not deployment, so it cannot be a svcName and\n * there is nothing to register in {@link ClientRegistry}.\n *\n * ```ts\n * RequestContext.run(() => {\n * RequestContext.putUntrusted(WebpiecesCoreHeaders.OVERRIDE_BASE_URL, webhook.url);\n * return partnerWebhookClient.deliver(envelope);\n * });\n * ```\n *\n * The contract's path IS appended to it (base + `@ApiPath` + `@Endpoint`). When the stored value\n * is a complete url — the usual partner-webhook row — use {@link OVERRIDE_FULL_URL} instead.\n *\n * ONLY a client carrying a `ContextBaseUrlFilter` reads it. Every other client IGNORES this key\n * entirely, which is what stops an ambient value re-pointing every other client in the same\n * fan-out loop at a partner's server. Installing that ONE filter IS the opt-in, so\n * `grep -rn ContextBaseUrlFilter` enumerates every client that can be re-pointed at all.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire, and that is load-bearing. This value\n * names where THIS hop goes. If it travelled, the callee would inherit it and re-point ITS\n * own outbound calls at the same host — one partner-supplied URL turning into an SSRF pivot\n * across the whole call tree. It is per-hop, always.\n * - `isLogged` TRUE → the destination of a partner delivery is exactly what you want in the log\n * line when one fails.\n *\n * UNTRUSTED, necessarily: it comes from a database column a partner edited. That is precisely\n * why re-pointing a request ARMS the framework's SSRF guard, automatically, rather than trusting\n * it — the guard sits beneath every app filter and reads the fact that the request was moved.\n */\n static readonly OVERRIDE_BASE_URL = ContextKey.untrusted<string>(\n 'overrideBaseUrl',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * The COMPLETE url — host, path AND query — ONE outbound call should be sent to, verbatim. The\n * sibling of {@link OVERRIDE_BASE_URL} for the other shape of runtime destination: a partner\n * that does not implement our contract at their own host, but hands us one opaque url to POST\n * to (`https://hooks.partner.example/in/abc?token=xyz`, straight out of a webhook row). With a\n * base-URL override the contract's path would be appended to it and the url would be wrong; with\n * this one the contract's path is NOT appended and nothing is inserted.\n *\n * ```ts\n * RequestContext.run(() => {\n * RequestContext.putUntrusted(WebpiecesCoreHeaders.OVERRIDE_FULL_URL, webhook.url);\n * return partnerWebhookClient.deliver(envelope);\n * });\n * ```\n *\n * ONLY a client carrying a `ContextFullUrlFilter` reads it, for exactly the reasons given on\n * {@link OVERRIDE_BASE_URL}: installing that filter IS the opt-in, and every other client ignores\n * the key. Not transferred over the wire (per-hop, or it becomes an SSRF pivot across the call\n * tree), logged (it is what you want in a failed-delivery log line), and UNTRUSTED (a partner\n * edited it) — which is why re-pointing at it arms the framework's SSRF guard automatically.\n */\n static readonly OVERRIDE_FULL_URL = ContextKey.untrusted<string>(\n 'overrideFullUrl',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * NO CREDENTIAL KEYS LIVE HERE.\n *\n * `authorization` and `x-webpieces-shared-secret` used to be ContextKeys. That made them\n * TRANSFERRED keys, so the inbound transfer copied them off the request into the\n * RequestContext, and every outbound RPC call and enqueued Cloud Task then carried the\n * caller's credential onward — to services that had no business seeing it.\n *\n * A credential belongs to ONE request hop. It is read straight off the {@link HttpRequest}\n * by the framework AuthFilter, and written straight onto the outbound request by the client\n * that mints it (NodeProxyClient, GcpTaskInvoker, InMemoryTaskInvoker). It never enters the\n * magic context, so nothing can propagate it by accident.\n *\n * An app that genuinely wants a credential to travel can still register its own ContextKey for\n * it — but that is now an explicit, visible decision rather than the default.\n */\n\n /**\n * All core context keys (the platform DEFAULT_HEADERS set). A `static readonly` CONSTANT, not a\n * method: it is compile-time data — a list of the key definitions above — read once at the startup\n * composition root (`HeaderRegistry.configure` / `HeaderRegistry.DEFAULT_HEADERS`). A method here\n * would be un-injectable behavior the DI design graph can't reach; a constant is honest data.\n */\n static readonly ALL_HEADERS: AnyContextKey[] = [\n WebpiecesCoreHeaders.REQUEST_ID,\n WebpiecesCoreHeaders.REQUEST_ID_SOURCE,\n WebpiecesCoreHeaders.CLIENT_VERSION,\n WebpiecesCoreHeaders.ACTION_ID,\n WebpiecesCoreHeaders.USER_ID,\n WebpiecesCoreHeaders.ORG_ID,\n WebpiecesCoreHeaders.USER_ROLES,\n WebpiecesCoreHeaders.RECORDING,\n WebpiecesCoreHeaders.API_CALL_INFO,\n WebpiecesCoreHeaders.HTTP_METHOD,\n WebpiecesCoreHeaders.REQUEST_PATH,\n WebpiecesCoreHeaders.CONTROLLER,\n WebpiecesCoreHeaders.METHOD,\n WebpiecesCoreHeaders.OVERRIDE_BASE_URL,\n WebpiecesCoreHeaders.OVERRIDE_FULL_URL,\n ];\n}\n"]}
1
+ {"version":3,"file":"WebpiecesCoreHeaders.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesCoreHeaders.ts"],"names":[],"mappings":";;;AAAA,8CAA0D;AAI1D;;;;;;;;;;;;;;;GAeG;AACH,MAAa,oBAAoB;IAC7B;;;OAGG;IACH,MAAM,CAAU,UAAU,GAAG,uBAAU,CAAC,SAAS,CAAS,WAAW,EAAE,cAAc,CAAC,CAAC;IAEvF;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAU,iBAAiB,GAAG,uBAAU,CAAC,SAAS,CACpD,iBAAiB;IACjB,cAAc,CAAC,SAAS,CAC3B,CAAC;IAEF;;;;;;;;;;;;OAYG;IACH,MAAM,CAAU,cAAc,GAAG,uBAAU,CAAC,SAAS,CACjD,eAAe,EACf,4BAA4B,CAC/B,CAAC;IAEF;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,MAAM,CAAU,SAAS,GAAG,uBAAU,CAAC,SAAS,CAAS,UAAU,EAAE,sBAAsB,CAAC,CAAC;IAE7F;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAU,MAAM,GAAG,uBAAU,CAAC,OAAO,CACvC,OAAO,EACP,oGAAoG,EACpG,UAAU,CACb,CAAC;IAEF,MAAM,CAAU,OAAO,GAAG,uBAAU,CAAC,OAAO,CACxC,QAAQ,EACR,oGAAoG,EACpG,WAAW,CACd,CAAC;IAEF,MAAM,CAAU,UAAU,GAAG,uBAAU,CAAC,OAAO,CAC3C,OAAO,EACP,oGAAoG,EACpG,mBAAmB,CACtB,CAAC;IAEF;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAU,OAAO,GAAG,uBAAU,CAAC,OAAO,CACxC,SAAS,EACT,+HAA+H,EAC/H,cAAc,CACjB,CAAC;IAEF;;;OAGG;IACH,MAAM,CAAU,SAAS,GAAG,uBAAU,CAAC,SAAS,CAAS,WAAW,EAAE,uBAAuB,CAAC,CAAC;IAE/F;;;;;;;;;;;OAWG;IACH,MAAM,CAAU,aAAa,GAAG,uBAAU,CAAC,SAAS,CAChD,KAAK;IACL,cAAc,CAAC,SAAS;IACxB,cAAc,CAAC,KAAK;IACpB,YAAY,CAAC,IAAI,CACpB,CAAC;IAEF;;;;;;;;;;OAUG;IACH,MAAM,CAAU,WAAW,GAAG,uBAAU,CAAC,SAAS,CAC9C,YAAY;IACZ,cAAc,CAAC,SAAS;IACxB,cAAc,CAAC,KAAK;IACpB,YAAY,CAAC,IAAI,CACpB,CAAC;IAEF,MAAM,CAAU,YAAY,GAAG,uBAAU,CAAC,SAAS,CAC/C,aAAa;IACb,cAAc,CAAC,SAAS;IACxB,cAAc,CAAC,KAAK;IACpB,YAAY,CAAC,IAAI,CACpB,CAAC;IAEF;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAU,UAAU,GAAG,uBAAU,CAAC,SAAS,CAC7C,YAAY;IACZ,cAAc,CAAC,SAAS;IACxB,cAAc,CAAC,KAAK;IACpB,YAAY,CAAC,IAAI,CACpB,CAAC;IAEF,MAAM,CAAU,MAAM,GAAG,uBAAU,CAAC,SAAS,CACzC,QAAQ;IACR,cAAc,CAAC,SAAS;IACxB,cAAc,CAAC,KAAK;IACpB,YAAY,CAAC,IAAI,CACpB,CAAC;IAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,MAAM,CAAU,iBAAiB,GAAG,uBAAU,CAAC,SAAS,CACpD,iBAAiB;IACjB,cAAc,CAAC,SAAS;IACxB,cAAc,CAAC,KAAK;IACpB,YAAY,CAAC,IAAI,CACpB,CAAC;IAEF;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAU,iBAAiB,GAAG,uBAAU,CAAC,SAAS,CACpD,iBAAiB;IACjB,cAAc,CAAC,SAAS;IACxB,cAAc,CAAC,KAAK;IACpB,YAAY,CAAC,IAAI,CACpB,CAAC;IAEF;;;;;;;;;;;;;;;OAeG;IAEH;;;;;OAKG;IACH,MAAM,CAAU,WAAW,GAAoB;QAC3C,oBAAoB,CAAC,UAAU;QAC/B,oBAAoB,CAAC,iBAAiB;QACtC,oBAAoB,CAAC,cAAc;QACnC,oBAAoB,CAAC,SAAS;QAC9B,oBAAoB,CAAC,OAAO;QAC5B,oBAAoB,CAAC,MAAM;QAC3B,oBAAoB,CAAC,UAAU;QAC/B,oBAAoB,CAAC,OAAO;QAC5B,oBAAoB,CAAC,SAAS;QAC9B,oBAAoB,CAAC,aAAa;QAClC,oBAAoB,CAAC,WAAW;QAChC,oBAAoB,CAAC,YAAY;QACjC,oBAAoB,CAAC,UAAU;QAC/B,oBAAoB,CAAC,MAAM;QAC3B,oBAAoB,CAAC,iBAAiB;QACtC,oBAAoB,CAAC,iBAAiB;KACzC,CAAC;;AA9TN,oDA+TC","sourcesContent":["import { ContextKey, AnyContextKey } from '../ContextKey';\nimport { ApiCallInfo } from './ApiCallInfo';\nimport { Surface } from './Surface';\n\n/**\n * Core framework context keys — the minimum the WebPieces framework needs to correlate one\n * request across every service it touches, and across every log line each of them writes.\n *\n * ONE id, propagated unchanged. The first service to see a request without an `x-request-id`\n * generates one (RequestContextHeaders.fillFromRequest); every hop copies it onward verbatim. Grep that id and you\n * have the whole call tree. There is no per-hop id and no parent pointer: a chain of ids you must\n * stitch back together buys nothing a single shared id does not already give you.\n *\n * Lives in core-util (browser-safe) so both the http clients and http-server can reference it.\n *\n * Exposed as {@link HeaderRegistry.DEFAULT_HEADERS} — a service opts into these by\n * passing `platformHeaders=true` to `HeaderRegistry.configure(...)`.\n *\n * Each key's `name` is the logical/log name; `httpHeader` is the wire name.\n */\nexport class WebpiecesCoreHeaders {\n /**\n * The id that correlates every hop of one request, and every log line of every hop.\n * Generated by the first service to see a request without one; propagated unchanged after that.\n */\n static readonly REQUEST_ID = ContextKey.untrusted<string>('requestId', 'x-request-id');\n\n /**\n * WHICH SERVICE MINTED {@link REQUEST_ID} — the name from {@link ServiceInfo}, stamped by\n * `RequestContextHeaders.fillFromRequest` ONLY on the branch that generates a new id (i.e. when\n * the inbound request carried no `x-request-id`). It answers the question the id alone cannot:\n * \"this trace starts here — is that right?\" An id appearing with no source means it came from\n * outside; an id sourced by a service that should never be an entry point is a routing bug.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire, and that is the WHOLE POINT. If it\n * travelled, hop 2 would inherit it, hop 3 would inherit it, and \"who started this trace\"\n * would be indistinguishable from \"who passed it along\" — the origin, the one fact this key\n * carries, would be lost. It is absent on every hop that did NOT mint the id, which is exactly\n * the signal: present == I am the origin.\n * - `isLogged` TRUE → emitted as a plain string at `jsonPayload.requestIdSource`.\n */\n static readonly REQUEST_ID_SOURCE = ContextKey.untrusted<string>(\n 'requestIdSource',\n /*httpHeader*/ undefined,\n );\n\n /**\n * The CALLER's build version — so a downstream server's logs record which build of the client\n * called it (surfaces as `jsonPayload.clientVersion`). Distinct from the log line's own `version`\n * (this service's build): `version` answers \"which build wrote this line?\", `clientVersion`\n * answers \"which build asked us to?\".\n *\n * - `httpHeader` SET → transferred over the wire, BUT unlike a normal transferred key it is NOT\n * copied from the context onward. Each hop OVERWRITES it with its OWN `ServiceInfo.getVersion()`\n * as it becomes the client to the next hop (see `buildOutboundHeaders`), so on any given server\n * `clientVersion` is always the IMMEDIATE caller's version, never a stale grand-caller's.\n * - `isLogged` TRUE → the inbound value lands in the context and flows through the normal log\n * field map; no backend change needed.\n */\n static readonly CLIENT_VERSION = ContextKey.untrusted<string>(\n 'clientVersion',\n 'x-webpieces-client-version',\n );\n\n /**\n * A frontend/app-minted correlation id that groups every request triggered by ONE user ACTION.\n *\n * An \"action\" is a single thing the user did in the GUI — a CLICK on a button/link, or TYPING in a\n * field — or a background poller tick: anything that may fan out into MULTIPLE remote calls. That one\n * action fires 1..N browser HTTP calls, each of which gets its own framework-minted {@link REQUEST_ID}\n * (one per HTTP call, shared within that call's server→server subtree). `actionId` sits ABOVE\n * `requestId` and is what stitches those N requests back to the single action that caused them:\n *\n * actionId (app-minted, ONE per user action, rides EVERY call of that action)\n * └── 1..N requestId (framework-minted, ONE per HTTP call)\n *\n * Grep one `actionId` in the logs → every `requestId` it spawned, and every log line of the whole\n * action. Minted and refreshed by the app (a UI concern), carried under `x-webpieces-actionid`.\n *\n * Browser/app-minted ONLY: unlike {@link REQUEST_ID}, the framework transfers and logs it but must\n * NOT auto-mint one server-side. Absent `actionId` ⇒ a non-action flow (system / cron / task), which\n * is the correct signal.\n *\n * - `httpHeader` SET → transferred: copied off the inbound request into context and re-emitted on\n * outbound hops, so the id follows the action across services.\n * - `isLogged` TRUE → emitted as a plain string on every log line of the request.\n */\n static readonly ACTION_ID = ContextKey.untrusted<string>('actionId', 'x-webpieces-actionid');\n\n /**\n * WHO the request is acting as, and WHAT they may do. All three are TRUSTED keys: they are the\n * inputs to authorization decisions, so a reader must be able to tell \"the framework proved\n * this\" from \"the caller typed this\" — see the trust section of the {@link ContextKey} doc.\n *\n * They keep their `httpHeader`, because propagating a verified identity to the next internal\n * service is the point. What makes that safe is not the header being absent, it is WHO is\n * allowed to have set it: an inbound value is held PENDING by\n * `RequestContextHeaders.fillFromRequest` and admitted by `AuthFilter` only on a route that\n * verified its CALLER (`@WpAuthOidc` / `@WpAuthSharedSecret`). On a browser-reachable route\n * (`@WpAuthJwt` / public) the value must match what the authenticator itself derived, or the\n * request is rejected.\n *\n * `provenance` says \"an app-bound JwtHook\" rather than naming one hook, because the framework\n * default ({@link DefaultJwtHook}) stamps NO context entries at all — an app supplies a hook that\n * returns {@link ContextTuple}s for the keys it can vouch for. Any of these three that an app's\n * hook does NOT stamp will be rejected when a caller supplies it, which is the correct and loud\n * outcome: nothing is vouching for it.\n */\n static readonly ORG_ID = ContextKey.trusted<string>(\n 'orgId',\n 'derived from a verified credential by an app-bound JwtHook (a ContextTuple in AuthenticatedCaller)',\n 'x-org-id',\n );\n\n static readonly USER_ID = ContextKey.trusted<string>(\n 'userId',\n 'derived from a verified credential by an app-bound JwtHook (a ContextTuple in AuthenticatedCaller)',\n 'x-user-id',\n );\n\n static readonly USER_ROLES = ContextKey.trusted<string>(\n 'roles',\n 'derived from a verified credential by an app-bound JwtHook (a ContextTuple in AuthenticatedCaller)',\n 'x-webpieces-roles',\n );\n\n /**\n * WHICH KIND OF CALLER is driving this request — `gui`, `llm` or `public-api`. See\n * {@link Surface} for the table and for why it is a property of the CALLER and not of the server.\n *\n * TRUSTED, and that is load-bearing twice over. It is what makes an app safe to branch on it in\n * its OWN authorization logic (\"this endpoint is not exposed to partners\"), and it is what makes\n * the value in {@link SurfaceEndUserStatus} believable: webpieces sets it itself, from the auth\n * mode that matched, never from caller input. A caller that simply SENDS `x-wp-surface` at an\n * endpoint whose auth did not establish one is rejected by `AuthFilter.reconcileWireTrust`, like\n * every other unvouched trusted header.\n *\n * It keeps its `httpHeader` because PROPAGATION is the point: the surface belongs to the ORIGINAL\n * caller, so `GUI -> api1 -> api2` must leave api2 seeing `gui`. An internal `@WpAuthOidc` hop\n * verifies its caller, so the propagated value is admitted verbatim — and the auth filter only\n * ever SETS a surface when none arrived, so an inherited one is never overwritten.\n */\n static readonly SURFACE = ContextKey.trusted<Surface>(\n 'surface',\n 'set by the webpieces AuthFilter from the auth mode that matched the incoming request, or inherited from the edge hop that did',\n 'x-wp-surface',\n );\n\n /**\n * Turns on test-case recording for this request (Java: x-webpieces-recording).\n * Transferred so recording follows the request across service hops.\n */\n static readonly RECORDING = ContextKey.untrusted<string>('recording', 'x-webpieces-recording');\n\n /**\n * The structured API-call tag ({@link ApiCallInfo}) stamped by {@link LogApiCallImpl} around every\n * outbound (client) / inbound (server) call. It rides the magic context so EVERY log line emitted\n * during the call inherits a filterable `api` object, surfacing in GCP as nested\n * `jsonPayload.api.{side,type,result,path,method}`.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire. Per-hop only: each server/client hop\n * stamps its own tag, so a downstream server records `side:'server'`, never the caller's `side:'client'`.\n * - `isLogged` TRUE → emitted by the logging backends. It carries an OBJECT value, so the backends\n * read it via {@link HeaderRegistry.buildStructuredLogFields} (object-aware); the flat\n * `buildLogFields()` string map deliberately skips it (typeof-string guard).\n */\n static readonly API_CALL_INFO = ContextKey.untrusted<ApiCallInfo>(\n 'api',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * The inbound request's HTTP method and path, stamped ONCE from the {@link HttpRequest} by\n * `RequestContextHeaders.fillFromRequest` (the atomic inbound choke point every transport funnels\n * through). They surface as top-level `jsonPayload.httpMethod` / `jsonPayload.requestPath` so every\n * log line of the request carries them — they used to ride inside {@link ApiCallInfo} (`api.path` /\n * `api.method`) but that coupled a per-CALL logger to the per-REQUEST transport shape.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire: a downstream hop stamps its OWN inbound\n * method/path, never the caller's. Outbound client calls never set these (no inbound path).\n * - `isLogged` TRUE → emitted by the logging backends as plain strings.\n */\n static readonly HTTP_METHOD = ContextKey.untrusted<string>(\n 'httpMethod',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n static readonly REQUEST_PATH = ContextKey.untrusted<string>(\n 'requestPath',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * The routed endpoint's IMPLEMENTATION identity: the concrete controller class name\n * ({@link RouteMetadata.controllerClassName}, e.g. `LoginController`) and the handler method NAME\n * ({@link RouteMetadata.methodName}, e.g. `login`), stamped once per request by {@link LogApiFilter}\n * after route matching so every subsequent log line of the request carries them.\n *\n * These say WHICH CODE ran, which is what you actually grep for — far more useful than the raw\n * `requestPath`. They are the top-level, filterable twin of what previously only lived nested in\n * {@link ApiCallInfo} (`api.method.controllerName` / `api.method.methodName`). The local console\n * formatters render them together as a compact `[Controller.method]` bracket; GCP keeps them as two\n * separate `jsonPayload.controller` / `jsonPayload.method` fields.\n *\n * NOTE: `method` here is the CODE method name (e.g. `login`), NOT the HTTP verb — that is\n * {@link HTTP_METHOD} (`httpMethod`).\n *\n * - `httpHeader` UNDEFINED → NOT transferred: each hop stamps its OWN routed controller/method.\n * - `isLogged` TRUE → emitted by the logging backends as plain strings.\n */\n static readonly CONTROLLER = ContextKey.untrusted<string>(\n 'controller',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n static readonly METHOD = ContextKey.untrusted<string>(\n 'method',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * The base URL ONE outbound call should go to, overriding whatever the client's `ClientConfig`\n * bound at construction — the answer to \"POST our published contract to a URL the PARTNER\n * registered at runtime\" (an `OrganizationWebhook.url` column, an OAuth callback, a per-tenant\n * or self-hosted host). The destination is DATA, not deployment, so it cannot be a svcName and\n * there is nothing to register in {@link ClientRegistry}.\n *\n * ```ts\n * RequestContext.run(() => {\n * RequestContext.putUntrusted(WebpiecesCoreHeaders.OVERRIDE_BASE_URL, webhook.url);\n * return partnerWebhookClient.deliver(envelope);\n * });\n * ```\n *\n * The contract's path IS appended to it (base + `@ApiPath` + `@Endpoint`). When the stored value\n * is a complete url — the usual partner-webhook row — use {@link OVERRIDE_FULL_URL} instead.\n *\n * ONLY a client carrying a `ContextBaseUrlFilter` reads it. Every other client IGNORES this key\n * entirely, which is what stops an ambient value re-pointing every other client in the same\n * fan-out loop at a partner's server. Installing that ONE filter IS the opt-in, so\n * `grep -rn ContextBaseUrlFilter` enumerates every client that can be re-pointed at all.\n *\n * - `httpHeader` UNDEFINED → NOT transferred over the wire, and that is load-bearing. This value\n * names where THIS hop goes. If it travelled, the callee would inherit it and re-point ITS\n * own outbound calls at the same host — one partner-supplied URL turning into an SSRF pivot\n * across the whole call tree. It is per-hop, always.\n * - `isLogged` TRUE → the destination of a partner delivery is exactly what you want in the log\n * line when one fails.\n *\n * UNTRUSTED, necessarily: it comes from a database column a partner edited. That is precisely\n * why re-pointing a request ARMS the framework's SSRF guard, automatically, rather than trusting\n * it — the guard sits beneath every app filter and reads the fact that the request was moved.\n */\n static readonly OVERRIDE_BASE_URL = ContextKey.untrusted<string>(\n 'overrideBaseUrl',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * The COMPLETE url — host, path AND query — ONE outbound call should be sent to, verbatim. The\n * sibling of {@link OVERRIDE_BASE_URL} for the other shape of runtime destination: a partner\n * that does not implement our contract at their own host, but hands us one opaque url to POST\n * to (`https://hooks.partner.example/in/abc?token=xyz`, straight out of a webhook row). With a\n * base-URL override the contract's path would be appended to it and the url would be wrong; with\n * this one the contract's path is NOT appended and nothing is inserted.\n *\n * ```ts\n * RequestContext.run(() => {\n * RequestContext.putUntrusted(WebpiecesCoreHeaders.OVERRIDE_FULL_URL, webhook.url);\n * return partnerWebhookClient.deliver(envelope);\n * });\n * ```\n *\n * ONLY a client carrying a `ContextFullUrlFilter` reads it, for exactly the reasons given on\n * {@link OVERRIDE_BASE_URL}: installing that filter IS the opt-in, and every other client ignores\n * the key. Not transferred over the wire (per-hop, or it becomes an SSRF pivot across the call\n * tree), logged (it is what you want in a failed-delivery log line), and UNTRUSTED (a partner\n * edited it) — which is why re-pointing at it arms the framework's SSRF guard automatically.\n */\n static readonly OVERRIDE_FULL_URL = ContextKey.untrusted<string>(\n 'overrideFullUrl',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ true,\n );\n\n /**\n * NO CREDENTIAL KEYS LIVE HERE.\n *\n * `authorization` and `x-webpieces-shared-secret` used to be ContextKeys. That made them\n * TRANSFERRED keys, so the inbound transfer copied them off the request into the\n * RequestContext, and every outbound RPC call and enqueued Cloud Task then carried the\n * caller's credential onward — to services that had no business seeing it.\n *\n * A credential belongs to ONE request hop. It is read straight off the {@link HttpRequest}\n * by the framework AuthFilter, and written straight onto the outbound request by the client\n * that mints it (NodeProxyClient, GcpTaskInvoker, InMemoryTaskInvoker). It never enters the\n * magic context, so nothing can propagate it by accident.\n *\n * An app that genuinely wants a credential to travel can still register its own ContextKey for\n * it — but that is now an explicit, visible decision rather than the default.\n */\n\n /**\n * All core context keys (the platform DEFAULT_HEADERS set). A `static readonly` CONSTANT, not a\n * method: it is compile-time data — a list of the key definitions above — read once at the startup\n * composition root (`HeaderRegistry.configure` / `HeaderRegistry.DEFAULT_HEADERS`). A method here\n * would be un-injectable behavior the DI design graph can't reach; a constant is honest data.\n */\n static readonly ALL_HEADERS: AnyContextKey[] = [\n WebpiecesCoreHeaders.REQUEST_ID,\n WebpiecesCoreHeaders.REQUEST_ID_SOURCE,\n WebpiecesCoreHeaders.CLIENT_VERSION,\n WebpiecesCoreHeaders.ACTION_ID,\n WebpiecesCoreHeaders.USER_ID,\n WebpiecesCoreHeaders.ORG_ID,\n WebpiecesCoreHeaders.USER_ROLES,\n WebpiecesCoreHeaders.SURFACE,\n WebpiecesCoreHeaders.RECORDING,\n WebpiecesCoreHeaders.API_CALL_INFO,\n WebpiecesCoreHeaders.HTTP_METHOD,\n WebpiecesCoreHeaders.REQUEST_PATH,\n WebpiecesCoreHeaders.CONTROLLER,\n WebpiecesCoreHeaders.METHOD,\n WebpiecesCoreHeaders.OVERRIDE_BASE_URL,\n WebpiecesCoreHeaders.OVERRIDE_FULL_URL,\n ];\n}\n"]}
@@ -0,0 +1,70 @@
1
+ import { ErrorTranslator } from './ErrorTranslator';
2
+ import { HttpResponseDto } from './HttpResponseDto';
3
+ /**
4
+ * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by
5
+ * every node client and by the browser bundle alike.
6
+ *
7
+ * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is
8
+ * what an app's own translator calls to DECLINE an error:
9
+ *
10
+ * ```ts
11
+ * private readonly fallback = new WebpiecesDefaultErrorTranslator();
12
+ * ```
13
+ *
14
+ * so "no translator registered" and "a translator that does not claim this error" produce
15
+ * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is
16
+ * the package every API contract already depends on and the only one both a server and a browser
17
+ * bundle can reach. Both halves needed nothing else: the outbound half wants
18
+ * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants
19
+ * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.
20
+ *
21
+ * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing
22
+ * your own costs nothing and reads better at a delegation site.
23
+ */
24
+ export declare class WebpiecesDefaultErrorTranslator implements ErrorTranslator {
25
+ private readonly boundary;
26
+ private readonly genericMessages;
27
+ /**
28
+ * SERVER: an exception in, the exact HTTP response out.
29
+ *
30
+ * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared
31
+ * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as
32
+ * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload
33
+ * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and
34
+ * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw
35
+ * error; unknown throws and caller-local failures come out as a 500 implementation failure
36
+ * because that is what the payload says.
37
+ *
38
+ * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that
39
+ * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request
40
+ * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —
41
+ * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any
42
+ * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator
43
+ * responsible for repeating its router's mode, and repeating it wrongly was silent.
44
+ */
45
+ toWire(error: Error): HttpResponseDto;
46
+ /**
47
+ * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.
48
+ *
49
+ * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is
50
+ * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller
51
+ * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply
52
+ * stop — a failure response can never fall through to a typed caller.
53
+ *
54
+ * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: 4xx is MY bug, 5xx
55
+ * is THEIRS, an `ApiDependencyError` is already attributed and passes through, 266 is the end
56
+ * user's own answer.
57
+ */
58
+ fromWire(response: HttpResponseDto): void;
59
+ /** The reason phrase webpieces writes beside a status it chose itself. */
60
+ genericMessage(code: number): string;
61
+ /**
62
+ * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.
63
+ * The status is only believed when it AGREES with the kind the body claims — a body saying
64
+ * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.
65
+ */
66
+ private decodeBody;
67
+ private fallbackMessage;
68
+ }
69
+ /** Process-wide built-in instance — stateless, so one shared instance is enough. */
70
+ export declare const WEBPIECES_DEFAULT_ERROR_TRANSLATOR: WebpiecesDefaultErrorTranslator;
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WEBPIECES_DEFAULT_ERROR_TRANSLATOR = exports.WebpiecesDefaultErrorTranslator = void 0;
4
+ const ApiErrorBoundary_1 = require("../errors/ApiErrorBoundary");
5
+ const ApiErrorCodec_1 = require("../errors/ApiErrorCodec");
6
+ const ApiError_1 = require("../errors/ApiError");
7
+ const ReceivedApiErrorRule_1 = require("../errors/ReceivedApiErrorRule");
8
+ const ApiErrorHttpStatus_1 = require("./ApiErrorHttpStatus");
9
+ const HttpResponseDto_1 = require("./HttpResponseDto");
10
+ /**
11
+ * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by
12
+ * every node client and by the browser bundle alike.
13
+ *
14
+ * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is
15
+ * what an app's own translator calls to DECLINE an error:
16
+ *
17
+ * ```ts
18
+ * private readonly fallback = new WebpiecesDefaultErrorTranslator();
19
+ * ```
20
+ *
21
+ * so "no translator registered" and "a translator that does not claim this error" produce
22
+ * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is
23
+ * the package every API contract already depends on and the only one both a server and a browser
24
+ * bundle can reach. Both halves needed nothing else: the outbound half wants
25
+ * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants
26
+ * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.
27
+ *
28
+ * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing
29
+ * your own costs nothing and reads better at a delegation site.
30
+ */
31
+ class WebpiecesDefaultErrorTranslator {
32
+ boundary = new ApiErrorBoundary_1.ApiErrorBoundary();
33
+ genericMessages = new Map([
34
+ [266, 'End User Error'],
35
+ [400, 'Bad Request'],
36
+ [401, 'Unauthorized'],
37
+ [403, 'Forbidden'],
38
+ [404, 'Not Found'],
39
+ [408, 'Request Timeout'],
40
+ [409, 'Conflict'],
41
+ [412, 'Precondition Failed'],
42
+ [415, 'Unsupported Media Type'],
43
+ [422, 'Unprocessable Content'],
44
+ [429, 'Too Many Requests'],
45
+ [500, 'Internal Server Error'],
46
+ [501, 'Not Implemented'],
47
+ [502, 'Bad Gateway'],
48
+ [503, 'Service Unavailable'],
49
+ [504, 'Gateway Timeout'],
50
+ ]);
51
+ /**
52
+ * SERVER: an exception in, the exact HTTP response out.
53
+ *
54
+ * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared
55
+ * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as
56
+ * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload
57
+ * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and
58
+ * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw
59
+ * error; unknown throws and caller-local failures come out as a 500 implementation failure
60
+ * because that is what the payload says.
61
+ *
62
+ * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that
63
+ * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request
64
+ * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —
65
+ * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any
66
+ * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator
67
+ * responsible for repeating its router's mode, and repeating it wrongly was silent.
68
+ */
69
+ toWire(error) {
70
+ const payload = this.boundary.encode(error);
71
+ // The boundary never publishes kind 'connection' — that is the whole first rule above — so
72
+ // this is the published subset ApiErrorHttpStatus.codeFor accepts.
73
+ const kind = payload.kind;
74
+ const status = ApiErrorHttpStatus_1.ApiErrorHttpStatus.codeFor(kind, payload.statusCode);
75
+ const headers = payload.retryAfterSeconds !== undefined
76
+ ? [new HttpResponseDto_1.HttpHeader('retry-after', String(payload.retryAfterSeconds))]
77
+ : [];
78
+ return new HttpResponseDto_1.HttpResponseDto(new HttpResponseDto_1.HttpResponseStatus(status, this.genericMessage(status)), headers, payload);
79
+ }
80
+ /**
81
+ * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.
82
+ *
83
+ * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is
84
+ * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller
85
+ * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply
86
+ * stop — a failure response can never fall through to a typed caller.
87
+ *
88
+ * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: 4xx is MY bug, 5xx
89
+ * is THEIRS, an `ApiDependencyError` is already attributed and passes through, 266 is the end
90
+ * user's own answer.
91
+ */
92
+ fromWire(response) {
93
+ const code = response.status.code;
94
+ if (code >= 200 && code < 300 && code !== 266) {
95
+ return;
96
+ }
97
+ const decoded = this.decodeBody(response);
98
+ const message = decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);
99
+ throw ReceivedApiErrorRule_1.ReceivedApiErrorRule.adapt(code, message, decoded);
100
+ }
101
+ /** The reason phrase webpieces writes beside a status it chose itself. */
102
+ genericMessage(code) {
103
+ return this.genericMessages.get(code) ?? 'Request Failed';
104
+ }
105
+ /**
106
+ * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.
107
+ * The status is only believed when it AGREES with the kind the body claims — a body saying
108
+ * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.
109
+ */
110
+ decodeBody(response) {
111
+ if (!ApiErrorCodec_1.ApiErrorCodec.isPayload(response.body)) {
112
+ return undefined;
113
+ }
114
+ const decoded = ApiErrorCodec_1.ApiErrorCodec.decode(response.body);
115
+ if (!ApiErrorHttpStatus_1.ApiErrorHttpStatus.hasCode(decoded)) {
116
+ return undefined;
117
+ }
118
+ // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.
119
+ const statusCode = decoded instanceof ApiError_1.ApiCodedError ? decoded.statusCode : undefined;
120
+ const declared = ApiErrorHttpStatus_1.ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);
121
+ return declared === response.status.code ? decoded : undefined;
122
+ }
123
+ // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them
124
+ fallbackMessage(body, reason) {
125
+ if (typeof body === 'object' && body !== null) {
126
+ const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;
127
+ if (typeof message === 'string' && message.length > 0)
128
+ return message.slice(0, 4096);
129
+ }
130
+ return reason || 'Request Failed';
131
+ }
132
+ }
133
+ exports.WebpiecesDefaultErrorTranslator = WebpiecesDefaultErrorTranslator;
134
+ /** Process-wide built-in instance — stateless, so one shared instance is enough. */
135
+ exports.WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();
136
+ //# sourceMappingURL=WebpiecesDefaultErrorTranslator.js.map