@webpieces/core-util 0.4.879 → 0.4.881

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 (78) hide show
  1. package/package.json +1 -1
  2. package/src/errors/ApiCallSite.d.ts +58 -0
  3. package/src/errors/ApiCallSite.js +96 -0
  4. package/src/errors/ApiCallSite.js.map +1 -0
  5. package/src/errors/ApiError.d.ts +47 -0
  6. package/src/errors/ApiError.js +46 -1
  7. package/src/errors/ApiError.js.map +1 -1
  8. package/src/errors/ApiErrorCodec.js +9 -2
  9. package/src/errors/ApiErrorCodec.js.map +1 -1
  10. package/src/errors/ClientRole.d.ts +27 -0
  11. package/src/errors/ClientRole.js +32 -0
  12. package/src/errors/ClientRole.js.map +1 -0
  13. package/src/errors/EndUserErrorCode.d.ts +22 -0
  14. package/src/errors/EndUserErrorCode.js +38 -0
  15. package/src/errors/EndUserErrorCode.js.map +1 -0
  16. package/src/errors/EndUserErrorRegistry.d.ts +61 -0
  17. package/src/errors/EndUserErrorRegistry.js +103 -0
  18. package/src/errors/EndUserErrorRegistry.js.map +1 -0
  19. package/src/errors/EndUserErrors.d.ts +28 -0
  20. package/src/errors/EndUserErrors.js +59 -0
  21. package/src/errors/EndUserErrors.js.map +1 -0
  22. package/src/errors/ReceivedApiErrorRule.d.ts +33 -3
  23. package/src/errors/ReceivedApiErrorRule.js +47 -8
  24. package/src/errors/ReceivedApiErrorRule.js.map +1 -1
  25. package/src/errors/ReportableApiFailure.d.ts +43 -0
  26. package/src/errors/ReportableApiFailure.js +75 -0
  27. package/src/errors/ReportableApiFailure.js.map +1 -0
  28. package/src/errors/index.d.ts +9 -1
  29. package/src/errors/index.js +17 -1
  30. package/src/errors/index.js.map +1 -1
  31. package/src/http/ContextMgr.d.ts +1 -1
  32. package/src/http/ContextMgr.js +1 -1
  33. package/src/http/ContextMgr.js.map +1 -1
  34. package/src/http/ErrorTranslator.d.ts +10 -4
  35. package/src/http/ErrorTranslator.js.map +1 -1
  36. package/src/http/HttpEndpointOptions.d.ts +8 -0
  37. package/src/http/HttpEndpointOptions.js.map +1 -1
  38. package/src/http/StreamErrorControl.d.ts +3 -1
  39. package/src/http/StreamErrorControl.js +4 -3
  40. package/src/http/StreamErrorControl.js.map +1 -1
  41. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +13 -2
  42. package/src/http/WebpiecesDefaultErrorTranslator.js +21 -3
  43. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
  44. package/src/http/WebpiecesDefaultFailureClassifier.d.ts +3 -2
  45. package/src/http/WebpiecesDefaultFailureClassifier.js +6 -3
  46. package/src/http/WebpiecesDefaultFailureClassifier.js.map +1 -1
  47. package/src/index.d.ts +8 -2
  48. package/src/index.js +27 -1
  49. package/src/index.js.map +1 -1
  50. package/src/ipc/IpcErrorTranslator.d.ts +6 -2
  51. package/src/ipc/IpcErrorTranslator.js.map +1 -1
  52. package/src/ipc/IpcRegistry.d.ts +4 -1
  53. package/src/ipc/IpcRegistry.js +5 -3
  54. package/src/ipc/IpcRegistry.js.map +1 -1
  55. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.d.ts +6 -2
  56. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js +6 -3
  57. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js.map +1 -1
  58. package/src/ipc/index.d.ts +1 -0
  59. package/src/ipc/index.js +3 -1
  60. package/src/ipc/index.js.map +1 -1
  61. package/src/oauth/OAuthAuthorizationErrorRedirect.d.ts +37 -0
  62. package/src/oauth/OAuthAuthorizationErrorRedirect.js +61 -0
  63. package/src/oauth/OAuthAuthorizationErrorRedirect.js.map +1 -0
  64. package/src/oauth/OAuthBearerChallenge.d.ts +31 -0
  65. package/src/oauth/OAuthBearerChallenge.js +52 -0
  66. package/src/oauth/OAuthBearerChallenge.js.map +1 -0
  67. package/src/oauth/OAuthErrorCode.d.ts +69 -0
  68. package/src/oauth/OAuthErrorCode.js +89 -0
  69. package/src/oauth/OAuthErrorCode.js.map +1 -0
  70. package/src/oauth/OAuthErrorResponse.d.ts +34 -0
  71. package/src/oauth/OAuthErrorResponse.js +74 -0
  72. package/src/oauth/OAuthErrorResponse.js.map +1 -0
  73. package/src/oauth/OAuthProtocolError.d.ts +61 -0
  74. package/src/oauth/OAuthProtocolError.js +83 -0
  75. package/src/oauth/OAuthProtocolError.js.map +1 -0
  76. package/src/oauth/OAuthProtocolErrorCompileAssertions.d.ts +10 -0
  77. package/src/oauth/OAuthProtocolErrorCompileAssertions.js +39 -0
  78. package/src/oauth/OAuthProtocolErrorCompileAssertions.js.map +1 -0
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EndUserErrorCode = void 0;
4
+ /**
5
+ * The grammar of `ApiEndUserError.errorCode`: `<code>[:<detail>]`.
6
+ *
7
+ * - `<code>` names WHICH mistake it was (`user-not-found`, `terms-not-accepted`). It is what
8
+ * {@link EndUserErrorRegistry} matches on to rebuild the subclass on the receiving side, so it must
9
+ * not itself contain a `:`.
10
+ * - `<detail>` is optional DATA the client acts on (the id that was not found, the terms version to
11
+ * accept), never prose: prose belongs in the message. Everything after the FIRST `:` is the detail,
12
+ * so a detail may itself contain colons.
13
+ *
14
+ * Kept as one tiny stateless class so a throw site, the registry and an app's own client code all
15
+ * split the string the same way.
16
+ */
17
+ class EndUserErrorCode {
18
+ static SEPARATOR = ':';
19
+ /** `code` alone, or `code:detail` when a detail is given. */
20
+ // webpieces-disable no-function-outside-class -- stateless grammar helper shared by browser and server
21
+ static format(code, detail) {
22
+ return detail === undefined ? code : `${code}${EndUserErrorCode.SEPARATOR}${detail}`;
23
+ }
24
+ /** The `<code>` part: everything before the first `:`, or the whole string when there is none. */
25
+ // webpieces-disable no-function-outside-class -- stateless grammar helper shared by browser and server
26
+ static codeOf(errorCode) {
27
+ const at = errorCode.indexOf(EndUserErrorCode.SEPARATOR);
28
+ return at < 0 ? errorCode : errorCode.slice(0, at);
29
+ }
30
+ /** The `<detail>` part: everything after the first `:`, or undefined when there is none. */
31
+ // webpieces-disable no-function-outside-class -- stateless grammar helper shared by browser and server
32
+ static detailOf(errorCode) {
33
+ const at = errorCode.indexOf(EndUserErrorCode.SEPARATOR);
34
+ return at < 0 ? undefined : errorCode.slice(at + 1);
35
+ }
36
+ }
37
+ exports.EndUserErrorCode = EndUserErrorCode;
38
+ //# sourceMappingURL=EndUserErrorCode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EndUserErrorCode.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/EndUserErrorCode.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;GAYG;AACH,MAAa,gBAAgB;IACzB,MAAM,CAAU,SAAS,GAAG,GAAG,CAAC;IAEhC,6DAA6D;IAC7D,uGAAuG;IACvG,MAAM,CAAC,MAAM,CAAC,IAAY,EAAE,MAAe;QACvC,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,gBAAgB,CAAC,SAAS,GAAG,MAAM,EAAE,CAAC;IACzF,CAAC;IAED,kGAAkG;IAClG,uGAAuG;IACvG,MAAM,CAAC,MAAM,CAAC,SAAiB;QAC3B,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACzD,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,4FAA4F;IAC5F,uGAAuG;IACvG,MAAM,CAAC,QAAQ,CAAC,SAAiB;QAC7B,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACzD,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACxD,CAAC;;AArBL,4CAsBC","sourcesContent":["/**\n * The grammar of `ApiEndUserError.errorCode`: `<code>[:<detail>]`.\n *\n * - `<code>` names WHICH mistake it was (`user-not-found`, `terms-not-accepted`). It is what\n * {@link EndUserErrorRegistry} matches on to rebuild the subclass on the receiving side, so it must\n * not itself contain a `:`.\n * - `<detail>` is optional DATA the client acts on (the id that was not found, the terms version to\n * accept), never prose: prose belongs in the message. Everything after the FIRST `:` is the detail,\n * so a detail may itself contain colons.\n *\n * Kept as one tiny stateless class so a throw site, the registry and an app's own client code all\n * split the string the same way.\n */\nexport class EndUserErrorCode {\n static readonly SEPARATOR = ':';\n\n /** `code` alone, or `code:detail` when a detail is given. */\n // webpieces-disable no-function-outside-class -- stateless grammar helper shared by browser and server\n static format(code: string, detail?: string): string {\n return detail === undefined ? code : `${code}${EndUserErrorCode.SEPARATOR}${detail}`;\n }\n\n /** The `<code>` part: everything before the first `:`, or the whole string when there is none. */\n // webpieces-disable no-function-outside-class -- stateless grammar helper shared by browser and server\n static codeOf(errorCode: string): string {\n const at = errorCode.indexOf(EndUserErrorCode.SEPARATOR);\n return at < 0 ? errorCode : errorCode.slice(0, at);\n }\n\n /** The `<detail>` part: everything after the first `:`, or undefined when there is none. */\n // webpieces-disable no-function-outside-class -- stateless grammar helper shared by browser and server\n static detailOf(errorCode: string): string | undefined {\n const at = errorCode.indexOf(EndUserErrorCode.SEPARATOR);\n return at < 0 ? undefined : errorCode.slice(at + 1);\n }\n}\n"]}
@@ -0,0 +1,61 @@
1
+ import { ApiEndUserError, EdgeHttpStatus } from './ApiError';
2
+ /**
3
+ * Rebuilds one registered {@link ApiEndUserError} subclass from what crossed the wire. It receives
4
+ * the published message and the `<detail>` half of `errorCode` (see {@link EndUserErrorCode}); the
5
+ * subclass's constructor fixes the `<code>` half itself.
6
+ */
7
+ export type EndUserErrorFactory = (message: string, detail: string | undefined) => ApiEndUserError;
8
+ /**
9
+ * errorCode -> {@link ApiEndUserError} SUBCLASS, so the receiving side of a 266 can `instanceof` WHICH
10
+ * mistake it was (send the person to the Terms page, treat a missing item as "it was deleted")
11
+ * instead of string-matching `errorCode` at every catch site.
12
+ *
13
+ * `ApiErrorCodec` consults it for every decoded `end-user` payload, which is the one decode HTTP
14
+ * (`WebpiecesDefaultErrorTranslator.fromWire`), IPC and JSONL streams all share, so a registration
15
+ * covers every protocol at once. Matching is on the `<code>` half of the `<code>[:<detail>]` grammar.
16
+ * An UNKNOWN code decodes to the base `ApiEndUserError` with `errorCode` and `edgeHttpStatus` intact:
17
+ * forward compatible, an old client receiving a new code still shows the message.
18
+ *
19
+ * REGISTER ONCE AT STARTUP, in EVERY process that may receive the error: the browser bundle, the
20
+ * Expo shell and every server. A server needs it too, because a server that receives a 266 from a
21
+ * peer rethrows the decoded error to its own caller, and only a decoded subclass is still that
22
+ * subclass when the next hop catches it.
23
+ *
24
+ * ```ts
25
+ * export class TermsNotAcceptedError extends ApiEndUserError {
26
+ * static readonly CODE = 'terms-not-accepted';
27
+ * constructor(message: string, detail?: string, cause?: Error) {
28
+ * super(message, EndUserErrorCode.format(TermsNotAcceptedError.CODE, detail), undefined, cause);
29
+ * }
30
+ * }
31
+ *
32
+ * // startup, ONCE per process (browser AND server)
33
+ * EndUserErrorRegistry.register(TermsNotAcceptedError.CODE,
34
+ * (message: string, detail: string | undefined) => new TermsNotAcceptedError(message, detail));
35
+ * ```
36
+ *
37
+ * The webpieces generics ({@link ApiEndUserNotFoundError}, {@link ApiEndUserForbiddenError},
38
+ * {@link ApiEndUserBadRequestError}) are PRE-REGISTERED here, so they decode typed with no setup.
39
+ * App-specific subclasses stay in the app.
40
+ *
41
+ * Process-global and static, like `ClientRegistry` / `HeaderRegistry`: the codec that consults it is
42
+ * a static, protocol-neutral decoder reached from the browser, node and IPC alike, and a registration
43
+ * is a fact about the PROCESS's error taxonomy, not about any one client.
44
+ */
45
+ export declare class EndUserErrorRegistry {
46
+ private static readonly factories;
47
+ /**
48
+ * Register the subclass for `code`. `code` is the bare `<code>` (no `:`), normally the subclass's
49
+ * static `CODE`. Registering a code twice THROWS: two classes claiming one code is a collision
50
+ * that would otherwise decode to whichever registered last, depending on startup order.
51
+ */
52
+ static register(code: string, factory: EndUserErrorFactory): void;
53
+ /**
54
+ * The decoded end-user error for a received payload: the registered subclass for `errorCode`'s
55
+ * `<code>`, else the base {@link ApiEndUserError} carrying everything the wire sent.
56
+ */
57
+ static create(message: string, errorCode?: string, edgeHttpStatus?: EdgeHttpStatus): ApiEndUserError;
58
+ /** Back to a fresh process: the webpieces built-ins only. For TESTS, so specs cannot leak. */
59
+ static resetForTests(): void;
60
+ private static builtIns;
61
+ }
@@ -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"]}
@@ -0,0 +1,43 @@
1
+ import { ApiCallSite } from './ApiCallSite';
2
+ /**
3
+ * The report-time wrapper for a failed api call: named `RpcCallFailed`, message
4
+ * `Api.method failed: <original message>`, the CALL SITE's stack, and `cause` = the original error.
5
+ *
6
+ * NEVER THROWN. It exists only to be handed to an error reporter, so that the reported stack is the
7
+ * app line that made the call while the original failure (and its own framework stack) is still
8
+ * one `cause` hop away. Here `cause` is honest — the call failed BECAUSE of the original error —
9
+ * which is exactly why the call site itself is not stored as the original's `cause`.
10
+ */
11
+ export declare class RpcCallFailed extends Error {
12
+ readonly callSite: ApiCallSite;
13
+ constructor(callSite: ApiCallSite, original: Error);
14
+ /**
15
+ * The call site's frames without its own header line. V8 prefixes a stack with
16
+ * `Name: message`; SpiderMonkey and JavaScriptCore do not, so only a matching first line is cut.
17
+ */
18
+ private static framesOf;
19
+ }
20
+ /**
21
+ * What an error reporter needs to report a failed api call well: the {@link RpcCallFailed} wrapper
22
+ * and a suggested grouping `fingerprint` of `[original.name, 'Api.method']`, so failures group per
23
+ * endpoint and error type instead of collapsing into one issue for the whole app.
24
+ *
25
+ * Reporter-agnostic on purpose — webpieces does not depend on any error-reporting SDK. A Sentry app,
26
+ * for example, would do:
27
+ *
28
+ * ```ts
29
+ * const report = ReportableApiFailure.toReportableError(err);
30
+ * if (report) Sentry.captureException(report.error, { fingerprint: [...report.fingerprint] });
31
+ * else Sentry.captureException(err);
32
+ * ```
33
+ */
34
+ export declare class ReportableApiFailure {
35
+ readonly error: RpcCallFailed;
36
+ readonly fingerprint: readonly string[];
37
+ constructor(error: RpcCallFailed, fingerprint: readonly string[]);
38
+ /**
39
+ * The reportable form of `failure`, or undefined when it carries no call site (it did not come
40
+ * out of a generated api client, so there is nothing to add — report it as it is).
41
+ */
42
+ static toReportableError(failure: unknown): ReportableApiFailure | undefined;
43
+ }