@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.
- package/package.json +1 -1
- package/src/errors/ApiCallSite.d.ts +58 -0
- package/src/errors/ApiCallSite.js +96 -0
- package/src/errors/ApiCallSite.js.map +1 -0
- package/src/errors/ApiError.d.ts +47 -0
- package/src/errors/ApiError.js +46 -1
- package/src/errors/ApiError.js.map +1 -1
- package/src/errors/ApiErrorCodec.js +9 -2
- package/src/errors/ApiErrorCodec.js.map +1 -1
- package/src/errors/ClientRole.d.ts +27 -0
- package/src/errors/ClientRole.js +32 -0
- package/src/errors/ClientRole.js.map +1 -0
- package/src/errors/EndUserErrorCode.d.ts +22 -0
- package/src/errors/EndUserErrorCode.js +38 -0
- package/src/errors/EndUserErrorCode.js.map +1 -0
- package/src/errors/EndUserErrorRegistry.d.ts +61 -0
- package/src/errors/EndUserErrorRegistry.js +103 -0
- package/src/errors/EndUserErrorRegistry.js.map +1 -0
- package/src/errors/EndUserErrors.d.ts +28 -0
- package/src/errors/EndUserErrors.js +59 -0
- package/src/errors/EndUserErrors.js.map +1 -0
- package/src/errors/ReceivedApiErrorRule.d.ts +33 -3
- package/src/errors/ReceivedApiErrorRule.js +47 -8
- package/src/errors/ReceivedApiErrorRule.js.map +1 -1
- package/src/errors/ReportableApiFailure.d.ts +43 -0
- package/src/errors/ReportableApiFailure.js +75 -0
- package/src/errors/ReportableApiFailure.js.map +1 -0
- package/src/errors/index.d.ts +9 -1
- package/src/errors/index.js +17 -1
- package/src/errors/index.js.map +1 -1
- package/src/http/ContextMgr.d.ts +1 -1
- package/src/http/ContextMgr.js +1 -1
- package/src/http/ContextMgr.js.map +1 -1
- package/src/http/ErrorTranslator.d.ts +10 -4
- package/src/http/ErrorTranslator.js.map +1 -1
- package/src/http/HttpEndpointOptions.d.ts +8 -0
- package/src/http/HttpEndpointOptions.js.map +1 -1
- package/src/http/StreamErrorControl.d.ts +3 -1
- package/src/http/StreamErrorControl.js +4 -3
- package/src/http/StreamErrorControl.js.map +1 -1
- package/src/http/WebpiecesDefaultErrorTranslator.d.ts +13 -2
- package/src/http/WebpiecesDefaultErrorTranslator.js +21 -3
- package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
- package/src/http/WebpiecesDefaultFailureClassifier.d.ts +3 -2
- package/src/http/WebpiecesDefaultFailureClassifier.js +6 -3
- package/src/http/WebpiecesDefaultFailureClassifier.js.map +1 -1
- package/src/index.d.ts +8 -2
- package/src/index.js +27 -1
- package/src/index.js.map +1 -1
- package/src/ipc/IpcErrorTranslator.d.ts +6 -2
- package/src/ipc/IpcErrorTranslator.js.map +1 -1
- package/src/ipc/IpcRegistry.d.ts +4 -1
- package/src/ipc/IpcRegistry.js +5 -3
- package/src/ipc/IpcRegistry.js.map +1 -1
- package/src/ipc/WebpiecesDefaultIpcErrorTranslator.d.ts +6 -2
- package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js +6 -3
- package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js.map +1 -1
- package/src/ipc/index.d.ts +1 -0
- package/src/ipc/index.js +3 -1
- package/src/ipc/index.js.map +1 -1
- package/src/oauth/OAuthAuthorizationErrorRedirect.d.ts +37 -0
- package/src/oauth/OAuthAuthorizationErrorRedirect.js +61 -0
- package/src/oauth/OAuthAuthorizationErrorRedirect.js.map +1 -0
- package/src/oauth/OAuthBearerChallenge.d.ts +31 -0
- package/src/oauth/OAuthBearerChallenge.js +52 -0
- package/src/oauth/OAuthBearerChallenge.js.map +1 -0
- package/src/oauth/OAuthErrorCode.d.ts +69 -0
- package/src/oauth/OAuthErrorCode.js +89 -0
- package/src/oauth/OAuthErrorCode.js.map +1 -0
- package/src/oauth/OAuthErrorResponse.d.ts +34 -0
- package/src/oauth/OAuthErrorResponse.js +74 -0
- package/src/oauth/OAuthErrorResponse.js.map +1 -0
- package/src/oauth/OAuthProtocolError.d.ts +61 -0
- package/src/oauth/OAuthProtocolError.js +83 -0
- package/src/oauth/OAuthProtocolError.js.map +1 -0
- package/src/oauth/OAuthProtocolErrorCompileAssertions.d.ts +10 -0
- package/src/oauth/OAuthProtocolErrorCompileAssertions.js +39 -0
- package/src/oauth/OAuthProtocolErrorCompileAssertions.js.map +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReportableApiFailure = exports.RpcCallFailed = void 0;
|
|
4
|
+
const ApiCallSite_1 = require("./ApiCallSite");
|
|
5
|
+
/**
|
|
6
|
+
* The report-time wrapper for a failed api call: named `RpcCallFailed`, message
|
|
7
|
+
* `Api.method failed: <original message>`, the CALL SITE's stack, and `cause` = the original error.
|
|
8
|
+
*
|
|
9
|
+
* NEVER THROWN. It exists only to be handed to an error reporter, so that the reported stack is the
|
|
10
|
+
* app line that made the call while the original failure (and its own framework stack) is still
|
|
11
|
+
* one `cause` hop away. Here `cause` is honest — the call failed BECAUSE of the original error —
|
|
12
|
+
* which is exactly why the call site itself is not stored as the original's `cause`.
|
|
13
|
+
*/
|
|
14
|
+
class RpcCallFailed extends Error {
|
|
15
|
+
callSite;
|
|
16
|
+
constructor(callSite, original) {
|
|
17
|
+
super(`${callSite.label} failed: ${original.message}`, { cause: original });
|
|
18
|
+
this.callSite = callSite;
|
|
19
|
+
this.name = 'RpcCallFailed';
|
|
20
|
+
this.stack = `${this.name}: ${this.message}\n${RpcCallFailed.framesOf(callSite)}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The call site's frames without its own header line. V8 prefixes a stack with
|
|
24
|
+
* `Name: message`; SpiderMonkey and JavaScriptCore do not, so only a matching first line is cut.
|
|
25
|
+
*/
|
|
26
|
+
// webpieces-disable no-function-outside-class -- pure stack-text helper owned by this class
|
|
27
|
+
static framesOf(callSite) {
|
|
28
|
+
const stack = callSite.stack ?? '';
|
|
29
|
+
const header = `${callSite.name}: ${callSite.message}`;
|
|
30
|
+
if (!stack.startsWith(header)) {
|
|
31
|
+
return stack;
|
|
32
|
+
}
|
|
33
|
+
return stack.slice(header.length).replace(/^\n/, '');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.RpcCallFailed = RpcCallFailed;
|
|
37
|
+
/**
|
|
38
|
+
* What an error reporter needs to report a failed api call well: the {@link RpcCallFailed} wrapper
|
|
39
|
+
* and a suggested grouping `fingerprint` of `[original.name, 'Api.method']`, so failures group per
|
|
40
|
+
* endpoint and error type instead of collapsing into one issue for the whole app.
|
|
41
|
+
*
|
|
42
|
+
* Reporter-agnostic on purpose — webpieces does not depend on any error-reporting SDK. A Sentry app,
|
|
43
|
+
* for example, would do:
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* const report = ReportableApiFailure.toReportableError(err);
|
|
47
|
+
* if (report) Sentry.captureException(report.error, { fingerprint: [...report.fingerprint] });
|
|
48
|
+
* else Sentry.captureException(err);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
class ReportableApiFailure {
|
|
52
|
+
error;
|
|
53
|
+
fingerprint;
|
|
54
|
+
constructor(error, fingerprint) {
|
|
55
|
+
this.error = error;
|
|
56
|
+
this.fingerprint = fingerprint;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The reportable form of `failure`, or undefined when it carries no call site (it did not come
|
|
60
|
+
* out of a generated api client, so there is nothing to add — report it as it is).
|
|
61
|
+
*/
|
|
62
|
+
// webpieces-disable no-function-outside-class -- stateless factory over a caught value; webpieces-disable no-any-unknown -- a caught value is genuinely unknown until narrowed here
|
|
63
|
+
static toReportableError(failure) {
|
|
64
|
+
const callSite = ApiCallSite_1.ApiCallSite.of(failure);
|
|
65
|
+
if (callSite === undefined || !(failure instanceof Error)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return new ReportableApiFailure(new RpcCallFailed(callSite, failure), [
|
|
69
|
+
failure.name,
|
|
70
|
+
callSite.label,
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.ReportableApiFailure = ReportableApiFailure;
|
|
75
|
+
//# sourceMappingURL=ReportableApiFailure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReportableApiFailure.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ReportableApiFailure.ts"],"names":[],"mappings":";;;AAAA,+CAA4C;AAE5C;;;;;;;;GAQG;AACH,MAAa,aAAc,SAAQ,KAAK;IAEhB;IADpB,YACoB,QAAqB,EACrC,QAAe;QAEf,KAAK,CAAC,GAAG,QAAQ,CAAC,KAAK,YAAY,QAAQ,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAH5D,aAAQ,GAAR,QAAQ,CAAa;QAIrC,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,OAAO,KAAK,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;IACtF,CAAC;IAED;;;OAGG;IACH,4FAA4F;IACpF,MAAM,CAAC,QAAQ,CAAC,QAAqB;QACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,KAAK,QAAQ,CAAC,OAAO,EAAE,CAAC;QACvD,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,CAAC;CACJ;AAvBD,sCAuBC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAa,oBAAoB;IAET;IACA;IAFpB,YACoB,KAAoB,EACpB,WAA8B;QAD9B,UAAK,GAAL,KAAK,CAAe;QACpB,gBAAW,GAAX,WAAW,CAAmB;IAC/C,CAAC;IAEJ;;;OAGG;IACH,oLAAoL;IACpL,MAAM,CAAC,iBAAiB,CAAC,OAAgB;QACrC,MAAM,QAAQ,GAAG,yBAAW,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,YAAY,KAAK,CAAC,EAAE,CAAC;YACxD,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,OAAO,IAAI,oBAAoB,CAAC,IAAI,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE;YAClE,OAAO,CAAC,IAAI;YACZ,QAAQ,CAAC,KAAK;SACjB,CAAC,CAAC;IACP,CAAC;CACJ;AArBD,oDAqBC","sourcesContent":["import { ApiCallSite } from './ApiCallSite';\n\n/**\n * The report-time wrapper for a failed api call: named `RpcCallFailed`, message\n * `Api.method failed: <original message>`, the CALL SITE's stack, and `cause` = the original error.\n *\n * NEVER THROWN. It exists only to be handed to an error reporter, so that the reported stack is the\n * app line that made the call while the original failure (and its own framework stack) is still\n * one `cause` hop away. Here `cause` is honest — the call failed BECAUSE of the original error —\n * which is exactly why the call site itself is not stored as the original's `cause`.\n */\nexport class RpcCallFailed extends Error {\n constructor(\n public readonly callSite: ApiCallSite,\n original: Error,\n ) {\n super(`${callSite.label} failed: ${original.message}`, { cause: original });\n this.name = 'RpcCallFailed';\n this.stack = `${this.name}: ${this.message}\\n${RpcCallFailed.framesOf(callSite)}`;\n }\n\n /**\n * The call site's frames without its own header line. V8 prefixes a stack with\n * `Name: message`; SpiderMonkey and JavaScriptCore do not, so only a matching first line is cut.\n */\n // webpieces-disable no-function-outside-class -- pure stack-text helper owned by this class\n private static framesOf(callSite: ApiCallSite): string {\n const stack = callSite.stack ?? '';\n const header = `${callSite.name}: ${callSite.message}`;\n if (!stack.startsWith(header)) {\n return stack;\n }\n return stack.slice(header.length).replace(/^\\n/, '');\n }\n}\n\n/**\n * What an error reporter needs to report a failed api call well: the {@link RpcCallFailed} wrapper\n * and a suggested grouping `fingerprint` of `[original.name, 'Api.method']`, so failures group per\n * endpoint and error type instead of collapsing into one issue for the whole app.\n *\n * Reporter-agnostic on purpose — webpieces does not depend on any error-reporting SDK. A Sentry app,\n * for example, would do:\n *\n * ```ts\n * const report = ReportableApiFailure.toReportableError(err);\n * if (report) Sentry.captureException(report.error, { fingerprint: [...report.fingerprint] });\n * else Sentry.captureException(err);\n * ```\n */\nexport class ReportableApiFailure {\n constructor(\n public readonly error: RpcCallFailed,\n public readonly fingerprint: readonly string[],\n ) {}\n\n /**\n * The reportable form of `failure`, or undefined when it carries no call site (it did not come\n * out of a generated api client, so there is nothing to add — report it as it is).\n */\n // webpieces-disable no-function-outside-class -- stateless factory over a caught value; webpieces-disable no-any-unknown -- a caught value is genuinely unknown until narrowed here\n static toReportableError(failure: unknown): ReportableApiFailure | undefined {\n const callSite = ApiCallSite.of(failure);\n if (callSite === undefined || !(failure instanceof Error)) {\n return undefined;\n }\n return new ReportableApiFailure(new RpcCallFailed(callSite, failure), [\n failure.name,\n callSite.label,\n ]);\n }\n}\n"]}
|
package/src/errors/index.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
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';
|
|
7
|
+
export { ApiCallSite } from './ApiCallSite';
|
|
8
|
+
export type { CallSiteBoundary } from './ApiCallSite';
|
|
9
|
+
export { ReportableApiFailure, RpcCallFailed } from './ReportableApiFailure';
|
|
2
10
|
export type { ApiErrorKind, ApiStatusCode, EdgeHttpStatus } from './ApiError';
|
|
3
11
|
export { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';
|
|
4
12
|
export { ApiErrorBoundary } from './ApiErrorBoundary';
|
package/src/errors/index.js
CHANGED
|
@@ -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.RpcCallFailed = exports.ReportableApiFailure = exports.ApiCallSite = 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,22 @@ 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; } });
|
|
38
|
+
var ApiCallSite_1 = require("./ApiCallSite");
|
|
39
|
+
Object.defineProperty(exports, "ApiCallSite", { enumerable: true, get: function () { return ApiCallSite_1.ApiCallSite; } });
|
|
40
|
+
var ReportableApiFailure_1 = require("./ReportableApiFailure");
|
|
41
|
+
Object.defineProperty(exports, "ReportableApiFailure", { enumerable: true, get: function () { return ReportableApiFailure_1.ReportableApiFailure; } });
|
|
42
|
+
Object.defineProperty(exports, "RpcCallFailed", { enumerable: true, get: function () { return ReportableApiFailure_1.RpcCallFailed; } });
|
|
27
43
|
var ApiErrorCodec_1 = require("./ApiErrorCodec");
|
|
28
44
|
Object.defineProperty(exports, "ApiErrorCodec", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorCodec; } });
|
|
29
45
|
Object.defineProperty(exports, "ApiErrorPayload", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorPayload; } });
|
package/src/errors/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,
|
|
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;AACnB,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,+DAA6E;AAApE,4HAAA,oBAAoB,OAAA;AAAE,qHAAA,aAAa,OAAA;AAE5C,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 { ApiCallSite } from './ApiCallSite';\nexport type { CallSiteBoundary } from './ApiCallSite';\nexport { ReportableApiFailure, RpcCallFailed } from './ReportableApiFailure';\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"]}
|
package/src/http/ContextMgr.d.ts
CHANGED
|
@@ -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 {
|
package/src/http/ContextMgr.js
CHANGED
|
@@ -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);
|
|
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
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorTranslator.ts"],"names":[],"mappings":"","sourcesContent":["import { HttpResponseDto } from './HttpResponseDto';\n\n/**\n * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over\n * the WHOLE response.\n *\n * An app implements this ONCE and registers it ONCE per process via\n * {@link ClientRegistry.setErrorTranslator} — on the server AND in the browser. Its `toWire` runs on\n * the SERVER (`ExpressWrapper.handleError`) and its `fromWire` runs on every CLIENT in that process\n * (`ClientErrorTranslator.throwIfFailure`, shared by `http-client-node` and `http-client-browser`).\n * The payoff is type symmetry across the wire: the server throws `OrderNotFoundError` and the caller\n * CATCHES `OrderNotFoundError`, instead of decoding a status code by hand at every call site.\n *\n * # THERE IS ALWAYS A TRANSLATOR\n *\n * {@link ClientRegistry.getErrorTranslator} is non-optional and starts life holding\n * {@link WebpiecesDefaultErrorTranslator}. So no caller anywhere asks \"did anyone register one\" —\n * every call site is ONE unconditional line. A registered app translator REPLACES the default and\n * declines by DELEGATING to it:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * That is one call, it is visible at the call site, and it composes: an app that wants to DECORATE\n * the default rather than replace it does so by calling it and editing the result. A `next` parameter\n * was considered and rejected — it buys nothing this does not, and it puts the framework back in the\n * business of sequencing somebody else's precedence.\n *\n * # Both halves speak {@link HttpResponseDto}, and that is the point\n *\n * `toWire` PRODUCES exactly what `fromWire` CONSUMES. Reading the two methods against each other in\n * one file is what makes a mistake visible, which is why this is one object rather than two\n * separately-registered functions.\n *\n * # The whole response, not a status plus a body\n *\n * The previous contract could express only `(statusCode, protocolError)`, so an app could not put its\n * own trace id, `Retry-After`, `WWW-Authenticate` or a cookie on an error response. A translator now\n * returns the status code, the reason phrase, the header LIST and the body — everything.\n *\n * # `toWire` RETURNS a response; `fromWire` THROWS\n *\n * They are mirrors, not twins. `toWire` is called inside a catch and its whole job is to produce a\n * value, so it returns one. `fromWire` is called on a response the caller is about to be handed, and\n * its whole job is to stop that from happening, so it THROWS and is typed `void`. Returning an\n * `Error` the caller then has to remember to throw was the previous shape and it is exactly the bug\n * class this deletes: a `void` return cannot be silently dropped.\n *\n * There is exactly ONE registered ErrorTranslator per process (a `set`, not an `add` to a list):\n * precedence between an app's own layers is the APP's to compose explicitly inside its `toWire`,\n * rather than something hidden in registration order inside the framework.\n *\n * # Not covered: 404 / unknown route\n *\n * A request that matches no route has no route context and never reaches this seam. That is\n * deliberate and out of scope.\n *\n * This is a business-logic contract (methods, not data), so it is an interface per the webpieces\n * guidelines.\n *\n * ```ts\n * class OrderErrorPayload {\n * constructor(public message: string, public errorCode: string) {}\n * }\n *\n * export class OrderErrorTranslator implements ErrorTranslator {\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n *\n * toWire(error: Error): HttpResponseDto {\n * if (!(error instanceof OrderNotFoundError)) {\n * return this.fallback.toWire(error); // not mine -> webpieces default\n * }\n * const body = new OrderErrorPayload(error.message, 'ORDER_NOT_FOUND');\n * return new HttpResponseDto(\n * new HttpResponseStatus(460, 'Order Not Found'),\n * [new HttpHeader('x-order-trace', error.traceId)],\n * body,\n * );\n * }\n *\n * fromWire(response: HttpResponseDto): void {\n * if (response.status.code !== 460) {\n * this.fallback.fromWire(response);
|
|
1
|
+
{"version":3,"file":"ErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorTranslator.ts"],"names":[],"mappings":"","sourcesContent":["import { ClientRole } from '../errors/ClientRole';\nimport { HttpResponseDto } from './HttpResponseDto';\n\n/**\n * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over\n * the WHOLE response.\n *\n * An app implements this ONCE and registers it ONCE per process via\n * {@link ClientRegistry.setErrorTranslator} — on the server AND in the browser. Its `toWire` runs on\n * the SERVER (`ExpressWrapper.handleError`) and its `fromWire` runs on every CLIENT in that process\n * (`ClientErrorTranslator.throwIfFailure`, shared by `http-client-node` and `http-client-browser`).\n * The payoff is type symmetry across the wire: the server throws `OrderNotFoundError` and the caller\n * CATCHES `OrderNotFoundError`, instead of decoding a status code by hand at every call site.\n *\n * # THERE IS ALWAYS A TRANSLATOR\n *\n * {@link ClientRegistry.getErrorTranslator} is non-optional and starts life holding\n * {@link WebpiecesDefaultErrorTranslator}. So no caller anywhere asks \"did anyone register one\" —\n * every call site is ONE unconditional line. A registered app translator REPLACES the default and\n * declines by DELEGATING to it:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * That is one call, it is visible at the call site, and it composes: an app that wants to DECORATE\n * the default rather than replace it does so by calling it and editing the result. A `next` parameter\n * was considered and rejected — it buys nothing this does not, and it puts the framework back in the\n * business of sequencing somebody else's precedence.\n *\n * # Both halves speak {@link HttpResponseDto}, and that is the point\n *\n * `toWire` PRODUCES exactly what `fromWire` CONSUMES. Reading the two methods against each other in\n * one file is what makes a mistake visible, which is why this is one object rather than two\n * separately-registered functions.\n *\n * # The whole response, not a status plus a body\n *\n * The previous contract could express only `(statusCode, protocolError)`, so an app could not put its\n * own trace id, `Retry-After`, `WWW-Authenticate` or a cookie on an error response. A translator now\n * returns the status code, the reason phrase, the header LIST and the body — everything.\n *\n * # `toWire` RETURNS a response; `fromWire` THROWS\n *\n * They are mirrors, not twins. `toWire` is called inside a catch and its whole job is to produce a\n * value, so it returns one. `fromWire` is called on a response the caller is about to be handed, and\n * its whole job is to stop that from happening, so it THROWS and is typed `void`. Returning an\n * `Error` the caller then has to remember to throw was the previous shape and it is exactly the bug\n * class this deletes: a `void` return cannot be silently dropped.\n *\n * There is exactly ONE registered ErrorTranslator per process (a `set`, not an `add` to a list):\n * precedence between an app's own layers is the APP's to compose explicitly inside its `toWire`,\n * rather than something hidden in registration order inside the framework.\n *\n * # Not covered: 404 / unknown route\n *\n * A request that matches no route has no route context and never reaches this seam. That is\n * deliberate and out of scope.\n *\n * This is a business-logic contract (methods, not data), so it is an interface per the webpieces\n * guidelines.\n *\n * ```ts\n * class OrderErrorPayload {\n * constructor(public message: string, public errorCode: string) {}\n * }\n *\n * export class OrderErrorTranslator implements ErrorTranslator {\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n *\n * toWire(error: Error): HttpResponseDto {\n * if (!(error instanceof OrderNotFoundError)) {\n * return this.fallback.toWire(error); // not mine -> webpieces default\n * }\n * const body = new OrderErrorPayload(error.message, 'ORDER_NOT_FOUND');\n * return new HttpResponseDto(\n * new HttpResponseStatus(460, 'Order Not Found'),\n * [new HttpHeader('x-order-trace', error.traceId)],\n * body,\n * );\n * }\n *\n * fromWire(response: HttpResponseDto, role: ClientRole): void {\n * if (response.status.code !== 460) {\n * this.fallback.fromWire(response, role); // not mine -> webpieces default\n * return;\n * }\n * throw new OrderNotFoundError(String(response.body));\n * }\n * }\n *\n * // startup, ONCE per process (server AND browser)\n * ClientRegistry.setErrorTranslator(new OrderErrorTranslator());\n * ```\n */\nexport interface ErrorTranslator {\n /**\n * SERVER: exception -> the ENTIRE response (status code, reason phrase, headers, body). Always a\n * response, never `undefined`; decline by returning\n * `new WebpiecesDefaultErrorTranslator().toWire(error)`.\n */\n toWire(error: Error): HttpResponseDto;\n\n /**\n * CLIENT: the ENTIRE response -> a THROW. Receives the SAME shape `toWire` produces, normalised\n * from whichever transport read it.\n *\n * Called for EVERY response, including a 2xx — deliberately, so an app whose 200 body signals\n * failure can turn it into a throw. Returning normally means \"let this response through\"; the\n * webpieces default returns normally for any 2xx that is not 266 and throws otherwise. Decline by\n * calling `new WebpiecesDefaultErrorTranslator().fromWire(response, role)`.\n *\n * `role` is WHO is receiving ({@link ClientRole}): the end-user client or a server. The client\n * that read the response passes the role ITS setup declared (`ClientConfig`), so one translator\n * registered in a process serves every client there. It decides what a received 401 means; pass\n * it through unchanged when declining.\n *\n * A non-2xx response can never reach a typed caller even if an app translator forgets to throw:\n * `ClientErrorTranslator.throwIfFailure` applies the webpieces default behind it. That is bug\n * containment, not a \"was one registered\" branch.\n */\n fromWire(response: HttpResponseDto, role: ClientRole): void;\n}\n"]}
|
|
@@ -86,6 +86,14 @@ export interface EndpointOptions {
|
|
|
86
86
|
* webpieces only declares and carries it (`RouteMetadata.allowUpgradeInFlight`); the app's
|
|
87
87
|
* `RequestLifecycleListener` / upgrade logic decides what to do with it. Default false.
|
|
88
88
|
* `grep -rn allowUpgradeInFlight` lists every endpoint an upgrade may cut off.
|
|
89
|
+
*
|
|
90
|
+
* THE SERVER'S HALF of the upgrade story is `ApiClientTooOldError` (HTTP 426,
|
|
91
|
+
* `errorCode: 'client-too-old'`). This flag is the CLIENT deciding WHEN it may upgrade; that
|
|
92
|
+
* error is a SERVER saying the client MUST. Who throws it: a server gate that compares the build
|
|
93
|
+
* the caller declared (a header, a version in the request) against the minimum it still
|
|
94
|
+
* supports. Receivers decode it back into `ApiClientTooOldError` on every hop, server-to-server
|
|
95
|
+
* and browser, HTTP and IPC, and treat it as an upgrade, never as a bug: the app's upgrade logic
|
|
96
|
+
* reloads or installs the new build, typically honouring this flag for the calls still in flight.
|
|
89
97
|
*/
|
|
90
98
|
allowUpgradeInFlight?: boolean;
|
|
91
99
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpEndpointOptions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpEndpointOptions.ts"],"names":[],"mappings":";;;AAGA,8EAA8E;AAC9E,IAAK,sBAIJ;AAJD,WAAK,sBAAsB;IACvB,uCAAa,CAAA;IACb,+DAAqC,CAAA;IACrC,yCAAe,CAAA;AACnB,CAAC,EAJI,sBAAsB,KAAtB,sBAAsB,QAI1B;AAED,wDAAwD;AAC3C,QAAA,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC;AACnC,QAAA,gBAAgB,GAAG,sBAAsB,CAAC,gBAAgB,CAAC;AAC3D,QAAA,KAAK,GAAG,sBAAsB,CAAC,KAAK,CAAC","sourcesContent":["import { ExternalSystemKind } from './external-caller';\nimport { EndpointResponseType } from './HttpContract';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum EndpointOperationValue {\n READ = 'read',\n WRITE_IDEMPOTENT = 'write-idempotent',\n WRITE = 'write',\n}\n\n/** Short, statically importable decorator arguments. */\nexport const READ = EndpointOperationValue.READ;\nexport const WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;\nexport const WRITE = EndpointOperationValue.WRITE;\n\n/** The side-effect contract callers use to decide whether repeating an operation is safe. */\nexport type EndpointOperation = typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE;\n\n/** Options for one `@Endpoint`, stored parallel to the method-to-path metadata. */\nexport interface EndpointOptions {\n /** Return only a JSON body (default), or the full transport-neutral status/header/body value. */\n responseType?: EndpointResponseType;\n /**\n * Parse the request body as application/x-www-form-urlencoded instead of JSON. The request\n * DTO must be flat; urlencoded has no nesting. Default false = JSON.\n */\n formPost?: boolean;\n /**\n * Retain the verbatim request bytes and absolute URL for a `webhook(...)` callback to verify.\n * This is retention, not new buffering: the Express adapter already accumulates the body.\n *\n * `webhook(...)` requires this option at wiring time. It combines with `formPost` for vendors\n * that sign flat form bodies, e.g. `{ formPost: true, rawBody: true }`.\n */\n rawBody?: boolean;\n /**\n * The app should show NO progress UI for this call — it is not something the USER is waiting on\n * (a background download, a log shipper, a heartbeat, a telemetry flush).\n *\n * webpieces draws no progress bar itself; it DECLARES the fact and carries it on\n * `RouteMetadata.hideProgress`, which an app's `RequestLifecycleListener` reads to keep the call\n * out of its bar. WHY a declaration on the endpoint: the alternative is the app matching path\n * strings in its listener, which silently drifts the first time a route is renamed. The ROUTE is\n * the stable fact, so the decision belongs to the route's author.\n *\n * Independent of {@link noLogging} and {@link allowUpgradeInFlight}: a background download hides\n * its progress yet still wants its `[API-*]` lines (exactly what you need when a sync is slow)\n * and still must finish before an app upgrades under it.\n *\n * Default false: absent means the call drives the app's progress UI, like any ordinary route.\n * `grep -rn hideProgress` lists every endpoint the user is not shown waiting on.\n */\n hideProgress?: boolean;\n /**\n * webpieces writes NO `[API-*-req]` / `[API-*-resp-*]` line for this call, on either side —\n * {@link LogApiCallImpl} reads it via `ApiMethodInfo.noLogging`, handed in by both the client\n * proxy and the server's `LogApiFilter` — and stamps no per-call `api` tag.\n *\n * WHY a declaration and not a suppression switch: shipping a log over a logged transport logs\n * about shipping logs — the line describing the call IS the thing being shipped, and emitting it\n * feeds the next batch its own payload. The two mechanisms that do not know the endpoint both\n * failed in prod (see #976) — a wall-clock \"we are shipping\" boolean swallowed unrelated lines\n * including an error alarm, and a logger-name allowlist was incomplete by construction the moment\n * anyone added a logger, producing a steady-state 4-lines-per-second request loop on an idle page.\n * The ENDPOINT is the only stable, declarative fact, so it is where the decision belongs.\n *\n * Rides the route metadata for the same reason {@link formPost} and {@link rawBody} do: the\n * consumer branches on the ROUTE, without knowing the apiClass/methodName.\n *\n * Default false: absent means the ordinary, fully-logged route. The quiet path is the one you\n * have to say out loud, so `grep -rn noLogging` lists every endpoint webpieces stops logging.\n */\n noLogging?: boolean;\n /**\n * An app MAY upgrade (reload onto a newer deployed bundle) while this call is in flight, and the\n * call neither blocks nor arms that upgrade.\n *\n * By default every RPC participates in the app's upgrade decision: the app must not upgrade while\n * it is in flight, and it counts toward the \"finished with a SUCCESS result\" boundary at which an\n * app typically arms its seamless upgrade. That default is the safe one — reloading under an\n * in-flight write or download loses it. A call nobody asked for, and that is harmless to cut off\n * (a log shipper, a heartbeat), declares `true` so it can neither hold an upgrade hostage nor be\n * the success that triggers one.\n *\n * WHY separate from {@link hideProgress}: apps commonly arm an upgrade off their progress bar's\n * request group, so hiding a call from the bar used to drop it out of the upgrade decision as a\n * side effect. A hidden background download must still block an upgrade until it succeeds; only\n * this flag, said out loud, lets a call stop participating.\n *\n * webpieces only declares and carries it (`RouteMetadata.allowUpgradeInFlight`); the app's\n * `RequestLifecycleListener` / upgrade logic decides what to do with it. Default false.\n * `grep -rn allowUpgradeInFlight` lists every endpoint an upgrade may cut off.\n */\n allowUpgradeInFlight?: boolean;\n /**\n * This route is NOT part of the CUSTOMER-facing contract. `public-openapi.json` — the document a\n * customer gets, and the only one a publish path uploads — contains no path, no operation, no\n * schema and no prose for it. It is not \"rendered with a flag set\", it is absent.\n *\n * It is still in `full-private-openapi.json`, and still in `mcp-openapi.json` when it carries\n * `@WpMcpTool`. The reason it is withheld belongs in the method's JSDoc, which is where every\n * other piece of its documentation already lives.\n *\n * WHY A BOOLEAN HERE IS NOT SHIM SHAPE #5, given that `@ApiType` exists precisely to avoid a\n * falsy default meaning \"publish\": the two operate at different levels and the default here is\n * NOT the permissive one. A contract reaches customers only by declaring\n * `@ApiType(..., EXTERNAL_CUSTOMER)` — one deliberate token per contract — and `hidden` only ever\n * SUBTRACTS from what that token already granted. Omitting it on a contract nobody published to\n * customers publishes nothing. The per-method case it exists for is real and cannot be said at\n * class level: a NEW endpoint on an already-published API, built on main before it is announced.\n *\n * Default false, and `grep -rn \"hidden: true\"` lists every method withheld from customers.\n *\n * HIDING IS A DOCUMENTATION DECISION AND NEVER AN ACCESS-CONTROL ONE. The route is served exactly\n * as before and is still reachable by anything holding its credential — including an agent, if it\n * also carries `@WpMcpTool`. Declare its authorization as loudly as a published one.\n */\n hidden?: boolean;\n /**\n * This operation may touch systems OUTSIDE this service — a payment processor, a partner's API,\n * the physical world. It feeds MCP's `openWorldHint`, and it is rendered as a sentence in the\n * operation's description so a human integrating against the document sees it too.\n *\n * DEFAULT FALSE, deliberately unlike the MCP specification's own `true` default. MCP defaults to\n * \"open world\" because it cannot know; webpieces CAN know, because the author of the endpoint is\n * right here. Defaulting to `true` would make the interesting case the silent one and the boring\n * case the one you have to type, which is backwards — and an agent told that every endpoint might\n * reach the outside world has been told nothing. `grep -rn \"openWorld: true\"` lists the ones that\n * genuinely do.\n */\n openWorld?: boolean;\n}\n\n/** An external endpoint must also identify the outside system that calls it. */\nexport interface ExternalEndpointOptions extends EndpointOptions {\n /** The outside system (`'twilio'`) — graph identity, not display text. */\n calledBy: string;\n /** What that caller is; picks its graph shape. Defaults to `'saas'`. */\n callerKind?: ExternalSystemKind;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"HttpEndpointOptions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpEndpointOptions.ts"],"names":[],"mappings":";;;AAGA,8EAA8E;AAC9E,IAAK,sBAIJ;AAJD,WAAK,sBAAsB;IACvB,uCAAa,CAAA;IACb,+DAAqC,CAAA;IACrC,yCAAe,CAAA;AACnB,CAAC,EAJI,sBAAsB,KAAtB,sBAAsB,QAI1B;AAED,wDAAwD;AAC3C,QAAA,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC;AACnC,QAAA,gBAAgB,GAAG,sBAAsB,CAAC,gBAAgB,CAAC;AAC3D,QAAA,KAAK,GAAG,sBAAsB,CAAC,KAAK,CAAC","sourcesContent":["import { ExternalSystemKind } from './external-caller';\nimport { EndpointResponseType } from './HttpContract';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum EndpointOperationValue {\n READ = 'read',\n WRITE_IDEMPOTENT = 'write-idempotent',\n WRITE = 'write',\n}\n\n/** Short, statically importable decorator arguments. */\nexport const READ = EndpointOperationValue.READ;\nexport const WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;\nexport const WRITE = EndpointOperationValue.WRITE;\n\n/** The side-effect contract callers use to decide whether repeating an operation is safe. */\nexport type EndpointOperation = typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE;\n\n/** Options for one `@Endpoint`, stored parallel to the method-to-path metadata. */\nexport interface EndpointOptions {\n /** Return only a JSON body (default), or the full transport-neutral status/header/body value. */\n responseType?: EndpointResponseType;\n /**\n * Parse the request body as application/x-www-form-urlencoded instead of JSON. The request\n * DTO must be flat; urlencoded has no nesting. Default false = JSON.\n */\n formPost?: boolean;\n /**\n * Retain the verbatim request bytes and absolute URL for a `webhook(...)` callback to verify.\n * This is retention, not new buffering: the Express adapter already accumulates the body.\n *\n * `webhook(...)` requires this option at wiring time. It combines with `formPost` for vendors\n * that sign flat form bodies, e.g. `{ formPost: true, rawBody: true }`.\n */\n rawBody?: boolean;\n /**\n * The app should show NO progress UI for this call — it is not something the USER is waiting on\n * (a background download, a log shipper, a heartbeat, a telemetry flush).\n *\n * webpieces draws no progress bar itself; it DECLARES the fact and carries it on\n * `RouteMetadata.hideProgress`, which an app's `RequestLifecycleListener` reads to keep the call\n * out of its bar. WHY a declaration on the endpoint: the alternative is the app matching path\n * strings in its listener, which silently drifts the first time a route is renamed. The ROUTE is\n * the stable fact, so the decision belongs to the route's author.\n *\n * Independent of {@link noLogging} and {@link allowUpgradeInFlight}: a background download hides\n * its progress yet still wants its `[API-*]` lines (exactly what you need when a sync is slow)\n * and still must finish before an app upgrades under it.\n *\n * Default false: absent means the call drives the app's progress UI, like any ordinary route.\n * `grep -rn hideProgress` lists every endpoint the user is not shown waiting on.\n */\n hideProgress?: boolean;\n /**\n * webpieces writes NO `[API-*-req]` / `[API-*-resp-*]` line for this call, on either side —\n * {@link LogApiCallImpl} reads it via `ApiMethodInfo.noLogging`, handed in by both the client\n * proxy and the server's `LogApiFilter` — and stamps no per-call `api` tag.\n *\n * WHY a declaration and not a suppression switch: shipping a log over a logged transport logs\n * about shipping logs — the line describing the call IS the thing being shipped, and emitting it\n * feeds the next batch its own payload. The two mechanisms that do not know the endpoint both\n * failed in prod (see #976) — a wall-clock \"we are shipping\" boolean swallowed unrelated lines\n * including an error alarm, and a logger-name allowlist was incomplete by construction the moment\n * anyone added a logger, producing a steady-state 4-lines-per-second request loop on an idle page.\n * The ENDPOINT is the only stable, declarative fact, so it is where the decision belongs.\n *\n * Rides the route metadata for the same reason {@link formPost} and {@link rawBody} do: the\n * consumer branches on the ROUTE, without knowing the apiClass/methodName.\n *\n * Default false: absent means the ordinary, fully-logged route. The quiet path is the one you\n * have to say out loud, so `grep -rn noLogging` lists every endpoint webpieces stops logging.\n */\n noLogging?: boolean;\n /**\n * An app MAY upgrade (reload onto a newer deployed bundle) while this call is in flight, and the\n * call neither blocks nor arms that upgrade.\n *\n * By default every RPC participates in the app's upgrade decision: the app must not upgrade while\n * it is in flight, and it counts toward the \"finished with a SUCCESS result\" boundary at which an\n * app typically arms its seamless upgrade. That default is the safe one — reloading under an\n * in-flight write or download loses it. A call nobody asked for, and that is harmless to cut off\n * (a log shipper, a heartbeat), declares `true` so it can neither hold an upgrade hostage nor be\n * the success that triggers one.\n *\n * WHY separate from {@link hideProgress}: apps commonly arm an upgrade off their progress bar's\n * request group, so hiding a call from the bar used to drop it out of the upgrade decision as a\n * side effect. A hidden background download must still block an upgrade until it succeeds; only\n * this flag, said out loud, lets a call stop participating.\n *\n * webpieces only declares and carries it (`RouteMetadata.allowUpgradeInFlight`); the app's\n * `RequestLifecycleListener` / upgrade logic decides what to do with it. Default false.\n * `grep -rn allowUpgradeInFlight` lists every endpoint an upgrade may cut off.\n *\n * THE SERVER'S HALF of the upgrade story is `ApiClientTooOldError` (HTTP 426,\n * `errorCode: 'client-too-old'`). This flag is the CLIENT deciding WHEN it may upgrade; that\n * error is a SERVER saying the client MUST. Who throws it: a server gate that compares the build\n * the caller declared (a header, a version in the request) against the minimum it still\n * supports. Receivers decode it back into `ApiClientTooOldError` on every hop, server-to-server\n * and browser, HTTP and IPC, and treat it as an upgrade, never as a bug: the app's upgrade logic\n * reloads or installs the new build, typically honouring this flag for the calls still in flight.\n */\n allowUpgradeInFlight?: boolean;\n /**\n * This route is NOT part of the CUSTOMER-facing contract. `public-openapi.json` — the document a\n * customer gets, and the only one a publish path uploads — contains no path, no operation, no\n * schema and no prose for it. It is not \"rendered with a flag set\", it is absent.\n *\n * It is still in `full-private-openapi.json`, and still in `mcp-openapi.json` when it carries\n * `@WpMcpTool`. The reason it is withheld belongs in the method's JSDoc, which is where every\n * other piece of its documentation already lives.\n *\n * WHY A BOOLEAN HERE IS NOT SHIM SHAPE #5, given that `@ApiType` exists precisely to avoid a\n * falsy default meaning \"publish\": the two operate at different levels and the default here is\n * NOT the permissive one. A contract reaches customers only by declaring\n * `@ApiType(..., EXTERNAL_CUSTOMER)` — one deliberate token per contract — and `hidden` only ever\n * SUBTRACTS from what that token already granted. Omitting it on a contract nobody published to\n * customers publishes nothing. The per-method case it exists for is real and cannot be said at\n * class level: a NEW endpoint on an already-published API, built on main before it is announced.\n *\n * Default false, and `grep -rn \"hidden: true\"` lists every method withheld from customers.\n *\n * HIDING IS A DOCUMENTATION DECISION AND NEVER AN ACCESS-CONTROL ONE. The route is served exactly\n * as before and is still reachable by anything holding its credential — including an agent, if it\n * also carries `@WpMcpTool`. Declare its authorization as loudly as a published one.\n */\n hidden?: boolean;\n /**\n * This operation may touch systems OUTSIDE this service — a payment processor, a partner's API,\n * the physical world. It feeds MCP's `openWorldHint`, and it is rendered as a sentence in the\n * operation's description so a human integrating against the document sees it too.\n *\n * DEFAULT FALSE, deliberately unlike the MCP specification's own `true` default. MCP defaults to\n * \"open world\" because it cannot know; webpieces CAN know, because the author of the endpoint is\n * right here. Defaulting to `true` would make the interesting case the silent one and the boring\n * case the one you have to type, which is backwards — and an agent told that every endpoint might\n * reach the outside world has been told nothing. `grep -rn \"openWorld: true\"` lists the ones that\n * genuinely do.\n */\n openWorld?: boolean;\n}\n\n/** An external endpoint must also identify the outside system that calls it. */\nexport interface ExternalEndpointOptions extends EndpointOptions {\n /** The outside system (`'twilio'`) — graph identity, not display text. */\n calledBy: string;\n /** What that caller is; picks its graph shape. Defaults to `'saas'`. */\n callerKind?: ExternalSystemKind;\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ClientRole } from '../errors/ClientRole';
|
|
1
2
|
import { StreamTransportError } from './StreamingContract';
|
|
2
3
|
export declare class StreamProtocolError extends StreamTransportError {
|
|
3
4
|
}
|
|
@@ -6,6 +7,7 @@ export declare class StreamDisconnectedError extends StreamTransportError {
|
|
|
6
7
|
/** Reserved ASCII-RS controls share exactly the unary ErrorTranslator seam in both directions. */
|
|
7
8
|
export declare class StreamErrorControl {
|
|
8
9
|
encode(error: Error): string;
|
|
9
|
-
|
|
10
|
+
/** @param role - who is receiving the record, declared at the reading side's setup. */
|
|
11
|
+
decode(line: string, role: ClientRole): Error;
|
|
10
12
|
private response;
|
|
11
13
|
}
|
|
@@ -35,12 +35,13 @@ class StreamErrorControl {
|
|
|
35
35
|
const response = ClientRegistry_1.ClientRegistry.getErrorTranslator().toWire(error);
|
|
36
36
|
return `${String.fromCharCode(0x1e)}${JSON.stringify({ control: 'error', response })}\n`;
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
/** @param role - who is receiving the record, declared at the reading side's setup. */
|
|
39
|
+
decode(line, role) {
|
|
39
40
|
const response = this.response(line);
|
|
40
41
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- fromWire returns its typed result by throwing
|
|
41
42
|
try {
|
|
42
|
-
ClientRegistry_1.ClientRegistry.getErrorTranslator().fromWire(response);
|
|
43
|
-
WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR.fromWire(response);
|
|
43
|
+
ClientRegistry_1.ClientRegistry.getErrorTranslator().fromWire(response, role);
|
|
44
|
+
WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR.fromWire(response, role);
|
|
44
45
|
}
|
|
45
46
|
catch (err) {
|
|
46
47
|
const error = (0, errorUtils_1.toError)(err);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamErrorControl.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/StreamErrorControl.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"StreamErrorControl.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/StreamErrorControl.ts"],"names":[],"mappings":"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","sourcesContent":["import { ClientRole } from '../errors/ClientRole';\nimport { ClientRegistry } from './ClientRegistry';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { DtoValue } from '../mcp/DtoSchema';\nimport { toError } from '../lib/errorUtils';\nimport { StreamTransportError } from './StreamingContract';\n\nexport class StreamProtocolError extends StreamTransportError {}\n\nexport class StreamDisconnectedError extends StreamTransportError {}\n\nclass ControlStatus {\n code?: number;\n reason?: string;\n}\n\nclass ControlHeader {\n name?: string;\n value?: string;\n}\n\nclass ControlResponse {\n status?: ControlStatus;\n headers?: ControlHeader[];\n body?: DtoValue;\n}\n\nclass ControlRecord {\n control?: string;\n response?: ControlResponse;\n}\n\n/** Reserved ASCII-RS controls share exactly the unary ErrorTranslator seam in both directions. */\nexport class StreamErrorControl {\n encode(error: Error): string {\n const response = ClientRegistry.getErrorTranslator().toWire(error);\n return `${String.fromCharCode(0x1e)}${JSON.stringify({ control: 'error', response })}\\n`;\n }\n\n /** @param role - who is receiving the record, declared at the reading side's setup. */\n decode(line: string, role: ClientRole): Error {\n const response = this.response(line);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- fromWire returns its typed result by throwing\n try {\n ClientRegistry.getErrorTranslator().fromWire(response, role);\n WEBPIECES_DEFAULT_ERROR_TRANSLATOR.fromWire(response, role);\n } catch (err: unknown) {\n const error = toError(err);\n return error;\n }\n return new StreamProtocolError(\n 'Error translator returned for a JSONL error control record.',\n );\n }\n\n private response(line: string): HttpResponseDto {\n let control: ControlRecord;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted control boundary\n try {\n control = JSON.parse(line.slice(1)) as ControlRecord;\n } catch (err: unknown) {\n const error = toError(err);\n throw new StreamProtocolError(\n 'Malformed JSONL error control record.',\n undefined,\n undefined,\n error,\n );\n }\n const status = control?.response?.status;\n const headers = control?.response?.headers;\n if (\n control?.control !== 'error' ||\n typeof status?.code !== 'number' ||\n typeof status.reason !== 'string' ||\n !Array.isArray(headers)\n )\n throw new StreamProtocolError('Malformed JSONL error control record.');\n const mapped = headers.map((header: ControlHeader): HttpHeader => {\n if (typeof header?.name !== 'string' || typeof header.value !== 'string') {\n throw new StreamProtocolError('Malformed JSONL error control header.');\n }\n return new HttpHeader(header.name, header.value);\n });\n return new HttpResponseDto(\n new HttpResponseStatus(status.code, status.reason),\n mapped,\n control.response?.body,\n );\n }\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ClientRole } from '../errors/ClientRole';
|
|
1
2
|
import { ErrorTranslator } from './ErrorTranslator';
|
|
2
3
|
import { HttpResponseDto } from './HttpResponseDto';
|
|
3
4
|
/**
|
|
@@ -18,6 +19,10 @@ import { HttpResponseDto } from './HttpResponseDto';
|
|
|
18
19
|
* {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants
|
|
19
20
|
* {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.
|
|
20
21
|
*
|
|
22
|
+
* It holds no ROLE: who is receiving ({@link ClientRole}) is an argument of `fromWire`, supplied by
|
|
23
|
+
* the client that read the response from its own setup, so one stateless instance serves a server's
|
|
24
|
+
* clients and a browser's alike.
|
|
25
|
+
*
|
|
21
26
|
* Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing
|
|
22
27
|
* your own costs nothing and reads better at a delegation site.
|
|
23
28
|
*/
|
|
@@ -37,6 +42,9 @@ export declare class WebpiecesDefaultErrorTranslator implements ErrorTranslator
|
|
|
37
42
|
* error; unknown throws and caller-local failures come out as a 500 implementation failure
|
|
38
43
|
* because that is what the payload says.
|
|
39
44
|
*
|
|
45
|
+
* The one exception is an {@link OAuthProtocolError}: its caller is an OAuth client, so it is
|
|
46
|
+
* answered in the RFC shape by `OAuthErrorResponse`, not as a payload.
|
|
47
|
+
*
|
|
40
48
|
* An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that
|
|
41
49
|
* as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request
|
|
42
50
|
* from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —
|
|
@@ -62,9 +70,12 @@ export declare class WebpiecesDefaultErrorTranslator implements ErrorTranslator
|
|
|
62
70
|
*
|
|
63
71
|
* The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY
|
|
64
72
|
* bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/
|
|
65
|
-
* backoff/unavailable meaning,
|
|
73
|
+
* backoff/unavailable meaning, 266 is the end user's own answer, a 426 `client-too-old` stays
|
|
74
|
+
* {@link ApiClientTooOldError}, and a 401 depends on `role` (see {@link ClientRole}).
|
|
75
|
+
*
|
|
76
|
+
* @param role - who is receiving, declared at the client's setup. No default.
|
|
66
77
|
*/
|
|
67
|
-
fromWire(response: HttpResponseDto): void;
|
|
78
|
+
fromWire(response: HttpResponseDto, role: ClientRole): void;
|
|
68
79
|
/** The reason phrase webpieces writes beside a status it chose itself. */
|
|
69
80
|
genericMessage(code: number): string;
|
|
70
81
|
/**
|
|
@@ -5,6 +5,8 @@ const ApiErrorBoundary_1 = require("../errors/ApiErrorBoundary");
|
|
|
5
5
|
const ApiErrorCodec_1 = require("../errors/ApiErrorCodec");
|
|
6
6
|
const ApiError_1 = require("../errors/ApiError");
|
|
7
7
|
const ReceivedApiErrorRule_1 = require("../errors/ReceivedApiErrorRule");
|
|
8
|
+
const OAuthErrorResponse_1 = require("../oauth/OAuthErrorResponse");
|
|
9
|
+
const OAuthProtocolError_1 = require("../oauth/OAuthProtocolError");
|
|
8
10
|
const ApiCallTimeoutError_1 = require("./ApiCallTimeoutError");
|
|
9
11
|
const ApiErrorHttpStatus_1 = require("./ApiErrorHttpStatus");
|
|
10
12
|
const HttpResponseDto_1 = require("./HttpResponseDto");
|
|
@@ -26,6 +28,10 @@ const HttpResponseDto_1 = require("./HttpResponseDto");
|
|
|
26
28
|
* {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants
|
|
27
29
|
* {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.
|
|
28
30
|
*
|
|
31
|
+
* It holds no ROLE: who is receiving ({@link ClientRole}) is an argument of `fromWire`, supplied by
|
|
32
|
+
* the client that read the response from its own setup, so one stateless instance serves a server's
|
|
33
|
+
* clients and a browser's alike.
|
|
34
|
+
*
|
|
29
35
|
* Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing
|
|
30
36
|
* your own costs nothing and reads better at a delegation site.
|
|
31
37
|
*/
|
|
@@ -44,6 +50,7 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
44
50
|
[412, 'Precondition Failed'],
|
|
45
51
|
[415, 'Unsupported Media Type'],
|
|
46
52
|
[422, 'Unprocessable Content'],
|
|
53
|
+
[426, 'Upgrade Required'],
|
|
47
54
|
[429, 'Too Many Requests'],
|
|
48
55
|
[500, 'Internal Server Error'],
|
|
49
56
|
[501, 'Not Implemented'],
|
|
@@ -62,6 +69,9 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
62
69
|
* error; unknown throws and caller-local failures come out as a 500 implementation failure
|
|
63
70
|
* because that is what the payload says.
|
|
64
71
|
*
|
|
72
|
+
* The one exception is an {@link OAuthProtocolError}: its caller is an OAuth client, so it is
|
|
73
|
+
* answered in the RFC shape by `OAuthErrorResponse`, not as a payload.
|
|
74
|
+
*
|
|
65
75
|
* An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that
|
|
66
76
|
* as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request
|
|
67
77
|
* from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —
|
|
@@ -70,6 +80,11 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
70
80
|
* responsible for repeating its router's mode, and repeating it wrongly was silent.
|
|
71
81
|
*/
|
|
72
82
|
toWire(error) {
|
|
83
|
+
// An OAuth refusal is answered to an OAuth CLIENT, which parses the RFC 6749/6750 body and
|
|
84
|
+
// never the ApiError envelope (#1176). Same renderer as every other OAuth boundary.
|
|
85
|
+
if (error instanceof OAuthProtocolError_1.OAuthProtocolError) {
|
|
86
|
+
return OAuthErrorResponse_1.OAuthErrorResponse.toResponse(error, undefined);
|
|
87
|
+
}
|
|
73
88
|
const payload = this.boundary.encode(error);
|
|
74
89
|
// The boundary never publishes kind 'connection' — that is the whole first rule above — so
|
|
75
90
|
// this is the published subset ApiErrorHttpStatus.codeFor accepts.
|
|
@@ -102,16 +117,19 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
102
117
|
*
|
|
103
118
|
* The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY
|
|
104
119
|
* bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/
|
|
105
|
-
* backoff/unavailable meaning,
|
|
120
|
+
* backoff/unavailable meaning, 266 is the end user's own answer, a 426 `client-too-old` stays
|
|
121
|
+
* {@link ApiClientTooOldError}, and a 401 depends on `role` (see {@link ClientRole}).
|
|
122
|
+
*
|
|
123
|
+
* @param role - who is receiving, declared at the client's setup. No default.
|
|
106
124
|
*/
|
|
107
|
-
fromWire(response) {
|
|
125
|
+
fromWire(response, role) {
|
|
108
126
|
const code = response.status.code;
|
|
109
127
|
if (code >= 200 && code < 300 && code !== 266) {
|
|
110
128
|
return;
|
|
111
129
|
}
|
|
112
130
|
const decoded = this.decodeBody(response);
|
|
113
131
|
const message = decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);
|
|
114
|
-
throw ReceivedApiErrorRule_1.ReceivedApiErrorRule.adapt(code, message, decoded, this.retryAfterSeconds(response.headers));
|
|
132
|
+
throw ReceivedApiErrorRule_1.ReceivedApiErrorRule.adapt(role, code, message, decoded, this.retryAfterSeconds(response.headers));
|
|
115
133
|
}
|
|
116
134
|
/** The reason phrase webpieces writes beside a status it chose itself. */
|
|
117
135
|
genericMessage(code) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { ApiCallTimeoutError } from './ApiCallTimeoutError';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n /** Reason phrase for a client deadline; see {@link ApiCallTimeoutError}. Never 'Gateway Timeout'. */\n static readonly CALL_TIMEOUT_REASON = 'Network Timeout';\n\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.reasonPhrase(error, status)),\n headers,\n payload,\n );\n }\n\n /**\n * The reason phrase for `error`. An {@link ApiCallTimeoutError} is THIS process's own client\n * deadline firing on an outbound call: no gateway was involved, so it is never titled\n * 'Gateway Timeout' (issue #1170). It keeps its 504 status and `dependency-timeout` payload —\n * retry semantics are unchanged — and only the human framing differs.\n */\n private reasonPhrase(error: Error, status: number): string {\n if (error instanceof ApiCallTimeoutError) {\n return WebpiecesDefaultErrorTranslator.CALL_TIMEOUT_REASON;\n }\n return this.genericMessage(status);\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY\n * bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/\n * backoff/unavailable meaning, and 266 is the end user's own answer.\n */\n fromWire(response: HttpResponseDto): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message =\n decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(\n code,\n message,\n decoded,\n this.retryAfterSeconds(response.headers),\n );\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n\n /** Parse either legal Retry-After form and bound untrusted guidance to one day. */\n private retryAfterSeconds(headers: readonly HttpHeader[]): number | undefined {\n const raw = headers.find(\n (header: HttpHeader) => header.name.toLowerCase() === 'retry-after',\n )?.value;\n if (raw === undefined) return undefined;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.min(Math.ceil(seconds), 86_400);\n const at = Date.parse(raw);\n if (!Number.isFinite(at)) return undefined;\n return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}
|
|
1
|
+
{"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ClientRole } from '../errors/ClientRole';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { OAuthErrorResponse } from '../oauth/OAuthErrorResponse';\nimport { OAuthProtocolError } from '../oauth/OAuthProtocolError';\nimport { ApiCallTimeoutError } from './ApiCallTimeoutError';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * It holds no ROLE: who is receiving ({@link ClientRole}) is an argument of `fromWire`, supplied by\n * the client that read the response from its own setup, so one stateless instance serves a server's\n * clients and a browser's alike.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n /** Reason phrase for a client deadline; see {@link ApiCallTimeoutError}. Never 'Gateway Timeout'. */\n static readonly CALL_TIMEOUT_REASON = 'Network Timeout';\n\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [426, 'Upgrade Required'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * The one exception is an {@link OAuthProtocolError}: its caller is an OAuth client, so it is\n * answered in the RFC shape by `OAuthErrorResponse`, not as a payload.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n // An OAuth refusal is answered to an OAuth CLIENT, which parses the RFC 6749/6750 body and\n // never the ApiError envelope (#1176). Same renderer as every other OAuth boundary.\n if (error instanceof OAuthProtocolError) {\n return OAuthErrorResponse.toResponse(error, undefined);\n }\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.reasonPhrase(error, status)),\n headers,\n payload,\n );\n }\n\n /**\n * The reason phrase for `error`. An {@link ApiCallTimeoutError} is THIS process's own client\n * deadline firing on an outbound call: no gateway was involved, so it is never titled\n * 'Gateway Timeout' (issue #1170). It keeps its 504 status and `dependency-timeout` payload —\n * retry semantics are unchanged — and only the human framing differs.\n */\n private reasonPhrase(error: Error, status: number): string {\n if (error instanceof ApiCallTimeoutError) {\n return WebpiecesDefaultErrorTranslator.CALL_TIMEOUT_REASON;\n }\n return this.genericMessage(status);\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY\n * bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/\n * backoff/unavailable meaning, 266 is the end user's own answer, a 426 `client-too-old` stays\n * {@link ApiClientTooOldError}, and a 401 depends on `role` (see {@link ClientRole}).\n *\n * @param role - who is receiving, declared at the client's setup. No default.\n */\n fromWire(response: HttpResponseDto, role: ClientRole): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message =\n decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(\n role,\n code,\n message,\n decoded,\n this.retryAfterSeconds(response.headers),\n );\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n\n /** Parse either legal Retry-After form and bound untrusted guidance to one day. */\n private retryAfterSeconds(headers: readonly HttpHeader[]): number | undefined {\n const raw = headers.find(\n (header: HttpHeader) => header.name.toLowerCase() === 'retry-after',\n )?.value;\n if (raw === undefined) return undefined;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.min(Math.ceil(seconds), 86_400);\n const at = Date.parse(raw);\n if (!Number.isFinite(at)) return undefined;\n return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}
|