@webpieces/core-util 0.4.785 → 0.4.787

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/core-util",
3
- "version": "0.4.785",
3
+ "version": "0.4.787",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -1,24 +1,43 @@
1
1
  import { Logger } from '../logging/Logger';
2
+ import { ApiErrorPayload } from './ApiErrorCodec';
2
3
  import { ApiError } from './ApiError';
3
4
  /**
4
5
  * The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before
5
6
  * rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape
6
- * they render; normalization and log levels live here so they cannot drift apart.
7
+ * they render; classification and log levels live here so they cannot drift apart.
8
+ *
9
+ * Nothing here SUBSTITUTES an object for the value that was thrown: the thrown error survives to
10
+ * every consumer, so an app can still `instanceof` its own error class and the operator log still
11
+ * names it. Classification is a VALUE computed from the thrown error, never a wrapper around it.
7
12
  */
8
13
  export declare class ApiErrorBoundary {
9
14
  private readonly log;
10
15
  constructor(log: Logger);
11
16
  /**
12
- * An `ApiError` (except a caller-local `ApiConnectionError`) stays as-is. Anything else, including
13
- * a raw `Error`, a non-Error throw, or a downstream connection failure, is a bug or broken state of
14
- * THIS service and becomes `ApiImplementationError`. Its message stays operator-only: encoders
15
- * publish only `ApiEndUserError` text.
17
+ * The classified API outcome `thrown` publishes as, or `undefined` when it is a bug or broken
18
+ * state of THIS service — a non-`ApiError`, a non-`Error` throw, or a caller-local
19
+ * `ApiConnectionError`.
20
+ *
21
+ * A connection failure is OUR outbound call failing, so from our caller's seat this service is
22
+ * the thing that is broken: it publishes as `implementation`, never as `connection`. That rule
23
+ * is deliberately NOT in `ApiErrorCodec`, which encodes a value faithfully; it belongs to the
24
+ * boundary that owns the failure.
25
+ *
26
+ * The returned error IS the thrown error — never a replacement.
16
27
  */
17
- normalize(thrown: unknown): ApiError;
28
+ apiOutcome(thrown: unknown): ApiError | undefined;
29
+ /**
30
+ * The wire payload THIS service publishes for `thrown`. Only an {@link ApiEndUserError} ever
31
+ * publishes its own message; everything else gets allowlisted generic text by kind.
32
+ */
33
+ encode(thrown: unknown): ApiErrorPayload;
18
34
  /**
19
35
  * Logs one operator line at the per-kind level: expected caller outcomes at info, everything else
20
36
  * at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).
37
+ *
38
+ * The line names the class that was ACTUALLY thrown, which is the single fact an operator needs
39
+ * and the one a wrapper would have destroyed.
21
40
  */
22
- logOperatorDetail(error: ApiError, correlation?: string): void;
41
+ logOperatorDetail(thrown: unknown, correlation?: string): void;
23
42
  private operatorDetail;
24
43
  }
@@ -2,11 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApiErrorBoundary = void 0;
4
4
  const errorUtils_1 = require("../lib/errorUtils");
5
+ const ApiErrorCodec_1 = require("./ApiErrorCodec");
5
6
  const ApiError_1 = require("./ApiError");
6
7
  /**
7
8
  * The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before
8
9
  * rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape
9
- * they render; normalization and log levels live here so they cannot drift apart.
10
+ * they render; classification and log levels live here so they cannot drift apart.
11
+ *
12
+ * Nothing here SUBSTITUTES an object for the value that was thrown: the thrown error survives to
13
+ * every consumer, so an app can still `instanceof` its own error class and the operator log still
14
+ * names it. Classification is a VALUE computed from the thrown error, never a wrapper around it.
10
15
  */
11
16
  class ApiErrorBoundary {
12
17
  log;
@@ -14,53 +19,77 @@ class ApiErrorBoundary {
14
19
  this.log = log;
15
20
  }
16
21
  /**
17
- * An `ApiError` (except a caller-local `ApiConnectionError`) stays as-is. Anything else, including
18
- * a raw `Error`, a non-Error throw, or a downstream connection failure, is a bug or broken state of
19
- * THIS service and becomes `ApiImplementationError`. Its message stays operator-only: encoders
20
- * publish only `ApiEndUserError` text.
22
+ * The classified API outcome `thrown` publishes as, or `undefined` when it is a bug or broken
23
+ * state of THIS service — a non-`ApiError`, a non-`Error` throw, or a caller-local
24
+ * `ApiConnectionError`.
25
+ *
26
+ * A connection failure is OUR outbound call failing, so from our caller's seat this service is
27
+ * the thing that is broken: it publishes as `implementation`, never as `connection`. That rule
28
+ * is deliberately NOT in `ApiErrorCodec`, which encodes a value faithfully; it belongs to the
29
+ * boundary that owns the failure.
30
+ *
31
+ * The returned error IS the thrown error — never a replacement.
21
32
  */
22
- // webpieces-disable no-any-unknown -- thrown values are unknown until normalized at this boundary
23
- normalize(thrown) {
33
+ // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary
34
+ apiOutcome(thrown) {
24
35
  if (thrown instanceof ApiError_1.ApiError && !(thrown instanceof ApiError_1.ApiConnectionError))
25
36
  return thrown;
26
- const error = (0, errorUtils_1.toError)(thrown);
27
- return new ApiError_1.ApiImplementationError(error.message, error);
37
+ return undefined;
38
+ }
39
+ /**
40
+ * The wire payload THIS service publishes for `thrown`. Only an {@link ApiEndUserError} ever
41
+ * publishes its own message; everything else gets allowlisted generic text by kind.
42
+ */
43
+ // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary
44
+ encode(thrown) {
45
+ const outcome = this.apiOutcome(thrown);
46
+ if (!outcome)
47
+ return new ApiErrorCodec_1.ApiErrorPayload('implementation', 'Internal Error');
48
+ return ApiErrorCodec_1.ApiErrorCodec.encode(outcome);
28
49
  }
29
50
  /**
30
51
  * Logs one operator line at the per-kind level: expected caller outcomes at info, everything else
31
52
  * at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).
53
+ *
54
+ * The line names the class that was ACTUALLY thrown, which is the single fact an operator needs
55
+ * and the one a wrapper would have destroyed.
32
56
  */
33
- logOperatorDetail(error, correlation) {
57
+ // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary
58
+ logOperatorDetail(thrown, correlation) {
34
59
  const suffix = correlation ? ` ${correlation}` : '';
35
- const detail = `${this.operatorDetail(error)}${suffix}`;
36
- if (error instanceof ApiError_1.ApiEndUserError)
60
+ const detail = `${this.operatorDetail(thrown)}${suffix}`;
61
+ if (thrown instanceof ApiError_1.ApiEndUserError)
37
62
  this.log.info(`End User Error: ${detail}`);
38
- else if (error instanceof ApiError_1.ApiBadRequestError)
63
+ else if (thrown instanceof ApiError_1.ApiBadRequestError)
39
64
  this.log.info(`Bad Request: ${detail}`);
40
- else if (error instanceof ApiError_1.ApiNotFoundError)
65
+ else if (thrown instanceof ApiError_1.ApiNotFoundError)
41
66
  this.log.info(`Not Found: ${detail}`);
42
- else if (error instanceof ApiError_1.ApiUnauthorizedError)
67
+ else if (thrown instanceof ApiError_1.ApiUnauthorizedError)
43
68
  this.log.info(`Unauthorized: ${detail}`);
44
- else if (error instanceof ApiError_1.ApiForbiddenError)
69
+ else if (thrown instanceof ApiError_1.ApiForbiddenError)
45
70
  this.log.info(`Forbidden: ${detail}`);
46
- else if (error instanceof ApiError_1.ApiConflictError)
71
+ else if (thrown instanceof ApiError_1.ApiConflictError)
47
72
  this.log.info(`Conflict: ${detail}`);
48
- else if (error instanceof ApiError_1.ApiUnprocessableError)
73
+ else if (thrown instanceof ApiError_1.ApiUnprocessableError)
49
74
  this.log.info(`Unprocessable: ${detail}`);
50
- else if (error instanceof ApiError_1.ApiPreconditionFailedError)
75
+ else if (thrown instanceof ApiError_1.ApiPreconditionFailedError)
51
76
  this.log.info(`Precondition Failed: ${detail}`);
52
- else if (error instanceof ApiError_1.ApiUnsupportedMediaTypeError)
77
+ else if (thrown instanceof ApiError_1.ApiUnsupportedMediaTypeError)
53
78
  this.log.info(`Unsupported Media Type: ${detail}`);
54
- else if (error instanceof ApiError_1.ApiCodedError && error.isCallerError())
55
- this.log.info(`Coded ${error.statusCode}: ${detail}`);
56
- else if (error instanceof ApiError_1.ApiRequestTimeoutError)
79
+ else if (thrown instanceof ApiError_1.ApiCodedError && thrown.isCallerError())
80
+ this.log.info(`Coded ${thrown.statusCode}: ${detail}`);
81
+ else if (thrown instanceof ApiError_1.ApiRequestTimeoutError)
57
82
  this.log.error(`Request Timeout: ${detail}`);
58
83
  else
59
84
  this.log.error(`API failure: ${detail}`);
60
85
  }
61
- operatorDetail(error) {
86
+ // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary
87
+ operatorDetail(thrown) {
88
+ const error = (0, errorUtils_1.toError)(thrown);
89
+ const kind = this.apiOutcome(thrown)?.kind ?? 'implementation';
90
+ const subType = thrown instanceof ApiError_1.ApiError ? thrown.subType : undefined;
62
91
  const cause = error.cause instanceof Error ? ` cause=${error.cause.message}` : '';
63
- return `[name=${error.name} kind=${error.kind} subType=${error.subType ?? 'none'}] ${error.message}${cause}`;
92
+ return `[name=${error.name} kind=${kind} subType=${subType ?? 'none'}] ${error.message}${cause}`;
64
93
  }
65
94
  }
66
95
  exports.ApiErrorBoundary = ApiErrorBoundary;
@@ -1 +1 @@
1
- {"version":3,"file":"ApiErrorBoundary.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiErrorBoundary.ts"],"names":[],"mappings":";;;AACA,kDAA4C;AAC5C,yCAeoB;AAEpB;;;;GAIG;AACH,MAAa,gBAAgB;IACI;IAA7B,YAA6B,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAE5C;;;;;OAKG;IACH,kGAAkG;IAClG,SAAS,CAAC,MAAe;QACrB,IAAI,MAAM,YAAY,mBAAQ,IAAI,CAAC,CAAC,MAAM,YAAY,6BAAkB,CAAC;YAAE,OAAO,MAAM,CAAC;QACzF,MAAM,KAAK,GAAG,IAAA,oBAAO,EAAC,MAAM,CAAC,CAAC;QAC9B,OAAO,IAAI,iCAAsB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC5D,CAAC;IAED;;;OAGG;IACH,iBAAiB,CAAC,KAAe,EAAE,WAAoB;QACnD,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpD,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC;QACxD,IAAI,KAAK,YAAY,0BAAe;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,mBAAmB,MAAM,EAAE,CAAC,CAAC;aAC5E,IAAI,KAAK,YAAY,6BAAkB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,MAAM,EAAE,CAAC,CAAC;aACjF,IAAI,KAAK,YAAY,2BAAgB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,MAAM,EAAE,CAAC,CAAC;aAC7E,IAAI,KAAK,YAAY,+BAAoB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,iBAAiB,MAAM,EAAE,CAAC,CAAC;aACpF,IAAI,KAAK,YAAY,4BAAiB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,MAAM,EAAE,CAAC,CAAC;aAC9E,IAAI,KAAK,YAAY,2BAAgB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,MAAM,EAAE,CAAC,CAAC;aAC5E,IAAI,KAAK,YAAY,gCAAqB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,kBAAkB,MAAM,EAAE,CAAC,CAAC;aACtF,IAAI,KAAK,YAAY,qCAA0B;YAChD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,wBAAwB,MAAM,EAAE,CAAC,CAAC;aAC/C,IAAI,KAAK,YAAY,uCAA4B;YAClD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,2BAA2B,MAAM,EAAE,CAAC,CAAC;aAClD,IAAI,KAAK,YAAY,wBAAa,IAAI,KAAK,CAAC,aAAa,EAAE;YAC5D,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,UAAU,KAAK,MAAM,EAAE,CAAC,CAAC;aACrD,IAAI,KAAK,YAAY,iCAAsB;YAC5C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,oBAAoB,MAAM,EAAE,CAAC,CAAC;;YAC5C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAEO,cAAc,CAAC,KAAe;QAClC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,OAAO,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,IAAI,YAAY,KAAK,CAAC,OAAO,IAAI,MAAM,KAAK,KAAK,CAAC,OAAO,GAAG,KAAK,EAAE,CAAC;IACjH,CAAC;CACJ;AA7CD,4CA6CC","sourcesContent":["import { Logger } from '../logging/Logger';\nimport { toError } from '../lib/errorUtils';\nimport {\n ApiBadRequestError,\n ApiCodedError,\n ApiConflictError,\n ApiConnectionError,\n ApiEndUserError,\n ApiError,\n ApiForbiddenError,\n ApiImplementationError,\n ApiNotFoundError,\n ApiPreconditionFailedError,\n ApiRequestTimeoutError,\n ApiUnauthorizedError,\n ApiUnprocessableError,\n ApiUnsupportedMediaTypeError,\n} from './ApiError';\n\n/**\n * The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before\n * rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape\n * they render; normalization and log levels live here so they cannot drift apart.\n */\nexport class ApiErrorBoundary {\n constructor(private readonly log: Logger) {}\n\n /**\n * An `ApiError` (except a caller-local `ApiConnectionError`) stays as-is. Anything else, including\n * a raw `Error`, a non-Error throw, or a downstream connection failure, is a bug or broken state of\n * THIS service and becomes `ApiImplementationError`. Its message stays operator-only: encoders\n * publish only `ApiEndUserError` text.\n */\n // webpieces-disable no-any-unknown -- thrown values are unknown until normalized at this boundary\n normalize(thrown: unknown): ApiError {\n if (thrown instanceof ApiError && !(thrown instanceof ApiConnectionError)) return thrown;\n const error = toError(thrown);\n return new ApiImplementationError(error.message, error);\n }\n\n /**\n * Logs one operator line at the per-kind level: expected caller outcomes at info, everything else\n * at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).\n */\n logOperatorDetail(error: ApiError, correlation?: string): void {\n const suffix = correlation ? ` ${correlation}` : '';\n const detail = `${this.operatorDetail(error)}${suffix}`;\n if (error instanceof ApiEndUserError) this.log.info(`End User Error: ${detail}`);\n else if (error instanceof ApiBadRequestError) this.log.info(`Bad Request: ${detail}`);\n else if (error instanceof ApiNotFoundError) this.log.info(`Not Found: ${detail}`);\n else if (error instanceof ApiUnauthorizedError) this.log.info(`Unauthorized: ${detail}`);\n else if (error instanceof ApiForbiddenError) this.log.info(`Forbidden: ${detail}`);\n else if (error instanceof ApiConflictError) this.log.info(`Conflict: ${detail}`);\n else if (error instanceof ApiUnprocessableError) this.log.info(`Unprocessable: ${detail}`);\n else if (error instanceof ApiPreconditionFailedError)\n this.log.info(`Precondition Failed: ${detail}`);\n else if (error instanceof ApiUnsupportedMediaTypeError)\n this.log.info(`Unsupported Media Type: ${detail}`);\n else if (error instanceof ApiCodedError && error.isCallerError())\n this.log.info(`Coded ${error.statusCode}: ${detail}`);\n else if (error instanceof ApiRequestTimeoutError)\n this.log.error(`Request Timeout: ${detail}`);\n else this.log.error(`API failure: ${detail}`);\n }\n\n private operatorDetail(error: ApiError): string {\n const cause = error.cause instanceof Error ? ` cause=${error.cause.message}` : '';\n return `[name=${error.name} kind=${error.kind} subType=${error.subType ?? 'none'}] ${error.message}${cause}`;\n }\n}\n"]}
1
+ {"version":3,"file":"ApiErrorBoundary.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiErrorBoundary.ts"],"names":[],"mappings":"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","sourcesContent":["import { Logger } from '../logging/Logger';\nimport { toError } from '../lib/errorUtils';\nimport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nimport {\n ApiBadRequestError,\n ApiCodedError,\n ApiConflictError,\n ApiConnectionError,\n ApiEndUserError,\n ApiError,\n ApiForbiddenError,\n ApiNotFoundError,\n ApiPreconditionFailedError,\n ApiRequestTimeoutError,\n ApiUnauthorizedError,\n ApiUnprocessableError,\n ApiUnsupportedMediaTypeError,\n} from './ApiError';\n\n/**\n * The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before\n * rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape\n * they render; classification and log levels live here so they cannot drift apart.\n *\n * Nothing here SUBSTITUTES an object for the value that was thrown: the thrown error survives to\n * every consumer, so an app can still `instanceof` its own error class and the operator log still\n * names it. Classification is a VALUE computed from the thrown error, never a wrapper around it.\n */\nexport class ApiErrorBoundary {\n constructor(private readonly log: Logger) {}\n\n /**\n * The classified API outcome `thrown` publishes as, or `undefined` when it is a bug or broken\n * state of THIS service — a non-`ApiError`, a non-`Error` throw, or a caller-local\n * `ApiConnectionError`.\n *\n * A connection failure is OUR outbound call failing, so from our caller's seat this service is\n * the thing that is broken: it publishes as `implementation`, never as `connection`. That rule\n * is deliberately NOT in `ApiErrorCodec`, which encodes a value faithfully; it belongs to the\n * boundary that owns the failure.\n *\n * The returned error IS the thrown error — never a replacement.\n */\n // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary\n apiOutcome(thrown: unknown): ApiError | undefined {\n if (thrown instanceof ApiError && !(thrown instanceof ApiConnectionError)) return thrown;\n return undefined;\n }\n\n /**\n * The wire payload THIS service publishes for `thrown`. Only an {@link ApiEndUserError} ever\n * publishes its own message; everything else gets allowlisted generic text by kind.\n */\n // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary\n encode(thrown: unknown): ApiErrorPayload {\n const outcome = this.apiOutcome(thrown);\n if (!outcome) return new ApiErrorPayload('implementation', 'Internal Error');\n return ApiErrorCodec.encode(outcome);\n }\n\n /**\n * Logs one operator line at the per-kind level: expected caller outcomes at info, everything else\n * at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).\n *\n * The line names the class that was ACTUALLY thrown, which is the single fact an operator needs\n * and the one a wrapper would have destroyed.\n */\n // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary\n logOperatorDetail(thrown: unknown, correlation?: string): void {\n const suffix = correlation ? ` ${correlation}` : '';\n const detail = `${this.operatorDetail(thrown)}${suffix}`;\n if (thrown instanceof ApiEndUserError) this.log.info(`End User Error: ${detail}`);\n else if (thrown instanceof ApiBadRequestError) this.log.info(`Bad Request: ${detail}`);\n else if (thrown instanceof ApiNotFoundError) this.log.info(`Not Found: ${detail}`);\n else if (thrown instanceof ApiUnauthorizedError) this.log.info(`Unauthorized: ${detail}`);\n else if (thrown instanceof ApiForbiddenError) this.log.info(`Forbidden: ${detail}`);\n else if (thrown instanceof ApiConflictError) this.log.info(`Conflict: ${detail}`);\n else if (thrown instanceof ApiUnprocessableError) this.log.info(`Unprocessable: ${detail}`);\n else if (thrown instanceof ApiPreconditionFailedError)\n this.log.info(`Precondition Failed: ${detail}`);\n else if (thrown instanceof ApiUnsupportedMediaTypeError)\n this.log.info(`Unsupported Media Type: ${detail}`);\n else if (thrown instanceof ApiCodedError && thrown.isCallerError())\n this.log.info(`Coded ${thrown.statusCode}: ${detail}`);\n else if (thrown instanceof ApiRequestTimeoutError)\n this.log.error(`Request Timeout: ${detail}`);\n else this.log.error(`API failure: ${detail}`);\n }\n\n // webpieces-disable no-any-unknown -- thrown values are unknown until classified at this boundary\n private operatorDetail(thrown: unknown): string {\n const error = toError(thrown);\n const kind = this.apiOutcome(thrown)?.kind ?? 'implementation';\n const subType = thrown instanceof ApiError ? thrown.subType : undefined;\n const cause = error.cause instanceof Error ? ` cause=${error.cause.message}` : '';\n return `[name=${error.name} kind=${kind} subType=${subType ?? 'none'}] ${error.message}${cause}`;\n }\n}\n"]}