@webpieces/core-util 0.4.879 → 0.4.880

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 (51) hide show
  1. package/package.json +1 -1
  2. package/src/errors/ApiError.d.ts +38 -0
  3. package/src/errors/ApiError.js +46 -1
  4. package/src/errors/ApiError.js.map +1 -1
  5. package/src/errors/ApiErrorCodec.js +9 -2
  6. package/src/errors/ApiErrorCodec.js.map +1 -1
  7. package/src/errors/ClientRole.d.ts +27 -0
  8. package/src/errors/ClientRole.js +32 -0
  9. package/src/errors/ClientRole.js.map +1 -0
  10. package/src/errors/EndUserErrorCode.d.ts +22 -0
  11. package/src/errors/EndUserErrorCode.js +38 -0
  12. package/src/errors/EndUserErrorCode.js.map +1 -0
  13. package/src/errors/EndUserErrorRegistry.d.ts +61 -0
  14. package/src/errors/EndUserErrorRegistry.js +103 -0
  15. package/src/errors/EndUserErrorRegistry.js.map +1 -0
  16. package/src/errors/EndUserErrors.d.ts +28 -0
  17. package/src/errors/EndUserErrors.js +59 -0
  18. package/src/errors/EndUserErrors.js.map +1 -0
  19. package/src/errors/ReceivedApiErrorRule.d.ts +33 -3
  20. package/src/errors/ReceivedApiErrorRule.js +47 -8
  21. package/src/errors/ReceivedApiErrorRule.js.map +1 -1
  22. package/src/errors/index.d.ts +6 -1
  23. package/src/errors/index.js +12 -1
  24. package/src/errors/index.js.map +1 -1
  25. package/src/http/ContextMgr.d.ts +1 -1
  26. package/src/http/ContextMgr.js +1 -1
  27. package/src/http/ContextMgr.js.map +1 -1
  28. package/src/http/ErrorTranslator.d.ts +10 -4
  29. package/src/http/ErrorTranslator.js.map +1 -1
  30. package/src/http/HttpEndpointOptions.d.ts +8 -0
  31. package/src/http/HttpEndpointOptions.js.map +1 -1
  32. package/src/http/StreamErrorControl.d.ts +3 -1
  33. package/src/http/StreamErrorControl.js +4 -3
  34. package/src/http/StreamErrorControl.js.map +1 -1
  35. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +10 -2
  36. package/src/http/WebpiecesDefaultErrorTranslator.js +11 -3
  37. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
  38. package/src/index.d.ts +2 -2
  39. package/src/index.js +8 -1
  40. package/src/index.js.map +1 -1
  41. package/src/ipc/IpcErrorTranslator.d.ts +6 -2
  42. package/src/ipc/IpcErrorTranslator.js.map +1 -1
  43. package/src/ipc/IpcRegistry.d.ts +4 -1
  44. package/src/ipc/IpcRegistry.js +5 -3
  45. package/src/ipc/IpcRegistry.js.map +1 -1
  46. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.d.ts +6 -2
  47. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js +6 -3
  48. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js.map +1 -1
  49. package/src/ipc/index.d.ts +1 -0
  50. package/src/ipc/index.js +3 -1
  51. package/src/ipc/index.js.map +1 -1
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EndUserErrorRegistry = void 0;
4
+ const ApiError_1 = require("./ApiError");
5
+ const EndUserErrorCode_1 = require("./EndUserErrorCode");
6
+ const EndUserErrors_1 = require("./EndUserErrors");
7
+ /**
8
+ * errorCode -> {@link ApiEndUserError} SUBCLASS, so the receiving side of a 266 can `instanceof` WHICH
9
+ * mistake it was (send the person to the Terms page, treat a missing item as "it was deleted")
10
+ * instead of string-matching `errorCode` at every catch site.
11
+ *
12
+ * `ApiErrorCodec` consults it for every decoded `end-user` payload, which is the one decode HTTP
13
+ * (`WebpiecesDefaultErrorTranslator.fromWire`), IPC and JSONL streams all share, so a registration
14
+ * covers every protocol at once. Matching is on the `<code>` half of the `<code>[:<detail>]` grammar.
15
+ * An UNKNOWN code decodes to the base `ApiEndUserError` with `errorCode` and `edgeHttpStatus` intact:
16
+ * forward compatible, an old client receiving a new code still shows the message.
17
+ *
18
+ * REGISTER ONCE AT STARTUP, in EVERY process that may receive the error: the browser bundle, the
19
+ * Expo shell and every server. A server needs it too, because a server that receives a 266 from a
20
+ * peer rethrows the decoded error to its own caller, and only a decoded subclass is still that
21
+ * subclass when the next hop catches it.
22
+ *
23
+ * ```ts
24
+ * export class TermsNotAcceptedError extends ApiEndUserError {
25
+ * static readonly CODE = 'terms-not-accepted';
26
+ * constructor(message: string, detail?: string, cause?: Error) {
27
+ * super(message, EndUserErrorCode.format(TermsNotAcceptedError.CODE, detail), undefined, cause);
28
+ * }
29
+ * }
30
+ *
31
+ * // startup, ONCE per process (browser AND server)
32
+ * EndUserErrorRegistry.register(TermsNotAcceptedError.CODE,
33
+ * (message: string, detail: string | undefined) => new TermsNotAcceptedError(message, detail));
34
+ * ```
35
+ *
36
+ * The webpieces generics ({@link ApiEndUserNotFoundError}, {@link ApiEndUserForbiddenError},
37
+ * {@link ApiEndUserBadRequestError}) are PRE-REGISTERED here, so they decode typed with no setup.
38
+ * App-specific subclasses stay in the app.
39
+ *
40
+ * Process-global and static, like `ClientRegistry` / `HeaderRegistry`: the codec that consults it is
41
+ * a static, protocol-neutral decoder reached from the browser, node and IPC alike, and a registration
42
+ * is a fact about the PROCESS's error taxonomy, not about any one client.
43
+ */
44
+ class EndUserErrorRegistry {
45
+ static factories = EndUserErrorRegistry.builtIns();
46
+ /**
47
+ * Register the subclass for `code`. `code` is the bare `<code>` (no `:`), normally the subclass's
48
+ * static `CODE`. Registering a code twice THROWS: two classes claiming one code is a collision
49
+ * that would otherwise decode to whichever registered last, depending on startup order.
50
+ */
51
+ // webpieces-disable no-function-outside-class -- static global singleton (like ClientRegistry/HeaderRegistry); populated once at startup, never DI-injected
52
+ static register(code, factory) {
53
+ if (code.length === 0 || code.includes(EndUserErrorCode_1.EndUserErrorCode.SEPARATOR)) {
54
+ throw new ApiError_1.ApiImplementationError(`EndUserErrorRegistry.register('${code}'): a code must be non-empty and must not ` +
55
+ `contain '${EndUserErrorCode_1.EndUserErrorCode.SEPARATOR}'. The errorCode grammar is ` +
56
+ `<code>[:<detail>]; register the bare <code>, normally the subclass's static CODE.`);
57
+ }
58
+ if (EndUserErrorRegistry.factories.has(code)) {
59
+ throw new ApiError_1.ApiImplementationError(`EndUserErrorRegistry.register('${code}'): that code is already registered. Two ` +
60
+ `ApiEndUserError subclasses cannot share one errorCode; rename one of them. ` +
61
+ `(Tests: call EndUserErrorRegistry.resetForTests() between specs.)`);
62
+ }
63
+ EndUserErrorRegistry.factories.set(code, factory);
64
+ }
65
+ /**
66
+ * The decoded end-user error for a received payload: the registered subclass for `errorCode`'s
67
+ * `<code>`, else the base {@link ApiEndUserError} carrying everything the wire sent.
68
+ */
69
+ // webpieces-disable no-function-outside-class -- static global singleton (like ClientRegistry/HeaderRegistry); populated once at startup, never DI-injected
70
+ static create(message, errorCode, edgeHttpStatus) {
71
+ if (errorCode === undefined)
72
+ return new ApiError_1.ApiEndUserError(message, errorCode, edgeHttpStatus);
73
+ const factory = EndUserErrorRegistry.factories.get(EndUserErrorCode_1.EndUserErrorCode.codeOf(errorCode));
74
+ if (factory === undefined)
75
+ return new ApiError_1.ApiEndUserError(message, errorCode, edgeHttpStatus);
76
+ return factory(message, EndUserErrorCode_1.EndUserErrorCode.detailOf(errorCode));
77
+ }
78
+ /** Back to a fresh process: the webpieces built-ins only. For TESTS, so specs cannot leak. */
79
+ // webpieces-disable no-function-outside-class -- static global singleton (like ClientRegistry/HeaderRegistry); populated once at startup, never DI-injected
80
+ static resetForTests() {
81
+ EndUserErrorRegistry.factories.clear();
82
+ EndUserErrorRegistry.builtIns().forEach((factory, code) => EndUserErrorRegistry.factories.set(code, factory));
83
+ }
84
+ // webpieces-disable no-function-outside-class -- the built-in table, shared by the initializer and resetForTests
85
+ static builtIns() {
86
+ return new Map([
87
+ [
88
+ EndUserErrors_1.ApiEndUserNotFoundError.CODE,
89
+ (message, detail) => new EndUserErrors_1.ApiEndUserNotFoundError(message, detail),
90
+ ],
91
+ [
92
+ EndUserErrors_1.ApiEndUserForbiddenError.CODE,
93
+ (message, detail) => new EndUserErrors_1.ApiEndUserForbiddenError(message, detail),
94
+ ],
95
+ [
96
+ EndUserErrors_1.ApiEndUserBadRequestError.CODE,
97
+ (message, detail) => new EndUserErrors_1.ApiEndUserBadRequestError(message, detail),
98
+ ],
99
+ ]);
100
+ }
101
+ }
102
+ exports.EndUserErrorRegistry = EndUserErrorRegistry;
103
+ //# sourceMappingURL=EndUserErrorRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EndUserErrorRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/EndUserErrorRegistry.ts"],"names":[],"mappings":";;;AAAA,yCAAqF;AACrF,yDAAsD;AACtD,mDAIyB;AASzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAa,oBAAoB;IACrB,MAAM,CAAU,SAAS,GAAG,oBAAoB,CAAC,QAAQ,EAAE,CAAC;IAEpE;;;;OAIG;IACH,4JAA4J;IAC5J,MAAM,CAAC,QAAQ,CAAC,IAAY,EAAE,OAA4B;QACtD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,mCAAgB,CAAC,SAAS,CAAC,EAAE,CAAC;YACjE,MAAM,IAAI,iCAAsB,CAC5B,kCAAkC,IAAI,4CAA4C;gBAC9E,YAAY,mCAAgB,CAAC,SAAS,8BAA8B;gBACpE,mFAAmF,CAC1F,CAAC;QACN,CAAC;QACD,IAAI,oBAAoB,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,iCAAsB,CAC5B,kCAAkC,IAAI,2CAA2C;gBAC7E,6EAA6E;gBAC7E,mEAAmE,CAC1E,CAAC;QACN,CAAC;QACD,oBAAoB,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAED;;;OAGG;IACH,4JAA4J;IAC5J,MAAM,CAAC,MAAM,CACT,OAAe,EACf,SAAkB,EAClB,cAA+B;QAE/B,IAAI,SAAS,KAAK,SAAS;YAAE,OAAO,IAAI,0BAAe,CAAC,OAAO,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QAC5F,MAAM,OAAO,GAAG,oBAAoB,CAAC,SAAS,CAAC,GAAG,CAAC,mCAAgB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QACvF,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,IAAI,0BAAe,CAAC,OAAO,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QAC1F,OAAO,OAAO,CAAC,OAAO,EAAE,mCAAgB,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,8FAA8F;IAC9F,4JAA4J;IAC5J,MAAM,CAAC,aAAa;QAChB,oBAAoB,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACvC,oBAAoB,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,OAA4B,EAAE,IAAY,EAAE,EAAE,CACnF,oBAAoB,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CACpD,CAAC;IACN,CAAC;IAED,iHAAiH;IACzG,MAAM,CAAC,QAAQ;QACnB,OAAO,IAAI,GAAG,CAA8B;YACxC;gBACI,uCAAuB,CAAC,IAAI;gBAC5B,CAAC,OAAe,EAAE,MAA0B,EAAE,EAAE,CAC5C,IAAI,uCAAuB,CAAC,OAAO,EAAE,MAAM,CAAC;aACnD;YACD;gBACI,wCAAwB,CAAC,IAAI;gBAC7B,CAAC,OAAe,EAAE,MAA0B,EAAE,EAAE,CAC5C,IAAI,wCAAwB,CAAC,OAAO,EAAE,MAAM,CAAC;aACpD;YACD;gBACI,yCAAyB,CAAC,IAAI;gBAC9B,CAAC,OAAe,EAAE,MAA0B,EAAE,EAAE,CAC5C,IAAI,yCAAyB,CAAC,OAAO,EAAE,MAAM,CAAC;aACrD;SACJ,CAAC,CAAC;IACP,CAAC;;AAvEL,oDAwEC","sourcesContent":["import { ApiEndUserError, ApiImplementationError, EdgeHttpStatus } from './ApiError';\nimport { EndUserErrorCode } from './EndUserErrorCode';\nimport {\n ApiEndUserBadRequestError,\n ApiEndUserForbiddenError,\n ApiEndUserNotFoundError,\n} from './EndUserErrors';\n\n/**\n * Rebuilds one registered {@link ApiEndUserError} subclass from what crossed the wire. It receives\n * the published message and the `<detail>` half of `errorCode` (see {@link EndUserErrorCode}); the\n * subclass's constructor fixes the `<code>` half itself.\n */\nexport type EndUserErrorFactory = (message: string, detail: string | undefined) => ApiEndUserError;\n\n/**\n * errorCode -> {@link ApiEndUserError} SUBCLASS, so the receiving side of a 266 can `instanceof` WHICH\n * mistake it was (send the person to the Terms page, treat a missing item as \"it was deleted\")\n * instead of string-matching `errorCode` at every catch site.\n *\n * `ApiErrorCodec` consults it for every decoded `end-user` payload, which is the one decode HTTP\n * (`WebpiecesDefaultErrorTranslator.fromWire`), IPC and JSONL streams all share, so a registration\n * covers every protocol at once. Matching is on the `<code>` half of the `<code>[:<detail>]` grammar.\n * An UNKNOWN code decodes to the base `ApiEndUserError` with `errorCode` and `edgeHttpStatus` intact:\n * forward compatible, an old client receiving a new code still shows the message.\n *\n * REGISTER ONCE AT STARTUP, in EVERY process that may receive the error: the browser bundle, the\n * Expo shell and every server. A server needs it too, because a server that receives a 266 from a\n * peer rethrows the decoded error to its own caller, and only a decoded subclass is still that\n * subclass when the next hop catches it.\n *\n * ```ts\n * export class TermsNotAcceptedError extends ApiEndUserError {\n * static readonly CODE = 'terms-not-accepted';\n * constructor(message: string, detail?: string, cause?: Error) {\n * super(message, EndUserErrorCode.format(TermsNotAcceptedError.CODE, detail), undefined, cause);\n * }\n * }\n *\n * // startup, ONCE per process (browser AND server)\n * EndUserErrorRegistry.register(TermsNotAcceptedError.CODE,\n * (message: string, detail: string | undefined) => new TermsNotAcceptedError(message, detail));\n * ```\n *\n * The webpieces generics ({@link ApiEndUserNotFoundError}, {@link ApiEndUserForbiddenError},\n * {@link ApiEndUserBadRequestError}) are PRE-REGISTERED here, so they decode typed with no setup.\n * App-specific subclasses stay in the app.\n *\n * Process-global and static, like `ClientRegistry` / `HeaderRegistry`: the codec that consults it is\n * a static, protocol-neutral decoder reached from the browser, node and IPC alike, and a registration\n * is a fact about the PROCESS's error taxonomy, not about any one client.\n */\nexport class EndUserErrorRegistry {\n private static readonly factories = EndUserErrorRegistry.builtIns();\n\n /**\n * Register the subclass for `code`. `code` is the bare `<code>` (no `:`), normally the subclass's\n * static `CODE`. Registering a code twice THROWS: two classes claiming one code is a collision\n * that would otherwise decode to whichever registered last, depending on startup order.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like ClientRegistry/HeaderRegistry); populated once at startup, never DI-injected\n static register(code: string, factory: EndUserErrorFactory): void {\n if (code.length === 0 || code.includes(EndUserErrorCode.SEPARATOR)) {\n throw new ApiImplementationError(\n `EndUserErrorRegistry.register('${code}'): a code must be non-empty and must not ` +\n `contain '${EndUserErrorCode.SEPARATOR}'. The errorCode grammar is ` +\n `<code>[:<detail>]; register the bare <code>, normally the subclass's static CODE.`,\n );\n }\n if (EndUserErrorRegistry.factories.has(code)) {\n throw new ApiImplementationError(\n `EndUserErrorRegistry.register('${code}'): that code is already registered. Two ` +\n `ApiEndUserError subclasses cannot share one errorCode; rename one of them. ` +\n `(Tests: call EndUserErrorRegistry.resetForTests() between specs.)`,\n );\n }\n EndUserErrorRegistry.factories.set(code, factory);\n }\n\n /**\n * The decoded end-user error for a received payload: the registered subclass for `errorCode`'s\n * `<code>`, else the base {@link ApiEndUserError} carrying everything the wire sent.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like ClientRegistry/HeaderRegistry); populated once at startup, never DI-injected\n static create(\n message: string,\n errorCode?: string,\n edgeHttpStatus?: EdgeHttpStatus,\n ): ApiEndUserError {\n if (errorCode === undefined) return new ApiEndUserError(message, errorCode, edgeHttpStatus);\n const factory = EndUserErrorRegistry.factories.get(EndUserErrorCode.codeOf(errorCode));\n if (factory === undefined) return new ApiEndUserError(message, errorCode, edgeHttpStatus);\n return factory(message, EndUserErrorCode.detailOf(errorCode));\n }\n\n /** Back to a fresh process: the webpieces built-ins only. For TESTS, so specs cannot leak. */\n // webpieces-disable no-function-outside-class -- static global singleton (like ClientRegistry/HeaderRegistry); populated once at startup, never DI-injected\n static resetForTests(): void {\n EndUserErrorRegistry.factories.clear();\n EndUserErrorRegistry.builtIns().forEach((factory: EndUserErrorFactory, code: string) =>\n EndUserErrorRegistry.factories.set(code, factory),\n );\n }\n\n // webpieces-disable no-function-outside-class -- the built-in table, shared by the initializer and resetForTests\n private static builtIns(): Map<string, EndUserErrorFactory> {\n return new Map<string, EndUserErrorFactory>([\n [\n ApiEndUserNotFoundError.CODE,\n (message: string, detail: string | undefined) =>\n new ApiEndUserNotFoundError(message, detail),\n ],\n [\n ApiEndUserForbiddenError.CODE,\n (message: string, detail: string | undefined) =>\n new ApiEndUserForbiddenError(message, detail),\n ],\n [\n ApiEndUserBadRequestError.CODE,\n (message: string, detail: string | undefined) =>\n new ApiEndUserBadRequestError(message, detail),\n ],\n ]);\n }\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import { ApiEndUserError } from './ApiError';
2
+ /**
3
+ * The thing the end user asked about does not exist (or no longer does): "that report was deleted".
4
+ * A partner-facing edge answers 404 for it.
5
+ */
6
+ export declare class ApiEndUserNotFoundError extends ApiEndUserError {
7
+ static readonly CODE = "user-not-found";
8
+ constructor(message: string, detail?: string, cause?: Error);
9
+ }
10
+ /**
11
+ * The end user may not do this, and the message tells them why ("only the owner can delete this
12
+ * board"). Deliberately NOT `ApiForbiddenError`: that is a 403 authorization failure whose text is
13
+ * never published, while this is an expected answer shown to the person verbatim. It sets no
14
+ * `edgeHttpStatus`, because `EdgeHttpStatus` excludes 403 on purpose: authorization keeps its own type.
15
+ */
16
+ export declare class ApiEndUserForbiddenError extends ApiEndUserError {
17
+ static readonly CODE = "user-forbidden";
18
+ constructor(message: string, detail?: string, cause?: Error);
19
+ }
20
+ /**
21
+ * The end user's input was well-formed but wrong in a way they must fix ("the two passwords do not
22
+ * match"). A partner-facing edge answers 400 for it. Distinct from `ApiBadRequestError`, whose
23
+ * `message` is operator-only and which means the CALLER built a bad request.
24
+ */
25
+ export declare class ApiEndUserBadRequestError extends ApiEndUserError {
26
+ static readonly CODE = "user-bad-request";
27
+ constructor(message: string, detail?: string, cause?: Error);
28
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiEndUserBadRequestError = exports.ApiEndUserForbiddenError = exports.ApiEndUserNotFoundError = void 0;
4
+ const ApiError_1 = require("./ApiError");
5
+ const EndUserErrorCode_1 = require("./EndUserErrorCode");
6
+ /*
7
+ * The generic end-user subclasses webpieces ships. Each one follows THE subclass pattern (issue
8
+ * #1173), which an app copies for its own (terms-not-accepted, domain refusals):
9
+ *
10
+ * 1. `CODE` is a static constant and the constructor FIXES it, so a throw site cannot send a missing
11
+ * or misspelled code; the caller supplies only the message and, optionally, a `detail` (data the
12
+ * client acts on, never prose — see EndUserErrorCode).
13
+ * 2. It is REGISTERED ONCE at startup, in every process that may receive it (browser bundle, Expo
14
+ * shell, every server), so `fromWire(toWire(new X(...)))` is an `X` on both ends of every hop:
15
+ *
16
+ * EndUserErrorRegistry.register(TermsNotAcceptedError.CODE,
17
+ * (message: string, detail: string | undefined) => new TermsNotAcceptedError(message, detail));
18
+ *
19
+ * These three are pre-registered by webpieces itself (see EndUserErrorRegistry), so they decode typed
20
+ * with no app setup. Their codes carry a `user-` prefix so they cannot collide with an `ApiErrorKind`
21
+ * of the same name and so an app's own short codes stay free.
22
+ */
23
+ /**
24
+ * The thing the end user asked about does not exist (or no longer does): "that report was deleted".
25
+ * A partner-facing edge answers 404 for it.
26
+ */
27
+ class ApiEndUserNotFoundError extends ApiError_1.ApiEndUserError {
28
+ static CODE = 'user-not-found';
29
+ constructor(message, detail, cause) {
30
+ super(message, EndUserErrorCode_1.EndUserErrorCode.format(ApiEndUserNotFoundError.CODE, detail), 404, cause);
31
+ }
32
+ }
33
+ exports.ApiEndUserNotFoundError = ApiEndUserNotFoundError;
34
+ /**
35
+ * The end user may not do this, and the message tells them why ("only the owner can delete this
36
+ * board"). Deliberately NOT `ApiForbiddenError`: that is a 403 authorization failure whose text is
37
+ * never published, while this is an expected answer shown to the person verbatim. It sets no
38
+ * `edgeHttpStatus`, because `EdgeHttpStatus` excludes 403 on purpose: authorization keeps its own type.
39
+ */
40
+ class ApiEndUserForbiddenError extends ApiError_1.ApiEndUserError {
41
+ static CODE = 'user-forbidden';
42
+ constructor(message, detail, cause) {
43
+ super(message, EndUserErrorCode_1.EndUserErrorCode.format(ApiEndUserForbiddenError.CODE, detail), undefined, cause);
44
+ }
45
+ }
46
+ exports.ApiEndUserForbiddenError = ApiEndUserForbiddenError;
47
+ /**
48
+ * The end user's input was well-formed but wrong in a way they must fix ("the two passwords do not
49
+ * match"). A partner-facing edge answers 400 for it. Distinct from `ApiBadRequestError`, whose
50
+ * `message` is operator-only and which means the CALLER built a bad request.
51
+ */
52
+ class ApiEndUserBadRequestError extends ApiError_1.ApiEndUserError {
53
+ static CODE = 'user-bad-request';
54
+ constructor(message, detail, cause) {
55
+ super(message, EndUserErrorCode_1.EndUserErrorCode.format(ApiEndUserBadRequestError.CODE, detail), 400, cause);
56
+ }
57
+ }
58
+ exports.ApiEndUserBadRequestError = ApiEndUserBadRequestError;
59
+ //# sourceMappingURL=EndUserErrors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EndUserErrors.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/EndUserErrors.ts"],"names":[],"mappings":";;;AAAA,yCAA6C;AAC7C,yDAAsD;AAEtD;;;;;;;;;;;;;;;;GAgBG;AAEH;;;GAGG;AACH,MAAa,uBAAwB,SAAQ,0BAAe;IACxD,MAAM,CAAU,IAAI,GAAG,gBAAgB,CAAC;IAExC,YAAY,OAAe,EAAE,MAAe,EAAE,KAAa;QACvD,KAAK,CAAC,OAAO,EAAE,mCAAgB,CAAC,MAAM,CAAC,uBAAuB,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;IAC9F,CAAC;;AALL,0DAMC;AAED;;;;;GAKG;AACH,MAAa,wBAAyB,SAAQ,0BAAe;IACzD,MAAM,CAAU,IAAI,GAAG,gBAAgB,CAAC;IAExC,YAAY,OAAe,EAAE,MAAe,EAAE,KAAa;QACvD,KAAK,CACD,OAAO,EACP,mCAAgB,CAAC,MAAM,CAAC,wBAAwB,CAAC,IAAI,EAAE,MAAM,CAAC,EAC9D,SAAS,EACT,KAAK,CACR,CAAC;IACN,CAAC;;AAVL,4DAWC;AAED;;;;GAIG;AACH,MAAa,yBAA0B,SAAQ,0BAAe;IAC1D,MAAM,CAAU,IAAI,GAAG,kBAAkB,CAAC;IAE1C,YAAY,OAAe,EAAE,MAAe,EAAE,KAAa;QACvD,KAAK,CAAC,OAAO,EAAE,mCAAgB,CAAC,MAAM,CAAC,yBAAyB,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;IAChG,CAAC;;AALL,8DAMC","sourcesContent":["import { ApiEndUserError } from './ApiError';\nimport { EndUserErrorCode } from './EndUserErrorCode';\n\n/*\n * The generic end-user subclasses webpieces ships. Each one follows THE subclass pattern (issue\n * #1173), which an app copies for its own (terms-not-accepted, domain refusals):\n *\n * 1. `CODE` is a static constant and the constructor FIXES it, so a throw site cannot send a missing\n * or misspelled code; the caller supplies only the message and, optionally, a `detail` (data the\n * client acts on, never prose — see EndUserErrorCode).\n * 2. It is REGISTERED ONCE at startup, in every process that may receive it (browser bundle, Expo\n * shell, every server), so `fromWire(toWire(new X(...)))` is an `X` on both ends of every hop:\n *\n * EndUserErrorRegistry.register(TermsNotAcceptedError.CODE,\n * (message: string, detail: string | undefined) => new TermsNotAcceptedError(message, detail));\n *\n * These three are pre-registered by webpieces itself (see EndUserErrorRegistry), so they decode typed\n * with no app setup. Their codes carry a `user-` prefix so they cannot collide with an `ApiErrorKind`\n * of the same name and so an app's own short codes stay free.\n */\n\n/**\n * The thing the end user asked about does not exist (or no longer does): \"that report was deleted\".\n * A partner-facing edge answers 404 for it.\n */\nexport class ApiEndUserNotFoundError extends ApiEndUserError {\n static readonly CODE = 'user-not-found';\n\n constructor(message: string, detail?: string, cause?: Error) {\n super(message, EndUserErrorCode.format(ApiEndUserNotFoundError.CODE, detail), 404, cause);\n }\n}\n\n/**\n * The end user may not do this, and the message tells them why (\"only the owner can delete this\n * board\"). Deliberately NOT `ApiForbiddenError`: that is a 403 authorization failure whose text is\n * never published, while this is an expected answer shown to the person verbatim. It sets no\n * `edgeHttpStatus`, because `EdgeHttpStatus` excludes 403 on purpose: authorization keeps its own type.\n */\nexport class ApiEndUserForbiddenError extends ApiEndUserError {\n static readonly CODE = 'user-forbidden';\n\n constructor(message: string, detail?: string, cause?: Error) {\n super(\n message,\n EndUserErrorCode.format(ApiEndUserForbiddenError.CODE, detail),\n undefined,\n cause,\n );\n }\n}\n\n/**\n * The end user's input was well-formed but wrong in a way they must fix (\"the two passwords do not\n * match\"). A partner-facing edge answers 400 for it. Distinct from `ApiBadRequestError`, whose\n * `message` is operator-only and which means the CALLER built a bad request.\n */\nexport class ApiEndUserBadRequestError extends ApiEndUserError {\n static readonly CODE = 'user-bad-request';\n\n constructor(message: string, detail?: string, cause?: Error) {\n super(message, EndUserErrorCode.format(ApiEndUserBadRequestError.CODE, detail), 400, cause);\n }\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { ApiError } from './ApiError';
2
+ import { ClientRole } from './ClientRole';
2
3
  /**
3
4
  * THE rule for an error this process RECEIVED from another one — identical over HTTP and over IPC,
4
5
  * which is why it lives here and not in either protocol's translator.
@@ -17,9 +18,13 @@ import { ApiError } from './ApiError';
17
18
  * - **408/429/502/503/504 preserve retry-relevant semantics.** Timeout, backoff, bad-gateway and
18
19
  * unavailable must not collapse into the generic non-retryable dependency bucket.
19
20
  * - **266 / `end-user` -> {@link ApiEndUserError}**, the intended user-facing message, published
20
- * verbatim. It is the one message the taxonomy lets a peer write for a human.
21
+ * verbatim. It is the one message the taxonomy lets a peer write for a human. `ApiErrorCodec` has
22
+ * already rebuilt the registered SUBCLASS for its `errorCode` (see `EndUserErrorRegistry`).
23
+ * - **426 / `client-too-old` -> {@link ApiClientTooOldError}, rethrown AS-IS, on EVERY hop and for
24
+ * every {@link ClientRole}.** It is not "I called wrong", it is "the build on the calling side is
25
+ * older than the server allows", and every receiver must see "upgrade", never a bug report.
21
26
  *
22
- * # Why this is UNIFORM, in a browser as much as in a server
27
+ * # Why this is UNIFORM, in a browser as much as in a server — with ONE exception, 401
23
28
  *
24
29
  * An earlier design made the browser pass a received status through unchanged on the theory that
25
30
  * "the browser IS the user's agent, so a 404 is the user's answer". That is wrong. If a browser
@@ -28,11 +33,27 @@ import { ApiError } from './ApiError';
28
33
  * as intended: `ApiImplementationError` means the client has a bug, `ApiDependencyError` means the
29
34
  * server has one.
30
35
  *
36
+ * **401 is the exception, and it is why the rule takes a {@link ClientRole}.** A received 401 means
37
+ * "the credential I presented was rejected", and WHOSE credential that was depends on who I am:
38
+ *
39
+ * - an {@link ClientRole.END_USER_CLIENT} (browser bundle, Expo shell, remote MCP client) presented
40
+ * the USER's session, so a 401 carrying the webpieces `unauthorized` payload is decoded as
41
+ * {@link ApiUnauthorizedError}: "log in again" (and, for a remote MCP client, "go find the
42
+ * authorization server"), not a bug.
43
+ * - a {@link ClientRole.SERVER} presented ITS OWN credential, so a 401 stays
44
+ * {@link ApiImplementationError} -> a 500 to its own caller. Relaying it as 401 would tell the
45
+ * browser "your session expired" and log the user out for a backend misconfiguration, and it would
46
+ * stop paging the team whose credential is broken.
47
+ *
48
+ * Every other status reads the same in both roles, so the role is consulted for 401 alone. The role
49
+ * has no default: each client states it at setup (see {@link ClientRole}).
50
+ *
31
51
  * An app that genuinely wants a peer's status relayed as its OWN typed error says so out loud, in one
32
52
  * greppable place, by throwing it from its registered translator's `fromWire`.
33
53
  */
34
54
  export declare class ReceivedApiErrorRule {
35
55
  /**
56
+ * @param role - who received it, declared at the client's setup. Decides what a 401 means.
36
57
  * @param statusCode - the HTTP status the peer published. IPC has no wire status, so it derives
37
58
  * the equivalent from the payload's `kind` through the same `ApiErrorHttpStatus` table — one
38
59
  * table, so the two protocols cannot drift apart.
@@ -41,5 +62,14 @@ export declare class ReceivedApiErrorRule {
41
62
  * @param decoded - the peer's own typed error when the body was a webpieces `ApiErrorPayload`.
42
63
  * Absent for a foreign responder.
43
64
  */
44
- static adapt(statusCode: number, message: string, decoded?: ApiError, retryAfterSeconds?: number): Error;
65
+ static adapt(role: ClientRole, statusCode: number, message: string, decoded?: ApiError, retryAfterSeconds?: number): Error;
66
+ /**
67
+ * The peer's own typed error, rethrown AS-IS, when re-classifying it would change its meaning:
68
+ *
69
+ * - {@link ApiClientTooOldError}: an upgrade, never a bug, on every hop and for every role.
70
+ * - a 401 {@link ApiUnauthorizedError} received by an END-USER client: the credential it sent IS
71
+ * the user's session, so this is "log in again". A SERVER's 401 is its own bug and falls through.
72
+ * - a dependency failure: already attributed further downstream.
73
+ */
74
+ private static passesThrough;
45
75
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ReceivedApiErrorRule = void 0;
4
4
  const ApiError_1 = require("./ApiError");
5
+ const ClientRole_1 = require("./ClientRole");
5
6
  /**
6
7
  * THE rule for an error this process RECEIVED from another one — identical over HTTP and over IPC,
7
8
  * which is why it lives here and not in either protocol's translator.
@@ -20,9 +21,13 @@ const ApiError_1 = require("./ApiError");
20
21
  * - **408/429/502/503/504 preserve retry-relevant semantics.** Timeout, backoff, bad-gateway and
21
22
  * unavailable must not collapse into the generic non-retryable dependency bucket.
22
23
  * - **266 / `end-user` -> {@link ApiEndUserError}**, the intended user-facing message, published
23
- * verbatim. It is the one message the taxonomy lets a peer write for a human.
24
+ * verbatim. It is the one message the taxonomy lets a peer write for a human. `ApiErrorCodec` has
25
+ * already rebuilt the registered SUBCLASS for its `errorCode` (see `EndUserErrorRegistry`).
26
+ * - **426 / `client-too-old` -> {@link ApiClientTooOldError}, rethrown AS-IS, on EVERY hop and for
27
+ * every {@link ClientRole}.** It is not "I called wrong", it is "the build on the calling side is
28
+ * older than the server allows", and every receiver must see "upgrade", never a bug report.
24
29
  *
25
- * # Why this is UNIFORM, in a browser as much as in a server
30
+ * # Why this is UNIFORM, in a browser as much as in a server — with ONE exception, 401
26
31
  *
27
32
  * An earlier design made the browser pass a received status through unchanged on the theory that
28
33
  * "the browser IS the user's agent, so a 404 is the user's answer". That is wrong. If a browser
@@ -31,11 +36,27 @@ const ApiError_1 = require("./ApiError");
31
36
  * as intended: `ApiImplementationError` means the client has a bug, `ApiDependencyError` means the
32
37
  * server has one.
33
38
  *
39
+ * **401 is the exception, and it is why the rule takes a {@link ClientRole}.** A received 401 means
40
+ * "the credential I presented was rejected", and WHOSE credential that was depends on who I am:
41
+ *
42
+ * - an {@link ClientRole.END_USER_CLIENT} (browser bundle, Expo shell, remote MCP client) presented
43
+ * the USER's session, so a 401 carrying the webpieces `unauthorized` payload is decoded as
44
+ * {@link ApiUnauthorizedError}: "log in again" (and, for a remote MCP client, "go find the
45
+ * authorization server"), not a bug.
46
+ * - a {@link ClientRole.SERVER} presented ITS OWN credential, so a 401 stays
47
+ * {@link ApiImplementationError} -> a 500 to its own caller. Relaying it as 401 would tell the
48
+ * browser "your session expired" and log the user out for a backend misconfiguration, and it would
49
+ * stop paging the team whose credential is broken.
50
+ *
51
+ * Every other status reads the same in both roles, so the role is consulted for 401 alone. The role
52
+ * has no default: each client states it at setup (see {@link ClientRole}).
53
+ *
34
54
  * An app that genuinely wants a peer's status relayed as its OWN typed error says so out loud, in one
35
55
  * greppable place, by throwing it from its registered translator's `fromWire`.
36
56
  */
37
57
  class ReceivedApiErrorRule {
38
58
  /**
59
+ * @param role - who received it, declared at the client's setup. Decides what a 401 means.
39
60
  * @param statusCode - the HTTP status the peer published. IPC has no wire status, so it derives
40
61
  * the equivalent from the payload's `kind` through the same `ApiErrorHttpStatus` table — one
41
62
  * table, so the two protocols cannot drift apart.
@@ -45,16 +66,13 @@ class ReceivedApiErrorRule {
45
66
  * Absent for a foreign responder.
46
67
  */
47
68
  // webpieces-disable no-function-outside-class -- stateless shared rule, called by both protocol translators
48
- static adapt(statusCode, message, decoded, retryAfterSeconds) {
69
+ static adapt(role, statusCode, message, decoded, retryAfterSeconds) {
49
70
  // 266 is protocol SUCCESS carrying the actor's own answer — the one message published verbatim.
50
71
  if (statusCode === 266) {
51
72
  return decoded instanceof ApiError_1.ApiEndUserError ? decoded : new ApiError_1.ApiEndUserError(message);
52
73
  }
53
- if (decoded instanceof ApiError_1.ApiDependencyError ||
54
- decoded instanceof ApiError_1.ApiBadGatewayError ||
55
- decoded instanceof ApiError_1.ApiUnavailableError ||
56
- decoded instanceof ApiError_1.ApiDependencyTimeoutError ||
57
- decoded instanceof ApiError_1.ApiDependencyBackoffError) {
74
+ if (decoded !== undefined &&
75
+ ReceivedApiErrorRule.passesThrough(role, statusCode, decoded)) {
58
76
  return decoded;
59
77
  }
60
78
  if (statusCode === 408 || statusCode === 504) {
@@ -79,6 +97,27 @@ class ReceivedApiErrorRule {
79
97
  `whether the dependency is deployed, and our service credentials. ` +
80
98
  `Downstream said: ${message}`, decoded);
81
99
  }
100
+ /**
101
+ * The peer's own typed error, rethrown AS-IS, when re-classifying it would change its meaning:
102
+ *
103
+ * - {@link ApiClientTooOldError}: an upgrade, never a bug, on every hop and for every role.
104
+ * - a 401 {@link ApiUnauthorizedError} received by an END-USER client: the credential it sent IS
105
+ * the user's session, so this is "log in again". A SERVER's 401 is its own bug and falls through.
106
+ * - a dependency failure: already attributed further downstream.
107
+ */
108
+ // webpieces-disable no-function-outside-class -- stateless shared rule, called by both protocol translators
109
+ static passesThrough(role, statusCode, decoded) {
110
+ if (decoded instanceof ApiError_1.ApiClientTooOldError)
111
+ return true;
112
+ if (statusCode === 401 && decoded instanceof ApiError_1.ApiUnauthorizedError) {
113
+ return role === ClientRole_1.ClientRole.END_USER_CLIENT;
114
+ }
115
+ return (decoded instanceof ApiError_1.ApiDependencyError ||
116
+ decoded instanceof ApiError_1.ApiBadGatewayError ||
117
+ decoded instanceof ApiError_1.ApiUnavailableError ||
118
+ decoded instanceof ApiError_1.ApiDependencyTimeoutError ||
119
+ decoded instanceof ApiError_1.ApiDependencyBackoffError);
120
+ }
82
121
  }
83
122
  exports.ReceivedApiErrorRule = ReceivedApiErrorRule;
84
123
  //# sourceMappingURL=ReceivedApiErrorRule.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ReceivedApiErrorRule.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ReceivedApiErrorRule.ts"],"names":[],"mappings":";;;AAAA,yCASoB;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAa,oBAAoB;IAC7B;;;;;;;;OAQG;IACH,4GAA4G;IAC5G,MAAM,CAAC,KAAK,CACR,UAAkB,EAClB,OAAe,EACf,OAAkB,EAClB,iBAA0B;QAE1B,gGAAgG;QAChG,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;YACrB,OAAO,OAAO,YAAY,0BAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,0BAAe,CAAC,OAAO,CAAC,CAAC;QACvF,CAAC;QACD,IACI,OAAO,YAAY,6BAAkB;YACrC,OAAO,YAAY,6BAAkB;YACrC,OAAO,YAAY,8BAAmB;YACtC,OAAO,YAAY,oCAAyB;YAC5C,OAAO,YAAY,oCAAyB,EAC9C,CAAC;YACC,OAAO,OAAO,CAAC;QACnB,CAAC;QACD,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;YAC3C,OAAO,IAAI,oCAAyB,CAChC,kCAAkC,UAAU,sBAAsB,OAAO,EAAE,EAC3E,OAAO,CACV,CAAC;QACN,CAAC;QACD,IAAI,UAAU,KAAK,GAAG,IAAI,CAAC,UAAU,KAAK,GAAG,IAAI,iBAAiB,KAAK,SAAS,CAAC,EAAE,CAAC;YAChF,OAAO,IAAI,oCAAyB,CAChC,wDAAwD,UAAU,IAAI;gBAClE,oBAAoB,OAAO,EAAE,EACjC,iBAAiB,EACjB,OAAO,CACV,CAAC;QACN,CAAC;QACD,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;YACrB,OAAO,IAAI,6BAAkB,CACzB,+CAA+C,OAAO,EAAE,EACxD,OAAO,CACV,CAAC;QACN,CAAC;QACD,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;YACrB,OAAO,IAAI,8BAAmB,CAC1B,0DAA0D,OAAO,EAAE,EACnE,OAAO,CACV,CAAC;QACN,CAAC;QACD,IAAI,UAAU,IAAI,GAAG,EAAE,CAAC;YACpB,OAAO,IAAI,6BAAkB,CACzB,uBAAuB,UAAU,qCAAqC;gBAClE,oBAAoB,OAAO,EAAE,EACjC,OAAO,CACV,CAAC;QACN,CAAC;QACD,OAAO,IAAI,iCAAsB,CAC7B,uBAAuB,UAAU,oDAAoD;YACjF,iFAAiF;YACjF,mEAAmE;YACnE,oBAAoB,OAAO,EAAE,EACjC,OAAO,CACV,CAAC;IACN,CAAC;CACJ;AAvED,oDAuEC","sourcesContent":["import {\n ApiBadGatewayError,\n ApiDependencyBackoffError,\n ApiDependencyError,\n ApiDependencyTimeoutError,\n ApiEndUserError,\n ApiError,\n ApiImplementationError,\n ApiUnavailableError,\n} from './ApiError';\n\n/**\n * THE rule for an error this process RECEIVED from another one — identical over HTTP and over IPC,\n * which is why it lives here and not in either protocol's translator.\n *\n * A status a peer answered describes OUR request to it. It is never, by itself, an answer for our\n * own caller, and the two ways it can be wrong have OPPOSITE owners:\n *\n * - **ordinary caller-error status (4xx-equivalent) -> {@link ApiImplementationError}.** WE sent a bad request,\n * called a path that does not exist, or presented the wrong credentials. MY bug. The peer that\n * answered 404 is correct.\n * - **ordinary server-error status (5xx-equivalent) -> {@link ApiDependencyError}.** THEY broke. Not my bug,\n * so this process's failure metrics stay clean and page the right team.\n * - **an {@link ApiDependencyError} coming back -> rethrown AS-IS.** The fault is already attributed,\n * somewhere further downstream; this hop adds nothing by re-wrapping it, and re-wrapping would bury\n * the original message one `cause` deeper on every hop of a chain.\n * - **408/429/502/503/504 preserve retry-relevant semantics.** Timeout, backoff, bad-gateway and\n * unavailable must not collapse into the generic non-retryable dependency bucket.\n * - **266 / `end-user` -> {@link ApiEndUserError}**, the intended user-facing message, published\n * verbatim. It is the one message the taxonomy lets a peer write for a human.\n *\n * # Why this is UNIFORM, in a browser as much as in a server\n *\n * An earlier design made the browser pass a received status through unchanged on the theory that\n * \"the browser IS the user's agent, so a 404 is the user's answer\". That is wrong. If a browser\n * received a 404 the browser called the wrong path — that is the BROWSER's bug, and the code that\n * catches it wants to know that, not to show a user a spinner. Read in a browser this reads exactly\n * as intended: `ApiImplementationError` means the client has a bug, `ApiDependencyError` means the\n * server has one.\n *\n * An app that genuinely wants a peer's status relayed as its OWN typed error says so out loud, in one\n * greppable place, by throwing it from its registered translator's `fromWire`.\n */\nexport class ReceivedApiErrorRule {\n /**\n * @param statusCode - the HTTP status the peer published. IPC has no wire status, so it derives\n * the equivalent from the payload's `kind` through the same `ApiErrorHttpStatus` table — one\n * table, so the two protocols cannot drift apart.\n * @param message - the text to carry forward: the decoded payload's message, else whatever the\n * transport could salvage from the body.\n * @param decoded - the peer's own typed error when the body was a webpieces `ApiErrorPayload`.\n * Absent for a foreign responder.\n */\n // webpieces-disable no-function-outside-class -- stateless shared rule, called by both protocol translators\n static adapt(\n statusCode: number,\n message: string,\n decoded?: ApiError,\n retryAfterSeconds?: number,\n ): Error {\n // 266 is protocol SUCCESS carrying the actor's own answer — the one message published verbatim.\n if (statusCode === 266) {\n return decoded instanceof ApiEndUserError ? decoded : new ApiEndUserError(message);\n }\n if (\n decoded instanceof ApiDependencyError ||\n decoded instanceof ApiBadGatewayError ||\n decoded instanceof ApiUnavailableError ||\n decoded instanceof ApiDependencyTimeoutError ||\n decoded instanceof ApiDependencyBackoffError\n ) {\n return decoded;\n }\n if (statusCode === 408 || statusCode === 504) {\n return new ApiDependencyTimeoutError(\n `dependency timed out with HTTP ${statusCode}. Downstream said: ${message}`,\n decoded,\n );\n }\n if (statusCode === 429 || (statusCode === 503 && retryAfterSeconds !== undefined)) {\n return new ApiDependencyBackoffError(\n `dependency asked this service to back off after HTTP ${statusCode}. ` +\n `Downstream said: ${message}`,\n retryAfterSeconds,\n decoded,\n );\n }\n if (statusCode === 502) {\n return new ApiBadGatewayError(\n `gateway answered HTTP 502. Downstream said: ${message}`,\n decoded,\n );\n }\n if (statusCode === 503) {\n return new ApiUnavailableError(\n `dependency is unavailable (HTTP 503). Downstream said: ${message}`,\n decoded,\n );\n }\n if (statusCode >= 500) {\n return new ApiDependencyError(\n `dependency answered ${statusCode} — it is broken, not this service. ` +\n `Downstream said: ${message}`,\n decoded,\n );\n }\n return new ApiImplementationError(\n `dependency answered ${statusCode}. That status describes OUR request to it, not an ` +\n `answer for our caller, so this service owns it — check the path, the base URL, ` +\n `whether the dependency is deployed, and our service credentials. ` +\n `Downstream said: ${message}`,\n decoded,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"ReceivedApiErrorRule.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ReceivedApiErrorRule.ts"],"names":[],"mappings":";;;AAAA,yCAWoB;AACpB,6CAA0C;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,MAAa,oBAAoB;IAC7B;;;;;;;;;OASG;IACH,4GAA4G;IAC5G,MAAM,CAAC,KAAK,CACR,IAAgB,EAChB,UAAkB,EAClB,OAAe,EACf,OAAkB,EAClB,iBAA0B;QAE1B,gGAAgG;QAChG,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;YACrB,OAAO,OAAO,YAAY,0BAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,0BAAe,CAAC,OAAO,CAAC,CAAC;QACvF,CAAC;QACD,IACI,OAAO,KAAK,SAAS;YACrB,oBAAoB,CAAC,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,EAC/D,CAAC;YACC,OAAO,OAAO,CAAC;QACnB,CAAC;QACD,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;YAC3C,OAAO,IAAI,oCAAyB,CAChC,kCAAkC,UAAU,sBAAsB,OAAO,EAAE,EAC3E,OAAO,CACV,CAAC;QACN,CAAC;QACD,IAAI,UAAU,KAAK,GAAG,IAAI,CAAC,UAAU,KAAK,GAAG,IAAI,iBAAiB,KAAK,SAAS,CAAC,EAAE,CAAC;YAChF,OAAO,IAAI,oCAAyB,CAChC,wDAAwD,UAAU,IAAI;gBAClE,oBAAoB,OAAO,EAAE,EACjC,iBAAiB,EACjB,OAAO,CACV,CAAC;QACN,CAAC;QACD,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;YACrB,OAAO,IAAI,6BAAkB,CACzB,+CAA+C,OAAO,EAAE,EACxD,OAAO,CACV,CAAC;QACN,CAAC;QACD,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;YACrB,OAAO,IAAI,8BAAmB,CAC1B,0DAA0D,OAAO,EAAE,EACnE,OAAO,CACV,CAAC;QACN,CAAC;QACD,IAAI,UAAU,IAAI,GAAG,EAAE,CAAC;YACpB,OAAO,IAAI,6BAAkB,CACzB,uBAAuB,UAAU,qCAAqC;gBAClE,oBAAoB,OAAO,EAAE,EACjC,OAAO,CACV,CAAC;QACN,CAAC;QACD,OAAO,IAAI,iCAAsB,CAC7B,uBAAuB,UAAU,oDAAoD;YACjF,iFAAiF;YACjF,mEAAmE;YACnE,oBAAoB,OAAO,EAAE,EACjC,OAAO,CACV,CAAC;IACN,CAAC;IAED;;;;;;;OAOG;IACH,4GAA4G;IACpG,MAAM,CAAC,aAAa,CAAC,IAAgB,EAAE,UAAkB,EAAE,OAAc;QAC7E,IAAI,OAAO,YAAY,+BAAoB;YAAE,OAAO,IAAI,CAAC;QACzD,IAAI,UAAU,KAAK,GAAG,IAAI,OAAO,YAAY,+BAAoB,EAAE,CAAC;YAChE,OAAO,IAAI,KAAK,uBAAU,CAAC,eAAe,CAAC;QAC/C,CAAC;QACD,OAAO,CACH,OAAO,YAAY,6BAAkB;YACrC,OAAO,YAAY,6BAAkB;YACrC,OAAO,YAAY,8BAAmB;YACtC,OAAO,YAAY,oCAAyB;YAC5C,OAAO,YAAY,oCAAyB,CAC/C,CAAC;IACN,CAAC;CACJ;AA7FD,oDA6FC","sourcesContent":["import {\n ApiBadGatewayError,\n ApiClientTooOldError,\n ApiDependencyBackoffError,\n ApiDependencyError,\n ApiDependencyTimeoutError,\n ApiEndUserError,\n ApiError,\n ApiImplementationError,\n ApiUnauthorizedError,\n ApiUnavailableError,\n} from './ApiError';\nimport { ClientRole } from './ClientRole';\n\n/**\n * THE rule for an error this process RECEIVED from another one — identical over HTTP and over IPC,\n * which is why it lives here and not in either protocol's translator.\n *\n * A status a peer answered describes OUR request to it. It is never, by itself, an answer for our\n * own caller, and the two ways it can be wrong have OPPOSITE owners:\n *\n * - **ordinary caller-error status (4xx-equivalent) -> {@link ApiImplementationError}.** WE sent a bad request,\n * called a path that does not exist, or presented the wrong credentials. MY bug. The peer that\n * answered 404 is correct.\n * - **ordinary server-error status (5xx-equivalent) -> {@link ApiDependencyError}.** THEY broke. Not my bug,\n * so this process's failure metrics stay clean and page the right team.\n * - **an {@link ApiDependencyError} coming back -> rethrown AS-IS.** The fault is already attributed,\n * somewhere further downstream; this hop adds nothing by re-wrapping it, and re-wrapping would bury\n * the original message one `cause` deeper on every hop of a chain.\n * - **408/429/502/503/504 preserve retry-relevant semantics.** Timeout, backoff, bad-gateway and\n * unavailable must not collapse into the generic non-retryable dependency bucket.\n * - **266 / `end-user` -> {@link ApiEndUserError}**, the intended user-facing message, published\n * verbatim. It is the one message the taxonomy lets a peer write for a human. `ApiErrorCodec` has\n * already rebuilt the registered SUBCLASS for its `errorCode` (see `EndUserErrorRegistry`).\n * - **426 / `client-too-old` -> {@link ApiClientTooOldError}, rethrown AS-IS, on EVERY hop and for\n * every {@link ClientRole}.** It is not \"I called wrong\", it is \"the build on the calling side is\n * older than the server allows\", and every receiver must see \"upgrade\", never a bug report.\n *\n * # Why this is UNIFORM, in a browser as much as in a server — with ONE exception, 401\n *\n * An earlier design made the browser pass a received status through unchanged on the theory that\n * \"the browser IS the user's agent, so a 404 is the user's answer\". That is wrong. If a browser\n * received a 404 the browser called the wrong path — that is the BROWSER's bug, and the code that\n * catches it wants to know that, not to show a user a spinner. Read in a browser this reads exactly\n * as intended: `ApiImplementationError` means the client has a bug, `ApiDependencyError` means the\n * server has one.\n *\n * **401 is the exception, and it is why the rule takes a {@link ClientRole}.** A received 401 means\n * \"the credential I presented was rejected\", and WHOSE credential that was depends on who I am:\n *\n * - an {@link ClientRole.END_USER_CLIENT} (browser bundle, Expo shell, remote MCP client) presented\n * the USER's session, so a 401 carrying the webpieces `unauthorized` payload is decoded as\n * {@link ApiUnauthorizedError}: \"log in again\" (and, for a remote MCP client, \"go find the\n * authorization server\"), not a bug.\n * - a {@link ClientRole.SERVER} presented ITS OWN credential, so a 401 stays\n * {@link ApiImplementationError} -> a 500 to its own caller. Relaying it as 401 would tell the\n * browser \"your session expired\" and log the user out for a backend misconfiguration, and it would\n * stop paging the team whose credential is broken.\n *\n * Every other status reads the same in both roles, so the role is consulted for 401 alone. The role\n * has no default: each client states it at setup (see {@link ClientRole}).\n *\n * An app that genuinely wants a peer's status relayed as its OWN typed error says so out loud, in one\n * greppable place, by throwing it from its registered translator's `fromWire`.\n */\nexport class ReceivedApiErrorRule {\n /**\n * @param role - who received it, declared at the client's setup. Decides what a 401 means.\n * @param statusCode - the HTTP status the peer published. IPC has no wire status, so it derives\n * the equivalent from the payload's `kind` through the same `ApiErrorHttpStatus` table — one\n * table, so the two protocols cannot drift apart.\n * @param message - the text to carry forward: the decoded payload's message, else whatever the\n * transport could salvage from the body.\n * @param decoded - the peer's own typed error when the body was a webpieces `ApiErrorPayload`.\n * Absent for a foreign responder.\n */\n // webpieces-disable no-function-outside-class -- stateless shared rule, called by both protocol translators\n static adapt(\n role: ClientRole,\n statusCode: number,\n message: string,\n decoded?: ApiError,\n retryAfterSeconds?: number,\n ): Error {\n // 266 is protocol SUCCESS carrying the actor's own answer — the one message published verbatim.\n if (statusCode === 266) {\n return decoded instanceof ApiEndUserError ? decoded : new ApiEndUserError(message);\n }\n if (\n decoded !== undefined &&\n ReceivedApiErrorRule.passesThrough(role, statusCode, decoded)\n ) {\n return decoded;\n }\n if (statusCode === 408 || statusCode === 504) {\n return new ApiDependencyTimeoutError(\n `dependency timed out with HTTP ${statusCode}. Downstream said: ${message}`,\n decoded,\n );\n }\n if (statusCode === 429 || (statusCode === 503 && retryAfterSeconds !== undefined)) {\n return new ApiDependencyBackoffError(\n `dependency asked this service to back off after HTTP ${statusCode}. ` +\n `Downstream said: ${message}`,\n retryAfterSeconds,\n decoded,\n );\n }\n if (statusCode === 502) {\n return new ApiBadGatewayError(\n `gateway answered HTTP 502. Downstream said: ${message}`,\n decoded,\n );\n }\n if (statusCode === 503) {\n return new ApiUnavailableError(\n `dependency is unavailable (HTTP 503). Downstream said: ${message}`,\n decoded,\n );\n }\n if (statusCode >= 500) {\n return new ApiDependencyError(\n `dependency answered ${statusCode} — it is broken, not this service. ` +\n `Downstream said: ${message}`,\n decoded,\n );\n }\n return new ApiImplementationError(\n `dependency answered ${statusCode}. That status describes OUR request to it, not an ` +\n `answer for our caller, so this service owns it — check the path, the base URL, ` +\n `whether the dependency is deployed, and our service credentials. ` +\n `Downstream said: ${message}`,\n decoded,\n );\n }\n\n /**\n * The peer's own typed error, rethrown AS-IS, when re-classifying it would change its meaning:\n *\n * - {@link ApiClientTooOldError}: an upgrade, never a bug, on every hop and for every role.\n * - a 401 {@link ApiUnauthorizedError} received by an END-USER client: the credential it sent IS\n * the user's session, so this is \"log in again\". A SERVER's 401 is its own bug and falls through.\n * - a dependency failure: already attributed further downstream.\n */\n // webpieces-disable no-function-outside-class -- stateless shared rule, called by both protocol translators\n private static passesThrough(role: ClientRole, statusCode: number, decoded: Error): boolean {\n if (decoded instanceof ApiClientTooOldError) return true;\n if (statusCode === 401 && decoded instanceof ApiUnauthorizedError) {\n return role === ClientRole.END_USER_CLIENT;\n }\n return (\n decoded instanceof ApiDependencyError ||\n decoded instanceof ApiBadGatewayError ||\n decoded instanceof ApiUnavailableError ||\n decoded instanceof ApiDependencyTimeoutError ||\n decoded instanceof ApiDependencyBackoffError\n );\n }\n}\n"]}
@@ -1,4 +1,9 @@
1
- export { ApiError, ApiNotFoundError, ApiDependencyError, ApiBadGatewayError, ApiUnavailableError, ApiDependencyTimeoutError, ApiImplementationError, ApiRateLimitedError, ApiForbiddenError, ApiRequestTimeoutError, ApiConnectionError, ApiEndpointNotFoundError, ApiEndUserError, ApiBadRequestError, ApiUnauthorizedError, ApiDependencyBackoffError, ApiConflictError, ApiUnprocessableError, ApiPreconditionFailedError, ApiUnsupportedMediaTypeError, ApiNotImplementedError, ApiCodedError, } from './ApiError';
1
+ export { ApiError, ApiNotFoundError, ApiDependencyError, ApiBadGatewayError, ApiUnavailableError, ApiDependencyTimeoutError, ApiImplementationError, ApiRateLimitedError, ApiForbiddenError, ApiRequestTimeoutError, ApiConnectionError, ApiEndpointNotFoundError, ApiEndUserError, ApiBadRequestError, ApiUnauthorizedError, ApiDependencyBackoffError, ApiConflictError, ApiUnprocessableError, ApiPreconditionFailedError, ApiUnsupportedMediaTypeError, ApiNotImplementedError, ApiCodedError, ApiClientTooOldError, } from './ApiError';
2
+ export { ApiEndUserNotFoundError, ApiEndUserForbiddenError, ApiEndUserBadRequestError, } from './EndUserErrors';
3
+ export { EndUserErrorCode } from './EndUserErrorCode';
4
+ export { EndUserErrorRegistry } from './EndUserErrorRegistry';
5
+ export type { EndUserErrorFactory } from './EndUserErrorRegistry';
6
+ export { ClientRole } from './ClientRole';
2
7
  export type { ApiErrorKind, ApiStatusCode, EdgeHttpStatus } from './ApiError';
3
8
  export { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';
4
9
  export { ApiErrorBoundary } from './ApiErrorBoundary';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.ReceivedApiErrorRule = exports.ApiErrorBoundary = exports.ApiErrorPayload = exports.ApiErrorCodec = exports.ApiCodedError = exports.ApiNotImplementedError = exports.ApiUnsupportedMediaTypeError = exports.ApiPreconditionFailedError = exports.ApiUnprocessableError = exports.ApiConflictError = exports.ApiDependencyBackoffError = exports.ApiUnauthorizedError = exports.ApiBadRequestError = exports.ApiEndUserError = exports.ApiEndpointNotFoundError = exports.ApiConnectionError = exports.ApiRequestTimeoutError = exports.ApiForbiddenError = exports.ApiRateLimitedError = exports.ApiImplementationError = exports.ApiDependencyTimeoutError = exports.ApiUnavailableError = exports.ApiBadGatewayError = exports.ApiDependencyError = exports.ApiNotFoundError = exports.ApiError = void 0;
3
+ exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.ReceivedApiErrorRule = exports.ApiErrorBoundary = exports.ApiErrorPayload = exports.ApiErrorCodec = exports.ClientRole = exports.EndUserErrorRegistry = exports.EndUserErrorCode = exports.ApiEndUserBadRequestError = exports.ApiEndUserForbiddenError = exports.ApiEndUserNotFoundError = exports.ApiClientTooOldError = exports.ApiCodedError = exports.ApiNotImplementedError = exports.ApiUnsupportedMediaTypeError = exports.ApiPreconditionFailedError = exports.ApiUnprocessableError = exports.ApiConflictError = exports.ApiDependencyBackoffError = exports.ApiUnauthorizedError = exports.ApiBadRequestError = exports.ApiEndUserError = exports.ApiEndpointNotFoundError = exports.ApiConnectionError = exports.ApiRequestTimeoutError = exports.ApiForbiddenError = exports.ApiRateLimitedError = exports.ApiImplementationError = exports.ApiDependencyTimeoutError = exports.ApiUnavailableError = exports.ApiBadGatewayError = exports.ApiDependencyError = exports.ApiNotFoundError = exports.ApiError = void 0;
4
4
  var ApiError_1 = require("./ApiError");
5
5
  Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return ApiError_1.ApiError; } });
6
6
  Object.defineProperty(exports, "ApiNotFoundError", { enumerable: true, get: function () { return ApiError_1.ApiNotFoundError; } });
@@ -24,6 +24,17 @@ Object.defineProperty(exports, "ApiPreconditionFailedError", { enumerable: true,
24
24
  Object.defineProperty(exports, "ApiUnsupportedMediaTypeError", { enumerable: true, get: function () { return ApiError_1.ApiUnsupportedMediaTypeError; } });
25
25
  Object.defineProperty(exports, "ApiNotImplementedError", { enumerable: true, get: function () { return ApiError_1.ApiNotImplementedError; } });
26
26
  Object.defineProperty(exports, "ApiCodedError", { enumerable: true, get: function () { return ApiError_1.ApiCodedError; } });
27
+ Object.defineProperty(exports, "ApiClientTooOldError", { enumerable: true, get: function () { return ApiError_1.ApiClientTooOldError; } });
28
+ var EndUserErrors_1 = require("./EndUserErrors");
29
+ Object.defineProperty(exports, "ApiEndUserNotFoundError", { enumerable: true, get: function () { return EndUserErrors_1.ApiEndUserNotFoundError; } });
30
+ Object.defineProperty(exports, "ApiEndUserForbiddenError", { enumerable: true, get: function () { return EndUserErrors_1.ApiEndUserForbiddenError; } });
31
+ Object.defineProperty(exports, "ApiEndUserBadRequestError", { enumerable: true, get: function () { return EndUserErrors_1.ApiEndUserBadRequestError; } });
32
+ var EndUserErrorCode_1 = require("./EndUserErrorCode");
33
+ Object.defineProperty(exports, "EndUserErrorCode", { enumerable: true, get: function () { return EndUserErrorCode_1.EndUserErrorCode; } });
34
+ var EndUserErrorRegistry_1 = require("./EndUserErrorRegistry");
35
+ Object.defineProperty(exports, "EndUserErrorRegistry", { enumerable: true, get: function () { return EndUserErrorRegistry_1.EndUserErrorRegistry; } });
36
+ var ClientRole_1 = require("./ClientRole");
37
+ Object.defineProperty(exports, "ClientRole", { enumerable: true, get: function () { return ClientRole_1.ClientRole; } });
27
38
  var ApiErrorCodec_1 = require("./ApiErrorCodec");
28
39
  Object.defineProperty(exports, "ApiErrorCodec", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorCodec; } });
29
40
  Object.defineProperty(exports, "ApiErrorPayload", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorPayload; } });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAuBoB;AAtBhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,8GAAA,kBAAkB,OAAA;AAClB,8GAAA,kBAAkB,OAAA;AAClB,+GAAA,mBAAmB,OAAA;AACnB,qHAAA,yBAAyB,OAAA;AACzB,kHAAA,sBAAsB,OAAA;AACtB,+GAAA,mBAAmB,OAAA;AACnB,6GAAA,iBAAiB,OAAA;AACjB,kHAAA,sBAAsB,OAAA;AACtB,8GAAA,kBAAkB,OAAA;AAClB,oHAAA,wBAAwB,OAAA;AACxB,2GAAA,eAAe,OAAA;AACf,8GAAA,kBAAkB,OAAA;AAClB,gHAAA,oBAAoB,OAAA;AACpB,qHAAA,yBAAyB,OAAA;AACzB,4GAAA,gBAAgB,OAAA;AAChB,iHAAA,qBAAqB,OAAA;AACrB,sHAAA,0BAA0B,OAAA;AAC1B,wHAAA,4BAA4B,OAAA;AAC5B,kHAAA,sBAAsB,OAAA;AACtB,yGAAA,aAAa,OAAA;AAGjB,iDAAiE;AAAxD,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AACvC,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,yDAS6B;AARzB,qHAAA,gBAAgB,OAAA;AAChB,qHAAA,gBAAgB,OAAA;AAChB,gHAAA,WAAW,OAAA;AACX,iHAAA,YAAY,OAAA;AACZ,wHAAA,mBAAmB,OAAA;AACnB,iHAAA,YAAY,OAAA;AACZ,kHAAA,aAAa,OAAA;AACb,gHAAA,WAAW,OAAA","sourcesContent":["export {\n ApiError,\n ApiNotFoundError,\n ApiDependencyError,\n ApiBadGatewayError,\n ApiUnavailableError,\n ApiDependencyTimeoutError,\n ApiImplementationError,\n ApiRateLimitedError,\n ApiForbiddenError,\n ApiRequestTimeoutError,\n ApiConnectionError,\n ApiEndpointNotFoundError,\n ApiEndUserError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiDependencyBackoffError,\n ApiConflictError,\n ApiUnprocessableError,\n ApiPreconditionFailedError,\n ApiUnsupportedMediaTypeError,\n ApiNotImplementedError,\n ApiCodedError,\n} from './ApiError';\nexport type { ApiErrorKind, ApiStatusCode, EdgeHttpStatus } from './ApiError';\nexport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nexport { ApiErrorBoundary } from './ApiErrorBoundary';\nexport { ReceivedApiErrorRule } from './ReceivedApiErrorRule';\nexport {\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './AuthErrorSubtypes';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAwBoB;AAvBhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,8GAAA,kBAAkB,OAAA;AAClB,8GAAA,kBAAkB,OAAA;AAClB,+GAAA,mBAAmB,OAAA;AACnB,qHAAA,yBAAyB,OAAA;AACzB,kHAAA,sBAAsB,OAAA;AACtB,+GAAA,mBAAmB,OAAA;AACnB,6GAAA,iBAAiB,OAAA;AACjB,kHAAA,sBAAsB,OAAA;AACtB,8GAAA,kBAAkB,OAAA;AAClB,oHAAA,wBAAwB,OAAA;AACxB,2GAAA,eAAe,OAAA;AACf,8GAAA,kBAAkB,OAAA;AAClB,gHAAA,oBAAoB,OAAA;AACpB,qHAAA,yBAAyB,OAAA;AACzB,4GAAA,gBAAgB,OAAA;AAChB,iHAAA,qBAAqB,OAAA;AACrB,sHAAA,0BAA0B,OAAA;AAC1B,wHAAA,4BAA4B,OAAA;AAC5B,kHAAA,sBAAsB,OAAA;AACtB,yGAAA,aAAa,OAAA;AACb,gHAAA,oBAAoB,OAAA;AAExB,iDAIyB;AAHrB,wHAAA,uBAAuB,OAAA;AACvB,yHAAA,wBAAwB,OAAA;AACxB,0HAAA,yBAAyB,OAAA;AAE7B,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAE7B,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AAEnB,iDAAiE;AAAxD,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AACvC,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,yDAS6B;AARzB,qHAAA,gBAAgB,OAAA;AAChB,qHAAA,gBAAgB,OAAA;AAChB,gHAAA,WAAW,OAAA;AACX,iHAAA,YAAY,OAAA;AACZ,wHAAA,mBAAmB,OAAA;AACnB,iHAAA,YAAY,OAAA;AACZ,kHAAA,aAAa,OAAA;AACb,gHAAA,WAAW,OAAA","sourcesContent":["export {\n ApiError,\n ApiNotFoundError,\n ApiDependencyError,\n ApiBadGatewayError,\n ApiUnavailableError,\n ApiDependencyTimeoutError,\n ApiImplementationError,\n ApiRateLimitedError,\n ApiForbiddenError,\n ApiRequestTimeoutError,\n ApiConnectionError,\n ApiEndpointNotFoundError,\n ApiEndUserError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiDependencyBackoffError,\n ApiConflictError,\n ApiUnprocessableError,\n ApiPreconditionFailedError,\n ApiUnsupportedMediaTypeError,\n ApiNotImplementedError,\n ApiCodedError,\n ApiClientTooOldError,\n} from './ApiError';\nexport {\n ApiEndUserNotFoundError,\n ApiEndUserForbiddenError,\n ApiEndUserBadRequestError,\n} from './EndUserErrors';\nexport { EndUserErrorCode } from './EndUserErrorCode';\nexport { EndUserErrorRegistry } from './EndUserErrorRegistry';\nexport type { EndUserErrorFactory } from './EndUserErrorRegistry';\nexport { ClientRole } from './ClientRole';\nexport type { ApiErrorKind, ApiStatusCode, EdgeHttpStatus } from './ApiError';\nexport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nexport { ApiErrorBoundary } from './ApiErrorBoundary';\nexport { ReceivedApiErrorRule } from './ReceivedApiErrorRule';\nexport {\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './AuthErrorSubtypes';\n"]}
@@ -20,7 +20,7 @@ import { DestinationTrust } from './DestinationTrust';
20
20
  *
21
21
  * const store = new MutableContextStore();
22
22
  * const factory = new ClientHttpBrowserFactory(store);
23
- * const client = factory.createRpcClient(SaveApi, new ClientConfig('http://api.example.com'));
23
+ * const client = factory.createRpcClient(SaveApi, new ClientConfig('http://api.example.com', ClientRole.SERVER));
24
24
  * ```
25
25
  */
26
26
  export declare class ContextMgr {
@@ -24,7 +24,7 @@ const WebpiecesCoreHeaders_1 = require("./WebpiecesCoreHeaders");
24
24
  *
25
25
  * const store = new MutableContextStore();
26
26
  * const factory = new ClientHttpBrowserFactory(store);
27
- * const client = factory.createRpcClient(SaveApi, new ClientConfig('http://api.example.com'));
27
+ * const client = factory.createRpcClient(SaveApi, new ClientConfig('http://api.example.com', ClientRole.SERVER));
28
28
  * ```
29
29
  */
30
30
  class ContextMgr {
@@ -1 +1 @@
1
- {"version":3,"file":"ContextMgr.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ContextMgr.ts"],"names":[],"mappings":";;;AAGA,qDAAkD;AAClD,+CAA4C;AAC5C,iEAA8D;AAE9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,UAAU;IAGC;IAFpB;IACI,iFAAiF;IACjE,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAC3C,CAAC;IAEJ;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,oBAAoB,CAAC,WAA6B;QAC9C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;QAE3C,KAAK,MAAM,GAAG,IAAI,+BAAc,CAAC,GAAG,EAAE,CAAC,kBAAkB,EAAE,EAAE,CAAC;YAC1D,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3B,SAAS;YACb,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACxD,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,UAAW,EAAE,KAAK,CAAC,CAAC;YACzC,CAAC;QACL,CAAC;QAED,2FAA2F;QAC3F,+FAA+F;QAC/F,gGAAgG;QAChG,MAAM,SAAS,GAAG,yBAAW,CAAC,UAAU,EAAE,CAAC;QAC3C,MAAM,mBAAmB,GAAG,2CAAoB,CAAC,cAAc,CAAC,UAAW,CAAC;QAC5E,IAAI,SAAS,EAAE,CAAC;YACZ,QAAQ,CAAC,GAAG,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;QACjD,CAAC;aAAM,CAAC;YACJ,QAAQ,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACzC,CAAC;QAED,OAAO,QAAQ,CAAC;IACpB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,qBAAqB,CAAC,OAAgB,EAAE,WAA6B;QACjE,KAAK,MAAM,GAAG,IAAI,+BAAc,CAAC,GAAG,EAAE,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,cAAe,CAAC,CAAC;YAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACjC,SAAS;YACb,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;gBAC9C,2EAA2E;gBAC3E,SAAS;YACb,CAAC;YACD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QAClG,CAAC;IACL,CAAC;CACJ;AArFD,gCAqFC","sourcesContent":["import { ContextKey } from '../ContextKey';\nimport { ContextStore } from './ContextWriter';\nimport { DestinationTrust } from './DestinationTrust';\nimport { HeaderRegistry } from './HeaderRegistry';\nimport { ServiceInfo } from './ServiceInfo';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\n\n/**\n * ContextMgr - propagates the magic context onto outbound BROWSER requests.\n *\n * BROWSER-ONLY. Only @webpieces/http-client-browser may name this class. The server reads\n * `RequestContext` directly through `RequestContextHeaders` (in @webpieces/core-context) — a\n * `ContextReader` indirection buys a server nothing, because there is exactly one right answer there.\n *\n * Browsers have no AsyncLocalStorage, so the app holds a `MutableContextStore` and sets values as\n * they become known (login token, tenant). Every transferred key (httpHeader set) in the GLOBAL\n * {@link HeaderRegistry} is read from it and added to outbound requests. The registry is a process\n * global configured once at startup (like LogManager) and is browser-safe: it is the key SCHEMA,\n * not the value store.\n *\n * Example usage:\n * ```typescript\n * // startup, before bootstrap:\n * HeaderRegistry.configure(CompanyHeaders.ALL_HEADERS, true);\n *\n * const store = new MutableContextStore();\n * const factory = new ClientHttpBrowserFactory(store);\n * const client = factory.createRpcClient(SaveApi, new ClientConfig('http://api.example.com'));\n * ```\n */\nexport class ContextMgr {\n constructor(\n /** The app-held store context-key values are read from AND written back into. */\n public readonly contextStore: ContextStore,\n ) {}\n\n /**\n * Build the headers to send on an outbound request: every transferred key (httpHeader set)\n * with a non-empty value THAT THIS DESTINATION MAY RECEIVE, emitted under its `httpHeader`\n * wire name.\n *\n * NO request-id chaining. A browser ORIGINATES a trace — it has no inbound request to point\n * back at. If the app puts an `x-request-id` on the store it goes out as-is, and the server's\n * inbound transfer adopts it as hop 1's own id. Chaining is a server concern; see\n * RequestContextHeaders.\n *\n * `destination` gates TRUSTED keys exactly as it does on the server side (see\n * {@link DestinationTrust}). In practice a browser never reaches the permissive branch — twice\n * over: `BrowserProxyClient.assertEndpointSupported` refuses to bind an `oidc(...)` /\n * `sharedSecret(...)` contract at all, so every browser destination is `jwt()` or `@WpAuthPublic`.\n * The rule is applied here anyway rather than argued away, because the OTHER guarantee people\n * reach for — \"`MutableContextStore.set` only accepts an untrusted key, so a browser store\n * cannot HOLD a trusted value\" — is true of that store and NOT of the seam: {@link ContextMgr}\n * takes any app-supplied {@link ContextStore}, whose `read` is handed an `AnyContextKey`. One\n * enforced rule in both builders beats a browser-only exemption resting on an implementation\n * detail of one implementation.\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n */\n buildOutboundHeaders(destination: DestinationTrust): Map<string, string> {\n const outbound = new Map<string, string>();\n\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n if (!destination.allows(key)) {\n continue;\n }\n const value = this.contextStore.read(key);\n if (value !== undefined && value !== null && value !== '') {\n outbound.set(key.httpHeader!, value);\n }\n }\n\n // CLIENT_VERSION: a browser ORIGINATES a call, so it sends its OWN app build version (from\n // ServiceInfo) and the server logs which client build called it — same rule as the server-side\n // RequestContextHeaders. Our version wins; absent if this app was never identified via setInfo.\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n outbound.set(clientVersionHeader, myVersion);\n } else {\n outbound.delete(clientVersionHeader);\n }\n\n return outbound;\n }\n\n /**\n * RESPONSE -> the store: every key that declares a `responseHeader` and appears on a response\n * this browser just received, merged into the app-held store per the key's own\n * {@link ContextKey.responseMerge}.\n *\n * The browser twin of `RequestContextHeaders.acceptResponseHeaders`, and the reason a value set\n * by a server reaches the page without the page naming a header.\n *\n * TRUSTED keys are DROPPED here, unconditionally and without consulting `destination`, and the\n * `if` is what makes that a compile-time fact rather than a convention: {@link ContextWriter.set}\n * takes an {@link AnyUntrustedContextKey}, so a browser store CANNOT hold a proven value. That is\n * the same rule as the outbound direction reaches by a different route — every browser\n * destination is `jwt()` or `@WpAuthPublic`, so `destination.allows` would refuse them\n * anyway — and `destination` is still threaded and still applied, so a future browser destination\n * that DID authenticate its caller does not silently start believing response headers.\n */\n acceptResponseHeaders(headers: Headers, destination: DestinationTrust): void {\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const value = headers.get(key.responseHeader!);\n if (value === null || value === '') {\n continue;\n }\n if (!destination.allows(key) || key.isTrusted()) {\n // DROPPED: a browser proves nothing, so it may never hold a trusted value.\n continue;\n }\n this.contextStore.set(key, key.mergeResponseValue(this.contextStore.readValue?.(key), value));\n }\n }\n}\n"]}
1
+ {"version":3,"file":"ContextMgr.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ContextMgr.ts"],"names":[],"mappings":";;;AAGA,qDAAkD;AAClD,+CAA4C;AAC5C,iEAA8D;AAE9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,UAAU;IAGC;IAFpB;IACI,iFAAiF;IACjE,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAC3C,CAAC;IAEJ;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,oBAAoB,CAAC,WAA6B;QAC9C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;QAE3C,KAAK,MAAM,GAAG,IAAI,+BAAc,CAAC,GAAG,EAAE,CAAC,kBAAkB,EAAE,EAAE,CAAC;YAC1D,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3B,SAAS;YACb,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACxD,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,UAAW,EAAE,KAAK,CAAC,CAAC;YACzC,CAAC;QACL,CAAC;QAED,2FAA2F;QAC3F,+FAA+F;QAC/F,gGAAgG;QAChG,MAAM,SAAS,GAAG,yBAAW,CAAC,UAAU,EAAE,CAAC;QAC3C,MAAM,mBAAmB,GAAG,2CAAoB,CAAC,cAAc,CAAC,UAAW,CAAC;QAC5E,IAAI,SAAS,EAAE,CAAC;YACZ,QAAQ,CAAC,GAAG,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;QACjD,CAAC;aAAM,CAAC;YACJ,QAAQ,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACzC,CAAC;QAED,OAAO,QAAQ,CAAC;IACpB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,qBAAqB,CAAC,OAAgB,EAAE,WAA6B;QACjE,KAAK,MAAM,GAAG,IAAI,+BAAc,CAAC,GAAG,EAAE,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,cAAe,CAAC,CAAC;YAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACjC,SAAS;YACb,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;gBAC9C,2EAA2E;gBAC3E,SAAS;YACb,CAAC;YACD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QAClG,CAAC;IACL,CAAC;CACJ;AArFD,gCAqFC","sourcesContent":["import { ContextKey } from '../ContextKey';\nimport { ContextStore } from './ContextWriter';\nimport { DestinationTrust } from './DestinationTrust';\nimport { HeaderRegistry } from './HeaderRegistry';\nimport { ServiceInfo } from './ServiceInfo';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\n\n/**\n * ContextMgr - propagates the magic context onto outbound BROWSER requests.\n *\n * BROWSER-ONLY. Only @webpieces/http-client-browser may name this class. The server reads\n * `RequestContext` directly through `RequestContextHeaders` (in @webpieces/core-context) — a\n * `ContextReader` indirection buys a server nothing, because there is exactly one right answer there.\n *\n * Browsers have no AsyncLocalStorage, so the app holds a `MutableContextStore` and sets values as\n * they become known (login token, tenant). Every transferred key (httpHeader set) in the GLOBAL\n * {@link HeaderRegistry} is read from it and added to outbound requests. The registry is a process\n * global configured once at startup (like LogManager) and is browser-safe: it is the key SCHEMA,\n * not the value store.\n *\n * Example usage:\n * ```typescript\n * // startup, before bootstrap:\n * HeaderRegistry.configure(CompanyHeaders.ALL_HEADERS, true);\n *\n * const store = new MutableContextStore();\n * const factory = new ClientHttpBrowserFactory(store);\n * const client = factory.createRpcClient(SaveApi, new ClientConfig('http://api.example.com', ClientRole.SERVER));\n * ```\n */\nexport class ContextMgr {\n constructor(\n /** The app-held store context-key values are read from AND written back into. */\n public readonly contextStore: ContextStore,\n ) {}\n\n /**\n * Build the headers to send on an outbound request: every transferred key (httpHeader set)\n * with a non-empty value THAT THIS DESTINATION MAY RECEIVE, emitted under its `httpHeader`\n * wire name.\n *\n * NO request-id chaining. A browser ORIGINATES a trace — it has no inbound request to point\n * back at. If the app puts an `x-request-id` on the store it goes out as-is, and the server's\n * inbound transfer adopts it as hop 1's own id. Chaining is a server concern; see\n * RequestContextHeaders.\n *\n * `destination` gates TRUSTED keys exactly as it does on the server side (see\n * {@link DestinationTrust}). In practice a browser never reaches the permissive branch — twice\n * over: `BrowserProxyClient.assertEndpointSupported` refuses to bind an `oidc(...)` /\n * `sharedSecret(...)` contract at all, so every browser destination is `jwt()` or `@WpAuthPublic`.\n * The rule is applied here anyway rather than argued away, because the OTHER guarantee people\n * reach for — \"`MutableContextStore.set` only accepts an untrusted key, so a browser store\n * cannot HOLD a trusted value\" — is true of that store and NOT of the seam: {@link ContextMgr}\n * takes any app-supplied {@link ContextStore}, whose `read` is handed an `AnyContextKey`. One\n * enforced rule in both builders beats a browser-only exemption resting on an implementation\n * detail of one implementation.\n *\n * Values are RAW (unmasked) — this map goes on the wire, not in logs.\n */\n buildOutboundHeaders(destination: DestinationTrust): Map<string, string> {\n const outbound = new Map<string, string>();\n\n for (const key of HeaderRegistry.get().getTransferredKeys()) {\n if (!destination.allows(key)) {\n continue;\n }\n const value = this.contextStore.read(key);\n if (value !== undefined && value !== null && value !== '') {\n outbound.set(key.httpHeader!, value);\n }\n }\n\n // CLIENT_VERSION: a browser ORIGINATES a call, so it sends its OWN app build version (from\n // ServiceInfo) and the server logs which client build called it — same rule as the server-side\n // RequestContextHeaders. Our version wins; absent if this app was never identified via setInfo.\n const myVersion = ServiceInfo.getVersion();\n const clientVersionHeader = WebpiecesCoreHeaders.CLIENT_VERSION.httpHeader!;\n if (myVersion) {\n outbound.set(clientVersionHeader, myVersion);\n } else {\n outbound.delete(clientVersionHeader);\n }\n\n return outbound;\n }\n\n /**\n * RESPONSE -> the store: every key that declares a `responseHeader` and appears on a response\n * this browser just received, merged into the app-held store per the key's own\n * {@link ContextKey.responseMerge}.\n *\n * The browser twin of `RequestContextHeaders.acceptResponseHeaders`, and the reason a value set\n * by a server reaches the page without the page naming a header.\n *\n * TRUSTED keys are DROPPED here, unconditionally and without consulting `destination`, and the\n * `if` is what makes that a compile-time fact rather than a convention: {@link ContextWriter.set}\n * takes an {@link AnyUntrustedContextKey}, so a browser store CANNOT hold a proven value. That is\n * the same rule as the outbound direction reaches by a different route — every browser\n * destination is `jwt()` or `@WpAuthPublic`, so `destination.allows` would refuse them\n * anyway — and `destination` is still threaded and still applied, so a future browser destination\n * that DID authenticate its caller does not silently start believing response headers.\n */\n acceptResponseHeaders(headers: Headers, destination: DestinationTrust): void {\n for (const key of HeaderRegistry.get().getResponseTxfrKeys()) {\n const value = headers.get(key.responseHeader!);\n if (value === null || value === '') {\n continue;\n }\n if (!destination.allows(key) || key.isTrusted()) {\n // DROPPED: a browser proves nothing, so it may never hold a trusted value.\n continue;\n }\n this.contextStore.set(key, key.mergeResponseValue(this.contextStore.readValue?.(key), value));\n }\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { ClientRole } from '../errors/ClientRole';
1
2
  import { HttpResponseDto } from './HttpResponseDto';
2
3
  /**
3
4
  * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over
@@ -78,9 +79,9 @@ import { HttpResponseDto } from './HttpResponseDto';
78
79
  * );
79
80
  * }
80
81
  *
81
- * fromWire(response: HttpResponseDto): void {
82
+ * fromWire(response: HttpResponseDto, role: ClientRole): void {
82
83
  * if (response.status.code !== 460) {
83
- * this.fallback.fromWire(response); // not mine -> webpieces default
84
+ * this.fallback.fromWire(response, role); // not mine -> webpieces default
84
85
  * return;
85
86
  * }
86
87
  * throw new OrderNotFoundError(String(response.body));
@@ -105,11 +106,16 @@ export interface ErrorTranslator {
105
106
  * Called for EVERY response, including a 2xx — deliberately, so an app whose 200 body signals
106
107
  * failure can turn it into a throw. Returning normally means "let this response through"; the
107
108
  * webpieces default returns normally for any 2xx that is not 266 and throws otherwise. Decline by
108
- * calling `new WebpiecesDefaultErrorTranslator().fromWire(response)`.
109
+ * calling `new WebpiecesDefaultErrorTranslator().fromWire(response, role)`.
110
+ *
111
+ * `role` is WHO is receiving ({@link ClientRole}): the end-user client or a server. The client
112
+ * that read the response passes the role ITS setup declared (`ClientConfig`), so one translator
113
+ * registered in a process serves every client there. It decides what a received 401 means; pass
114
+ * it through unchanged when declining.
109
115
  *
110
116
  * A non-2xx response can never reach a typed caller even if an app translator forgets to throw:
111
117
  * `ClientErrorTranslator.throwIfFailure` applies the webpieces default behind it. That is bug
112
118
  * containment, not a "was one registered" branch.
113
119
  */
114
- fromWire(response: HttpResponseDto): void;
120
+ fromWire(response: HttpResponseDto, role: ClientRole): void;
115
121
  }