@webpieces/core-util 0.4.773 → 0.4.774

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/README.md CHANGED
@@ -103,6 +103,29 @@ try {
103
103
 
104
104
  This package has zero dependencies and works in all modern browsers and Node.js environments.
105
105
 
106
+ ## Typed streaming contracts
107
+
108
+ `@WpStream(() => RequestEvent, () => ResponseEvent)` marks one API method with the shared duplex
109
+ contract. The server receives request events through the returned `RequestStream`; callers receive
110
+ response events through the `ResponseStream` passed to the method. The same declaration drives the
111
+ HTTP server, Node and browser clients, and in-process feature client.
112
+
113
+ Every `event`, `fail`, and `complete` call awaits its next transport/consumer boundary. Events are
114
+ therefore ordered and backpressured rather than accumulated in an unbounded framework buffer.
115
+ `complete` and terminal `fail` permanently close that direction, writes after termination reject, and
116
+ `cancel` runs registered cancellation work once. Failures are terminal by default. A non-terminal
117
+ failure continues only on an adapter that explicitly supports it; otherwise it is promoted to
118
+ terminal. `StreamCorrelation` is an explicit stable application key and never relies on object
119
+ identity or event arrival order.
120
+
121
+ The generic HTTP request direction uses NDJSON: each `StreamEnvelope` is one JSON record followed by
122
+ `\n`. The response direction uses SSE: `event: message`, one or more `data:` lines, then a blank line.
123
+ Blank lines (`\n\n` or `\r\n\r\n`) dispatch an event, multiple `data:` lines join with `\n`, and a
124
+ leading `:` is a comment/keepalive. Generic SSE does not use `id` or `Last-Event-ID` for resumption.
125
+ Open failures use the ordinary typed Webpieces HTTP error/status mapping. Legal in-band failures carry
126
+ the standard `ApiErrorPayload`; disconnects, malformed frames, and failures that cannot be written are
127
+ reported locally as `StreamTransportError` with their cause and available correlation metadata.
128
+
106
129
  ## Related Packages
107
130
 
108
131
  - [@webpieces/nx-webpieces-rules](https://www.npmjs.com/package/@webpieces/nx-webpieces-rules) - Includes ESLint rule that enforces this pattern
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/core-util",
3
- "version": "0.4.773",
3
+ "version": "0.4.774",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -40,13 +40,25 @@ export declare class ApiConnectionError extends ApiError {
40
40
  export declare class ApiEndpointNotFoundError extends ApiNotFoundError {
41
41
  readonly kind: "endpoint-not-found";
42
42
  }
43
- /** Expected end-user mistake. Its message is deliberately caller-safe. */
43
+ /**
44
+ * Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor
45
+ * must fix (for example "the two passwords you entered do not match"). The actor may be a human user in
46
+ * a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately
47
+ * caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and
48
+ * `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model
49
+ * byte-for-byte.
50
+ */
44
51
  export declare class ApiEndUserError extends ApiError {
45
52
  errorCode?: string | undefined;
46
53
  readonly kind: "end-user";
47
54
  constructor(message: string, errorCode?: string | undefined, cause?: Error);
48
55
  }
49
- /** Invalid API caller input. message is diagnostic; callerMessage is safe to publish. */
56
+ /**
57
+ * Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic
58
+ * text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR
59
+ * an MCP model is shown so it can correct its input, so throw sites that want the caller to
60
+ * self-correct MUST set it.
61
+ */
50
62
  export declare class ApiBadRequestError extends ApiError {
51
63
  field?: string | undefined;
52
64
  callerMessage?: string | undefined;
@@ -58,7 +58,14 @@ class ApiEndpointNotFoundError extends ApiNotFoundError {
58
58
  kind = 'endpoint-not-found';
59
59
  }
60
60
  exports.ApiEndpointNotFoundError = ApiEndpointNotFoundError;
61
- /** Expected end-user mistake. Its message is deliberately caller-safe. */
61
+ /**
62
+ * Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor
63
+ * must fix (for example "the two passwords you entered do not match"). The actor may be a human user in
64
+ * a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately
65
+ * caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and
66
+ * `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model
67
+ * byte-for-byte.
68
+ */
62
69
  class ApiEndUserError extends ApiError {
63
70
  errorCode;
64
71
  kind = 'end-user';
@@ -69,7 +76,12 @@ class ApiEndUserError extends ApiError {
69
76
  }
70
77
  }
71
78
  exports.ApiEndUserError = ApiEndUserError;
72
- /** Invalid API caller input. message is diagnostic; callerMessage is safe to publish. */
79
+ /**
80
+ * Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic
81
+ * text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR
82
+ * an MCP model is shown so it can correct its input, so throw sites that want the caller to
83
+ * self-correct MUST set it.
84
+ */
73
85
  class ApiBadRequestError extends ApiError {
74
86
  field;
75
87
  callerMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"ApiError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiError.ts"],"names":[],"mappings":";;;AAgBA,8FAA8F;AAC9F,MAAsB,QAAS,SAAQ,KAAK;IAEjC,OAAO,CAAU;IAExB,YAAY,OAAe,EAAE,KAAa;QACtC,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IAChC,CAAC;CACJ;AARD,4BAQC;AAED,MAAa,gBAAiB,SAAQ,QAAQ;IACxB,IAAI,GAAiB,WAAW,CAAC;CACtD;AAFD,4CAEC;AACD,MAAa,kBAAmB,SAAQ,QAAQ;IAC1B,IAAI,GAAG,YAAqB,CAAC;CAClD;AAFD,gDAEC;AACD,MAAa,mBAAoB,SAAQ,QAAQ;IAC3B,IAAI,GAAG,aAAsB,CAAC;CACnD;AAFD,kDAEC;AACD,MAAa,yBAA0B,SAAQ,QAAQ;IACjC,IAAI,GAAG,oBAA6B,CAAC;CAC1D;AAFD,8DAEC;AAED,2GAA2G;AAC3G,MAAa,sBAAuB,SAAQ,QAAQ;IAM5B;IALF,IAAI,GAAG,gBAAyB,CAAC;IAEnD,YACI,OAAe,EACf,KAAa,EACG,cAAc,KAAK;QAEnC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAFN,gBAAW,GAAX,WAAW,CAAQ;IAGvC,CAAC;CACJ;AAVD,wDAUC;AAED,MAAa,mBAAoB,SAAQ,QAAQ;IAC3B,IAAI,GAAG,cAAuB,CAAC;CACpD;AAFD,kDAEC;AACD,MAAa,iBAAkB,SAAQ,QAAQ;IACzB,IAAI,GAAG,WAAoB,CAAC;CACjD;AAFD,8CAEC;AACD,MAAa,sBAAuB,SAAQ,QAAQ;IAC9B,IAAI,GAAG,iBAA0B,CAAC;CACvD;AAFD,wDAEC;AAED,gGAAgG;AAChG,MAAa,kBAAmB,SAAQ,QAAQ;IAC1B,IAAI,GAAG,YAAqB,CAAC;CAClD;AAFD,gDAEC;AAED,2EAA2E;AAC3E,MAAa,wBAAyB,SAAQ,gBAAgB;IACxC,IAAI,GAAG,oBAA6B,CAAC;CAC1D;AAFD,4DAEC;AAED,0EAA0E;AAC1E,MAAa,eAAgB,SAAQ,QAAQ;IAK9B;IAJO,IAAI,GAAG,UAAmB,CAAC;IAE7C,YACI,OAAe,EACR,SAAkB,EACzB,KAAa;QAEb,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAHf,cAAS,GAAT,SAAS,CAAS;QAIzB,IAAI,CAAC,OAAO,GAAG,YAAY,CAAC;IAChC,CAAC;CACJ;AAXD,0CAWC;AAED,yFAAyF;AACzF,MAAa,kBAAmB,SAAQ,QAAQ;IAKjC;IACA;IALO,IAAI,GAAG,aAAsB,CAAC;IAEhD,YACI,OAAe,EACR,KAAc,EACd,aAAsB,EAC7B,KAAa;QAEb,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAJf,UAAK,GAAL,KAAK,CAAS;QACd,kBAAa,GAAb,aAAa,CAAS;IAIjC,CAAC;CACJ;AAXD,gDAWC;AAED,MAAa,oBAAqB,SAAQ,QAAQ;IAC5B,IAAI,GAAG,cAAuB,CAAC;IAEjD,YAAY,OAAe,EAAE,OAAgB,EAAE,KAAa;QACxD,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AAPD,oDAOC;AAED,MAAa,yBAA0B,SAAQ,QAAQ;IAKxC;IAJO,IAAI,GAAG,oBAA6B,CAAC;IAEvD,YACI,OAAe,EACR,oBAAoB,EAAE,EAC7B,KAAa;QAEb,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAHf,sBAAiB,GAAjB,iBAAiB,CAAK;IAIjC,CAAC;CACJ;AAVD,8DAUC","sourcesContent":["export type ApiErrorKind =\n | 'end-user'\n | 'bad-request'\n | 'unauthorized'\n | 'forbidden'\n | 'not-found'\n | 'endpoint-not-found'\n | 'request-timeout'\n | 'rate-limited'\n | 'implementation'\n | 'dependency'\n | 'unavailable'\n | 'dependency-timeout'\n | 'dependency-backoff'\n | 'connection';\n\n/** Category for portable API failures. Concrete subclasses alone define protocol mappings. */\nexport abstract class ApiError extends Error {\n abstract readonly kind: ApiErrorKind;\n public subType?: string;\n\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = new.target.name;\n }\n}\n\nexport class ApiNotFoundError extends ApiError {\n override readonly kind: ApiErrorKind = 'not-found';\n}\nexport class ApiDependencyError extends ApiError {\n override readonly kind = 'dependency' as const;\n}\nexport class ApiUnavailableError extends ApiError {\n override readonly kind = 'unavailable' as const;\n}\nexport class ApiDependencyTimeoutError extends ApiError {\n override readonly kind = 'dependency-timeout' as const;\n}\n\n/** A bug or invalid implementation state. Local errors become server errors only after a remote decode. */\nexport class ApiImplementationError extends ApiError {\n override readonly kind = 'implementation' as const;\n\n constructor(\n message: string,\n cause?: Error,\n public readonly serverError = false,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiRateLimitedError extends ApiError {\n override readonly kind = 'rate-limited' as const;\n}\nexport class ApiForbiddenError extends ApiError {\n override readonly kind = 'forbidden' as const;\n}\nexport class ApiRequestTimeoutError extends ApiError {\n override readonly kind = 'request-timeout' as const;\n}\n\n/** A caller-local connection failure. HTTP servers normalize this to ApiImplementationError. */\nexport class ApiConnectionError extends ApiError {\n override readonly kind = 'connection' as const;\n}\n\n/** The API operation is missing, distinct from an absent domain entity. */\nexport class ApiEndpointNotFoundError extends ApiNotFoundError {\n override readonly kind = 'endpoint-not-found' as const;\n}\n\n/** Expected end-user mistake. Its message is deliberately caller-safe. */\nexport class ApiEndUserError extends ApiError {\n override readonly kind = 'end-user' as const;\n\n constructor(\n message: string,\n public errorCode?: string,\n cause?: Error,\n ) {\n super(message, cause);\n this.subType = 'USER_ERROR';\n }\n}\n\n/** Invalid API caller input. message is diagnostic; callerMessage is safe to publish. */\nexport class ApiBadRequestError extends ApiError {\n override readonly kind = 'bad-request' as const;\n\n constructor(\n message: string,\n public field?: string,\n public callerMessage?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiUnauthorizedError extends ApiError {\n override readonly kind = 'unauthorized' as const;\n\n constructor(message: string, subType?: string, cause?: Error) {\n super(message, cause);\n this.subType = subType;\n }\n}\n\nexport class ApiDependencyBackoffError extends ApiError {\n override readonly kind = 'dependency-backoff' as const;\n\n constructor(\n message: string,\n public retryAfterSeconds = 30,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n"]}
1
+ {"version":3,"file":"ApiError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiError.ts"],"names":[],"mappings":";;;AAgBA,8FAA8F;AAC9F,MAAsB,QAAS,SAAQ,KAAK;IAEjC,OAAO,CAAU;IAExB,YAAY,OAAe,EAAE,KAAa;QACtC,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IAChC,CAAC;CACJ;AARD,4BAQC;AAED,MAAa,gBAAiB,SAAQ,QAAQ;IACxB,IAAI,GAAiB,WAAW,CAAC;CACtD;AAFD,4CAEC;AACD,MAAa,kBAAmB,SAAQ,QAAQ;IAC1B,IAAI,GAAG,YAAqB,CAAC;CAClD;AAFD,gDAEC;AACD,MAAa,mBAAoB,SAAQ,QAAQ;IAC3B,IAAI,GAAG,aAAsB,CAAC;CACnD;AAFD,kDAEC;AACD,MAAa,yBAA0B,SAAQ,QAAQ;IACjC,IAAI,GAAG,oBAA6B,CAAC;CAC1D;AAFD,8DAEC;AAED,2GAA2G;AAC3G,MAAa,sBAAuB,SAAQ,QAAQ;IAM5B;IALF,IAAI,GAAG,gBAAyB,CAAC;IAEnD,YACI,OAAe,EACf,KAAa,EACG,cAAc,KAAK;QAEnC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAFN,gBAAW,GAAX,WAAW,CAAQ;IAGvC,CAAC;CACJ;AAVD,wDAUC;AAED,MAAa,mBAAoB,SAAQ,QAAQ;IAC3B,IAAI,GAAG,cAAuB,CAAC;CACpD;AAFD,kDAEC;AACD,MAAa,iBAAkB,SAAQ,QAAQ;IACzB,IAAI,GAAG,WAAoB,CAAC;CACjD;AAFD,8CAEC;AACD,MAAa,sBAAuB,SAAQ,QAAQ;IAC9B,IAAI,GAAG,iBAA0B,CAAC;CACvD;AAFD,wDAEC;AAED,gGAAgG;AAChG,MAAa,kBAAmB,SAAQ,QAAQ;IAC1B,IAAI,GAAG,YAAqB,CAAC;CAClD;AAFD,gDAEC;AAED,2EAA2E;AAC3E,MAAa,wBAAyB,SAAQ,gBAAgB;IACxC,IAAI,GAAG,oBAA6B,CAAC;CAC1D;AAFD,4DAEC;AAED;;;;;;;GAOG;AACH,MAAa,eAAgB,SAAQ,QAAQ;IAK9B;IAJO,IAAI,GAAG,UAAmB,CAAC;IAE7C,YACI,OAAe,EACR,SAAkB,EACzB,KAAa;QAEb,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAHf,cAAS,GAAT,SAAS,CAAS;QAIzB,IAAI,CAAC,OAAO,GAAG,YAAY,CAAC;IAChC,CAAC;CACJ;AAXD,0CAWC;AAED;;;;;GAKG;AACH,MAAa,kBAAmB,SAAQ,QAAQ;IAKjC;IACA;IALO,IAAI,GAAG,aAAsB,CAAC;IAEhD,YACI,OAAe,EACR,KAAc,EACd,aAAsB,EAC7B,KAAa;QAEb,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAJf,UAAK,GAAL,KAAK,CAAS;QACd,kBAAa,GAAb,aAAa,CAAS;IAIjC,CAAC;CACJ;AAXD,gDAWC;AAED,MAAa,oBAAqB,SAAQ,QAAQ;IAC5B,IAAI,GAAG,cAAuB,CAAC;IAEjD,YAAY,OAAe,EAAE,OAAgB,EAAE,KAAa;QACxD,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AAPD,oDAOC;AAED,MAAa,yBAA0B,SAAQ,QAAQ;IAKxC;IAJO,IAAI,GAAG,oBAA6B,CAAC;IAEvD,YACI,OAAe,EACR,oBAAoB,EAAE,EAC7B,KAAa;QAEb,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAHf,sBAAiB,GAAjB,iBAAiB,CAAK;IAIjC,CAAC;CACJ;AAVD,8DAUC","sourcesContent":["export type ApiErrorKind =\n | 'end-user'\n | 'bad-request'\n | 'unauthorized'\n | 'forbidden'\n | 'not-found'\n | 'endpoint-not-found'\n | 'request-timeout'\n | 'rate-limited'\n | 'implementation'\n | 'dependency'\n | 'unavailable'\n | 'dependency-timeout'\n | 'dependency-backoff'\n | 'connection';\n\n/** Category for portable API failures. Concrete subclasses alone define protocol mappings. */\nexport abstract class ApiError extends Error {\n abstract readonly kind: ApiErrorKind;\n public subType?: string;\n\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = new.target.name;\n }\n}\n\nexport class ApiNotFoundError extends ApiError {\n override readonly kind: ApiErrorKind = 'not-found';\n}\nexport class ApiDependencyError extends ApiError {\n override readonly kind = 'dependency' as const;\n}\nexport class ApiUnavailableError extends ApiError {\n override readonly kind = 'unavailable' as const;\n}\nexport class ApiDependencyTimeoutError extends ApiError {\n override readonly kind = 'dependency-timeout' as const;\n}\n\n/** A bug or invalid implementation state. Local errors become server errors only after a remote decode. */\nexport class ApiImplementationError extends ApiError {\n override readonly kind = 'implementation' as const;\n\n constructor(\n message: string,\n cause?: Error,\n public readonly serverError = false,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiRateLimitedError extends ApiError {\n override readonly kind = 'rate-limited' as const;\n}\nexport class ApiForbiddenError extends ApiError {\n override readonly kind = 'forbidden' as const;\n}\nexport class ApiRequestTimeoutError extends ApiError {\n override readonly kind = 'request-timeout' as const;\n}\n\n/** A caller-local connection failure. HTTP servers normalize this to ApiImplementationError. */\nexport class ApiConnectionError extends ApiError {\n override readonly kind = 'connection' as const;\n}\n\n/** The API operation is missing, distinct from an absent domain entity. */\nexport class ApiEndpointNotFoundError extends ApiNotFoundError {\n override readonly kind = 'endpoint-not-found' as const;\n}\n\n/**\n * Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor\n * must fix (for example \"the two passwords you entered do not match\"). The actor may be a human user in\n * a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately\n * caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and\n * `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model\n * byte-for-byte.\n */\nexport class ApiEndUserError extends ApiError {\n override readonly kind = 'end-user' as const;\n\n constructor(\n message: string,\n public errorCode?: string,\n cause?: Error,\n ) {\n super(message, cause);\n this.subType = 'USER_ERROR';\n }\n}\n\n/**\n * Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic\n * text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR\n * an MCP model is shown so it can correct its input, so throw sites that want the caller to\n * self-correct MUST set it.\n */\nexport class ApiBadRequestError extends ApiError {\n override readonly kind = 'bad-request' as const;\n\n constructor(\n message: string,\n public field?: string,\n public callerMessage?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiUnauthorizedError extends ApiError {\n override readonly kind = 'unauthorized' as const;\n\n constructor(message: string, subType?: string, cause?: Error) {\n super(message, cause);\n this.subType = subType;\n }\n}\n\nexport class ApiDependencyBackoffError extends ApiError {\n override readonly kind = 'dependency-backoff' as const;\n\n constructor(\n message: string,\n public retryAfterSeconds = 30,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n"]}
@@ -0,0 +1,24 @@
1
+ import { Logger } from '../logging/Logger';
2
+ import { ApiError } from './ApiError';
3
+ /**
4
+ * The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before
5
+ * 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
+ */
8
+ export declare class ApiErrorBoundary {
9
+ private readonly log;
10
+ constructor(log: Logger);
11
+ /**
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.
16
+ */
17
+ normalize(thrown: unknown): ApiError;
18
+ /**
19
+ * Logs one operator line at the per-kind level: expected caller outcomes at info, everything else
20
+ * at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).
21
+ */
22
+ logOperatorDetail(error: ApiError, correlation?: string): void;
23
+ private operatorDetail;
24
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiErrorBoundary = void 0;
4
+ const errorUtils_1 = require("../lib/errorUtils");
5
+ const ApiError_1 = require("./ApiError");
6
+ /**
7
+ * The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before
8
+ * 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
+ */
11
+ class ApiErrorBoundary {
12
+ log;
13
+ constructor(log) {
14
+ this.log = log;
15
+ }
16
+ /**
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.
21
+ */
22
+ // webpieces-disable no-any-unknown -- thrown values are unknown until normalized at this boundary
23
+ normalize(thrown) {
24
+ if (thrown instanceof ApiError_1.ApiError && !(thrown instanceof ApiError_1.ApiConnectionError))
25
+ return thrown;
26
+ const error = (0, errorUtils_1.toError)(thrown);
27
+ return new ApiError_1.ApiImplementationError(error.message, error);
28
+ }
29
+ /**
30
+ * Logs one operator line at the per-kind level: expected caller outcomes at info, everything else
31
+ * at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).
32
+ */
33
+ logOperatorDetail(error, correlation) {
34
+ const suffix = correlation ? ` ${correlation}` : '';
35
+ const detail = `${this.operatorDetail(error)}${suffix}`;
36
+ if (error instanceof ApiError_1.ApiEndUserError)
37
+ this.log.info(`End User Error: ${detail}`);
38
+ else if (error instanceof ApiError_1.ApiBadRequestError)
39
+ this.log.info(`Bad Request: ${detail}`);
40
+ else if (error instanceof ApiError_1.ApiNotFoundError)
41
+ this.log.info(`Not Found: ${detail}`);
42
+ else if (error instanceof ApiError_1.ApiUnauthorizedError)
43
+ this.log.info(`Unauthorized: ${detail}`);
44
+ else if (error instanceof ApiError_1.ApiForbiddenError)
45
+ this.log.info(`Forbidden: ${detail}`);
46
+ else if (error instanceof ApiError_1.ApiRequestTimeoutError)
47
+ this.log.error(`Request Timeout: ${detail}`);
48
+ else
49
+ this.log.error(`API failure: ${detail}`);
50
+ }
51
+ operatorDetail(error) {
52
+ const cause = error.cause instanceof Error ? ` cause=${error.cause.message}` : '';
53
+ return `[name=${error.name} kind=${error.kind} subType=${error.subType ?? 'none'}] ${error.message}${cause}`;
54
+ }
55
+ }
56
+ exports.ApiErrorBoundary = ApiErrorBoundary;
57
+ //# sourceMappingURL=ApiErrorBoundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiErrorBoundary.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiErrorBoundary.ts"],"names":[],"mappings":";;;AACA,kDAA4C;AAC5C,yCAUoB;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,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;AArCD,4CAqCC","sourcesContent":["import { Logger } from '../logging/Logger';\nimport { toError } from '../lib/errorUtils';\nimport {\n ApiBadRequestError,\n ApiConnectionError,\n ApiEndUserError,\n ApiError,\n ApiForbiddenError,\n ApiImplementationError,\n ApiNotFoundError,\n ApiRequestTimeoutError,\n ApiUnauthorizedError,\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 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,4 +1,5 @@
1
1
  export { ApiError, ApiNotFoundError, ApiDependencyError, ApiUnavailableError, ApiDependencyTimeoutError, ApiImplementationError, ApiRateLimitedError, ApiForbiddenError, ApiRequestTimeoutError, ApiConnectionError, ApiEndpointNotFoundError, ApiEndUserError, ApiBadRequestError, ApiUnauthorizedError, ApiDependencyBackoffError, } from './ApiError';
2
2
  export type { ApiErrorKind } from './ApiError';
3
3
  export { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';
4
+ export { ApiErrorBoundary } from './ApiErrorBoundary';
4
5
  export { ENTITY_NOT_FOUND, WRONG_LOGIN_TYPE, WRONG_LOGIN, NOT_APPROVED, EMAIL_NOT_CONFIRMED, WRONG_DOMAIN, WRONG_COMPANY, NO_REG_CODE, } from './AuthErrorSubtypes';
@@ -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.ApiErrorPayload = exports.ApiErrorCodec = 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.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.ApiErrorBoundary = exports.ApiErrorPayload = exports.ApiErrorCodec = 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.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; } });
@@ -20,6 +20,8 @@ Object.defineProperty(exports, "ApiDependencyBackoffError", { enumerable: true,
20
20
  var ApiErrorCodec_1 = require("./ApiErrorCodec");
21
21
  Object.defineProperty(exports, "ApiErrorCodec", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorCodec; } });
22
22
  Object.defineProperty(exports, "ApiErrorPayload", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorPayload; } });
23
+ var ApiErrorBoundary_1 = require("./ApiErrorBoundary");
24
+ Object.defineProperty(exports, "ApiErrorBoundary", { enumerable: true, get: function () { return ApiErrorBoundary_1.ApiErrorBoundary; } });
23
25
  var AuthErrorSubtypes_1 = require("./AuthErrorSubtypes");
24
26
  Object.defineProperty(exports, "ENTITY_NOT_FOUND", { enumerable: true, get: function () { return AuthErrorSubtypes_1.ENTITY_NOT_FOUND; } });
25
27
  Object.defineProperty(exports, "WRONG_LOGIN_TYPE", { enumerable: true, get: function () { return AuthErrorSubtypes_1.WRONG_LOGIN_TYPE; } });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAgBoB;AAfhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,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;AAG7B,iDAAiE;AAAxD,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AACvC,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 ApiUnavailableError,\n ApiDependencyTimeoutError,\n ApiImplementationError,\n ApiRateLimitedError,\n ApiForbiddenError,\n ApiRequestTimeoutError,\n ApiConnectionError,\n ApiEndpointNotFoundError,\n ApiEndUserError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiDependencyBackoffError,\n} from './ApiError';\nexport type { ApiErrorKind } from './ApiError';\nexport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nexport {\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './AuthErrorSubtypes';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAgBoB;AAfhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,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;AAG7B,iDAAiE;AAAxD,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AACvC,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,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 ApiUnavailableError,\n ApiDependencyTimeoutError,\n ApiImplementationError,\n ApiRateLimitedError,\n ApiForbiddenError,\n ApiRequestTimeoutError,\n ApiConnectionError,\n ApiEndpointNotFoundError,\n ApiEndUserError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiDependencyBackoffError,\n} from './ApiError';\nexport type { ApiErrorKind } from './ApiError';\nexport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nexport { ApiErrorBoundary } from './ApiErrorBoundary';\nexport {\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './AuthErrorSubtypes';\n"]}
@@ -1,6 +1,7 @@
1
1
  import { MaskSpec } from './LogFieldMask';
2
2
  import { AuthMeta } from './auth-mode';
3
3
  import { EndpointResponseType, HttpParameterBinding } from './HttpContract';
4
+ import { StreamingEndpointMetadata } from './StreamingContract';
4
5
  /**
5
6
  * Route metadata stored per-method at runtime.
6
7
  * Used internally by http-routing and http-client as the runtime representation
@@ -17,6 +18,8 @@ export declare class RouteMetadata {
17
18
  readonly bodyParameterIndex?: number | undefined;
18
19
  /** Whether clients receive only the body or the complete status/header/body response. */
19
20
  readonly responseType: EndpointResponseType;
21
+ /** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */
22
+ readonly streaming?: StreamingEndpointMetadata | undefined;
20
23
  httpMethod: string;
21
24
  path: string;
22
25
  methodName: string;
@@ -49,5 +52,7 @@ export declare class RouteMetadata {
49
52
  /** The sole unannotated POST parameter, or undefined for a bodyless route. */
50
53
  bodyParameterIndex?: number | undefined,
51
54
  /** Whether clients receive only the body or the complete status/header/body response. */
52
- responseType?: EndpointResponseType);
55
+ responseType?: EndpointResponseType,
56
+ /** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */
57
+ streaming?: StreamingEndpointMetadata | undefined);
53
58
  }
@@ -14,6 +14,7 @@ class RouteMetadata {
14
14
  parameterBindings;
15
15
  bodyParameterIndex;
16
16
  responseType;
17
+ streaming;
17
18
  httpMethod;
18
19
  path;
19
20
  methodName;
@@ -46,10 +47,13 @@ class RouteMetadata {
46
47
  /** The sole unannotated POST parameter, or undefined for a bodyless route. */
47
48
  bodyParameterIndex,
48
49
  /** Whether clients receive only the body or the complete status/header/body response. */
49
- responseType = 'body') {
50
+ responseType = 'body',
51
+ /** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */
52
+ streaming) {
50
53
  this.parameterBindings = parameterBindings;
51
54
  this.bodyParameterIndex = bodyParameterIndex;
52
55
  this.responseType = responseType;
56
+ this.streaming = streaming;
53
57
  this.httpMethod = httpMethod;
54
58
  this.path = path;
55
59
  this.methodName = methodName;
@@ -62,6 +66,7 @@ class RouteMetadata {
62
66
  this.parameterBindings = parameterBindings;
63
67
  this.bodyParameterIndex = bodyParameterIndex;
64
68
  this.responseType = responseType;
69
+ this.streaming = streaming;
65
70
  }
66
71
  }
67
72
  exports.RouteMetadata = RouteMetadata;
@@ -1 +1 @@
1
- {"version":3,"file":"RouteMetadata.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadata.ts"],"names":[],"mappings":";;;AAIA;;;;;;;;GAQG;AACH,MAAa,aAAa;IAuCT;IAEA;IAEA;IA1Cb,UAAU,CAAS;IACnB,IAAI,CAAS;IACb,UAAU,CAAS;IACnB,mBAAmB,CAAU;IAC7B,QAAQ,CAAY;IACpB,wFAAwF;IACxF,OAAO,CAAU;IACjB;;;;OAIG;IACM,QAAQ,CAAU;IAC3B;;;;OAIG;IACM,IAAI,CAAY;IACzB;;;;;OAKG;IACM,OAAO,CAAU;IAE1B,YACI,UAAkB,EAClB,IAAY,EACZ,UAAkB,EAClB,mBAA4B,EAC5B,QAAmB,EACnB,OAAgB,EAChB,WAAoB,KAAK,EACzB,IAAe,EACf,UAAmB,KAAK;IACxB,8DAA8D;IACrD,oBAAqD,EAAE;IAChE,8EAA8E;IACrE,kBAA2B;IACpC,yFAAyF;IAChF,eAAqC,MAAM;QAJ3C,sBAAiB,GAAjB,iBAAiB,CAAsC;QAEvD,uBAAkB,GAAlB,kBAAkB,CAAS;QAE3B,iBAAY,GAAZ,YAAY,CAA+B;QAEpD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAC3C,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;CACJ;AA1DD,sCA0DC","sourcesContent":["import { MaskSpec } from './LogFieldMask';\nimport { AuthMeta } from './auth-mode';\nimport { EndpointResponseType, HttpParameterBinding } from './HttpContract';\n\n/**\n * Route metadata stored per-method at runtime.\n * Used internally by http-routing and http-client as the runtime representation\n * of a route. Constructed from @ApiPath + @Endpoint metadata by ProxyClient\n * and ApiRoutingFactory.\n *\n * Lives in its own file (one class per file) purely for file size, exactly as `api-kind.ts` and\n * `external-caller.ts` were split off `decorators.ts` before it. Nothing about its role changed.\n */\nexport class RouteMetadata {\n httpMethod: string;\n path: string;\n methodName: string;\n controllerClassName?: string;\n authMeta?: AuthMeta;\n /** The API contract class name (e.g. 'SaveApi') — distinct from the controller name. */\n apiName?: string;\n /**\n * True when @Endpoint(..., { formPost: true }): the body is application/x-www-form-urlencoded\n * (flat key→value), not JSON. Rides the route metadata so the per-route body parse can branch\n * without knowing the apiClass/methodName. Default false = JSON.\n */\n readonly formPost: boolean;\n /**\n * The @MaskLog field-mask spec for this route, or undefined when the method declared none. Read\n * ONCE here at route-build time and handed to {@link LogApiCallImpl} via ApiMethodInfo, so the per-call\n * log path pays for masking only on routes that opted in (the rest stay on plain JSON.stringify).\n */\n readonly mask?: MaskSpec;\n /**\n * True when @Endpoint(..., { rawBody: true }): the transport must retain the verbatim bytes +\n * absolute url for the `@WpAuthWebhook` hook to verify a vendor signature over. Rides the route\n * metadata for the same reason {@link formPost} does — the transport adapter decides how to read\n * the body from the ROUTE, without knowing the apiClass/methodName.\n */\n readonly rawBody: boolean;\n\n constructor(\n httpMethod: string,\n path: string,\n methodName: string,\n controllerClassName?: string,\n authMeta?: AuthMeta,\n apiName?: string,\n formPost: boolean = false,\n mask?: MaskSpec,\n rawBody: boolean = false,\n /** Explicit path/query mappings, in API declaration order. */\n readonly parameterBindings: readonly HttpParameterBinding[] = [],\n /** The sole unannotated POST parameter, or undefined for a bodyless route. */\n readonly bodyParameterIndex?: number,\n /** Whether clients receive only the body or the complete status/header/body response. */\n readonly responseType: EndpointResponseType = 'body',\n ) {\n this.httpMethod = httpMethod;\n this.path = path;\n this.methodName = methodName;\n this.controllerClassName = controllerClassName;\n this.authMeta = authMeta;\n this.apiName = apiName;\n this.formPost = formPost;\n this.mask = mask;\n this.rawBody = rawBody;\n this.parameterBindings = parameterBindings;\n this.bodyParameterIndex = bodyParameterIndex;\n this.responseType = responseType;\n }\n}\n"]}
1
+ {"version":3,"file":"RouteMetadata.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadata.ts"],"names":[],"mappings":";;;AAKA;;;;;;;;GAQG;AACH,MAAa,aAAa;IAuCT;IAEA;IAEA;IAEA;IA5Cb,UAAU,CAAS;IACnB,IAAI,CAAS;IACb,UAAU,CAAS;IACnB,mBAAmB,CAAU;IAC7B,QAAQ,CAAY;IACpB,wFAAwF;IACxF,OAAO,CAAU;IACjB;;;;OAIG;IACM,QAAQ,CAAU;IAC3B;;;;OAIG;IACM,IAAI,CAAY;IACzB;;;;;OAKG;IACM,OAAO,CAAU;IAE1B,YACI,UAAkB,EAClB,IAAY,EACZ,UAAkB,EAClB,mBAA4B,EAC5B,QAAmB,EACnB,OAAgB,EAChB,WAAoB,KAAK,EACzB,IAAe,EACf,UAAmB,KAAK;IACxB,8DAA8D;IACrD,oBAAqD,EAAE;IAChE,8EAA8E;IACrE,kBAA2B;IACpC,yFAAyF;IAChF,eAAqC,MAAM;IACpD,+EAA+E;IACtE,SAAqC;QANrC,sBAAiB,GAAjB,iBAAiB,CAAsC;QAEvD,uBAAkB,GAAlB,kBAAkB,CAAS;QAE3B,iBAAY,GAAZ,YAAY,CAA+B;QAE3C,cAAS,GAAT,SAAS,CAA4B;QAE9C,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAC3C,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AA7DD,sCA6DC","sourcesContent":["import { MaskSpec } from './LogFieldMask';\nimport { AuthMeta } from './auth-mode';\nimport { EndpointResponseType, HttpParameterBinding } from './HttpContract';\nimport { StreamingEndpointMetadata } from './StreamingContract';\n\n/**\n * Route metadata stored per-method at runtime.\n * Used internally by http-routing and http-client as the runtime representation\n * of a route. Constructed from @ApiPath + @Endpoint metadata by ProxyClient\n * and ApiRoutingFactory.\n *\n * Lives in its own file (one class per file) purely for file size, exactly as `api-kind.ts` and\n * `external-caller.ts` were split off `decorators.ts` before it. Nothing about its role changed.\n */\nexport class RouteMetadata {\n httpMethod: string;\n path: string;\n methodName: string;\n controllerClassName?: string;\n authMeta?: AuthMeta;\n /** The API contract class name (e.g. 'SaveApi') — distinct from the controller name. */\n apiName?: string;\n /**\n * True when @Endpoint(..., { formPost: true }): the body is application/x-www-form-urlencoded\n * (flat key→value), not JSON. Rides the route metadata so the per-route body parse can branch\n * without knowing the apiClass/methodName. Default false = JSON.\n */\n readonly formPost: boolean;\n /**\n * The @MaskLog field-mask spec for this route, or undefined when the method declared none. Read\n * ONCE here at route-build time and handed to {@link LogApiCallImpl} via ApiMethodInfo, so the per-call\n * log path pays for masking only on routes that opted in (the rest stay on plain JSON.stringify).\n */\n readonly mask?: MaskSpec;\n /**\n * True when @Endpoint(..., { rawBody: true }): the transport must retain the verbatim bytes +\n * absolute url for the `@WpAuthWebhook` hook to verify a vendor signature over. Rides the route\n * metadata for the same reason {@link formPost} does — the transport adapter decides how to read\n * the body from the ROUTE, without knowing the apiClass/methodName.\n */\n readonly rawBody: boolean;\n\n constructor(\n httpMethod: string,\n path: string,\n methodName: string,\n controllerClassName?: string,\n authMeta?: AuthMeta,\n apiName?: string,\n formPost: boolean = false,\n mask?: MaskSpec,\n rawBody: boolean = false,\n /** Explicit path/query mappings, in API declaration order. */\n readonly parameterBindings: readonly HttpParameterBinding[] = [],\n /** The sole unannotated POST parameter, or undefined for a bodyless route. */\n readonly bodyParameterIndex?: number,\n /** Whether clients receive only the body or the complete status/header/body response. */\n readonly responseType: EndpointResponseType = 'body',\n /** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */\n readonly streaming?: StreamingEndpointMetadata,\n ) {\n this.httpMethod = httpMethod;\n this.path = path;\n this.methodName = methodName;\n this.controllerClassName = controllerClassName;\n this.authMeta = authMeta;\n this.apiName = apiName;\n this.formPost = formPost;\n this.mask = mask;\n this.rawBody = rawBody;\n this.parameterBindings = parameterBindings;\n this.bodyParameterIndex = bodyParameterIndex;\n this.responseType = responseType;\n this.streaming = streaming;\n }\n}\n"]}
@@ -6,6 +6,8 @@ import { RouteMetadata } from './RouteMetadata';
6
6
  */
7
7
  export declare class RouteMetadataFactory {
8
8
  static create(apiClass: Function, methodName: string, controllerClassName?: string): RouteMetadata;
9
+ private static validateEndpointOptions;
10
+ private static validateStreamingParameters;
9
11
  private static joinPath;
10
12
  private static httpMethod;
11
13
  private static responseType;
@@ -5,6 +5,7 @@ const decorators_1 = require("./decorators");
5
5
  const http_parameter_decorators_1 = require("./http-parameter-decorators");
6
6
  const HttpContract_1 = require("./HttpContract");
7
7
  const RouteMetadata_1 = require("./RouteMetadata");
8
+ const StreamingContract_1 = require("./StreamingContract");
8
9
  /**
9
10
  * Build the one runtime route model consumed by incoming routing, in-process clients, and both
10
11
  * generated HTTP clients. Keeping this join/validation here prevents four subtly different
@@ -20,20 +21,38 @@ class RouteMetadataFactory {
20
21
  }
21
22
  const fullPath = this.joinPath((0, decorators_1.getApiPath)(apiClass) ?? '', endpointPath);
22
23
  const options = (0, decorators_1.getEndpointOptions)(apiClass, methodName);
24
+ const streaming = (0, StreamingContract_1.getStreamingEndpoint)(apiClass, methodName);
23
25
  const httpMethod = this.httpMethod(options.httpMethod, apiClass, methodName);
24
- if (httpMethod === 'GET' && options.formPost === true) {
25
- throw new Error(`${apiClass.name}.${methodName} is GET and cannot declare formPost:true because GET has no request body.`);
26
- }
27
- if (httpMethod === 'GET' && options.rawBody === true) {
28
- throw new Error(`${apiClass.name}.${methodName} is GET and cannot declare rawBody:true because GET has no request body.`);
29
- }
26
+ this.validateEndpointOptions(apiClass, methodName, httpMethod, options.formPost, options.rawBody);
30
27
  const parameterTypes = this.parameterTypes(apiClass, methodName);
31
28
  const declarations = (0, http_parameter_decorators_1.getHttpParameterDeclarations)(apiClass, methodName);
32
- const bodyParameterIndex = this.validateParameters(apiClass, methodName, fullPath, httpMethod, parameterTypes, declarations);
29
+ const bodyParameterIndex = streaming
30
+ ? this.validateStreamingParameters(apiClass, methodName, httpMethod, parameterTypes, declarations)
31
+ : this.validateParameters(apiClass, methodName, fullPath, httpMethod, parameterTypes, declarations);
33
32
  const bindings = declarations
34
33
  .map((declaration) => new HttpContract_1.HttpParameterBinding(declaration.index, declaration.source, declaration.wireName, this.valueType(parameterTypes[declaration.index])))
35
34
  .sort((a, b) => a.index - b.index);
36
- return new RouteMetadata_1.RouteMetadata(httpMethod, fullPath, methodName, controllerClassName, (0, decorators_1.getAuthMeta)(apiClass, methodName), apiClass.name || 'UnknownApi', options.formPost === true, (0, decorators_1.getMaskSpec)(apiClass, methodName), options.rawBody === true, bindings, bodyParameterIndex, this.responseType(options.responseType, apiClass, methodName));
35
+ return new RouteMetadata_1.RouteMetadata(httpMethod, fullPath, methodName, controllerClassName, (0, decorators_1.getAuthMeta)(apiClass, methodName), apiClass.name || 'UnknownApi', options.formPost === true, (0, decorators_1.getMaskSpec)(apiClass, methodName), options.rawBody === true, bindings, bodyParameterIndex, this.responseType(options.responseType, apiClass, methodName), streaming);
36
+ }
37
+ // webpieces-disable no-function-outside-class -- private pure helper keeps the existing static metadata factory below the method-size limit
38
+ static validateEndpointOptions(apiClass, methodName, httpMethod, formPost, rawBody) {
39
+ if (httpMethod === 'GET' && formPost === true) {
40
+ throw new Error(`${apiClass.name}.${methodName} is GET and cannot declare formPost:true because GET has no request body.`);
41
+ }
42
+ if (httpMethod === 'GET' && rawBody === true) {
43
+ throw new Error(`${apiClass.name}.${methodName} is GET and cannot declare rawBody:true because GET has no request body.`);
44
+ }
45
+ }
46
+ // webpieces-disable no-function-outside-class -- private pure validator alongside existing contract validators
47
+ static validateStreamingParameters(apiClass, methodName, httpMethod, parameterTypes, declarations) {
48
+ const label = `${apiClass.name || 'Unknown'}.${methodName}`;
49
+ if (httpMethod !== 'POST')
50
+ throw new Error(`${label} is streaming and must use POST.`);
51
+ if (declarations.length > 0)
52
+ throw new Error(`${label} is streaming and cannot declare path/query parameters.`);
53
+ if (parameterTypes.length !== 1)
54
+ throw new Error(`${label} must take exactly one ResponseStream parameter.`);
55
+ return undefined;
37
56
  }
38
57
  // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory
39
58
  static joinPath(basePath, endpointPath) {
@@ -1 +1 @@
1
- {"version":3,"file":"RouteMetadataFactory.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadataFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n getApiPath,\n getAuthMeta,\n getEndpointOptions,\n getEndpoints,\n getMaskSpec,\n} from './decorators';\nimport { getHttpParameterDeclarations } from './http-parameter-decorators';\nimport {\n ContractHttpMethod,\n EndpointResponseType,\n HttpParameterBinding,\n HttpParameterDeclaration,\n HttpParameterValueType,\n} from './HttpContract';\nimport { RouteMetadata } from './RouteMetadata';\n\n/**\n * Build the one runtime route model consumed by incoming routing, in-process clients, and both\n * generated HTTP clients. Keeping this join/validation here prevents four subtly different\n * interpretations of the same decorators.\n */\nexport class RouteMetadataFactory {\n // webpieces-disable no-function-outside-class -- deterministic decorator metadata factory shared by DI-free browser and server runtimes\n static create(\n apiClass: Function,\n methodName: string,\n controllerClassName?: string,\n ): RouteMetadata {\n const endpoints = getEndpoints(apiClass) ?? {};\n const endpointPath = endpoints[methodName];\n if (endpointPath === undefined) {\n throw new Error(\n `No @Endpoint metadata for ${apiClass.name || 'Unknown'}.${methodName}.`,\n );\n }\n const fullPath = this.joinPath(getApiPath(apiClass) ?? '', endpointPath);\n const options = getEndpointOptions(apiClass, methodName);\n const httpMethod = this.httpMethod(options.httpMethod, apiClass, methodName);\n if (httpMethod === 'GET' && options.formPost === true) {\n throw new Error(\n `${apiClass.name}.${methodName} is GET and cannot declare formPost:true because GET has no request body.`,\n );\n }\n if (httpMethod === 'GET' && options.rawBody === true) {\n throw new Error(\n `${apiClass.name}.${methodName} is GET and cannot declare rawBody:true because GET has no request body.`,\n );\n }\n\n const parameterTypes = this.parameterTypes(apiClass, methodName);\n const declarations = getHttpParameterDeclarations(apiClass, methodName);\n const bodyParameterIndex = this.validateParameters(\n apiClass,\n methodName,\n fullPath,\n httpMethod,\n parameterTypes,\n declarations,\n );\n const bindings = declarations\n .map(\n (declaration: HttpParameterDeclaration) =>\n new HttpParameterBinding(\n declaration.index,\n declaration.source,\n declaration.wireName,\n this.valueType(parameterTypes[declaration.index]),\n ),\n )\n .sort((a: HttpParameterBinding, b: HttpParameterBinding) => a.index - b.index);\n\n return new RouteMetadata(\n httpMethod,\n fullPath,\n methodName,\n controllerClassName,\n getAuthMeta(apiClass, methodName),\n apiClass.name || 'UnknownApi',\n options.formPost === true,\n getMaskSpec(apiClass, methodName),\n options.rawBody === true,\n bindings,\n bodyParameterIndex,\n this.responseType(options.responseType, apiClass, methodName),\n );\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static joinPath(basePath: string, endpointPath: string): string {\n const joined = `${basePath}/${endpointPath}`.replace(/\\/{2,}/g, '/');\n return joined.startsWith('/') ? joined : `/${joined}`;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static httpMethod(\n value: ContractHttpMethod | undefined,\n apiClass: Function,\n methodName: string,\n ): ContractHttpMethod {\n const method = value ?? 'POST';\n if (method === 'GET' || method === 'POST') return method;\n throw new Error(\n `${apiClass.name}.${methodName} declares unsupported httpMethod '${String(method)}'; use GET or POST.`,\n );\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static responseType(\n value: EndpointResponseType | undefined,\n apiClass: Function,\n methodName: string,\n ): EndpointResponseType {\n const responseType = value ?? 'body';\n if (responseType === 'body' || responseType === 'full') return responseType;\n throw new Error(\n `${apiClass.name}.${methodName} declares unsupported responseType '${String(responseType)}'; use body or full.`,\n );\n }\n\n // webpieces-disable no-function-outside-class -- Reflect metadata lookup has no injectable dependency\n private static parameterTypes(apiClass: Function, methodName: string): Function[] {\n return Reflect.getMetadata('design:paramtypes', apiClass.prototype, methodName) ?? [];\n }\n\n // webpieces-disable no-function-outside-class -- private deterministic contract validation helper\n private static validateParameters(\n apiClass: Function,\n methodName: string,\n fullPath: string,\n httpMethod: ContractHttpMethod,\n parameterTypes: readonly Function[],\n declarations: readonly HttpParameterDeclaration[],\n ): number | undefined {\n const label = `${apiClass.name || 'Unknown'}.${methodName}`;\n const placeholders = [...fullPath.matchAll(/\\{([^{}]+)\\}/g)].map(\n (match: RegExpMatchArray) => match[1],\n );\n const pathNames = declarations\n .filter((declaration: HttpParameterDeclaration) => declaration.source === 'path')\n .map((declaration: HttpParameterDeclaration) => declaration.wireName);\n for (const placeholder of new Set(placeholders)) {\n if (!pathNames.includes(placeholder)) {\n throw new Error(\n `${label} path '${fullPath}' contains {${placeholder}} but no @PathParam('${placeholder}') parameter.`,\n );\n }\n }\n for (const pathName of pathNames) {\n if (!placeholders.includes(pathName)) {\n throw new Error(\n `${label} declares @PathParam('${pathName}') but '${fullPath}' has no {${pathName}} placeholder.`,\n );\n }\n }\n\n const seenWire = new Set<string>();\n const mappedIndexes = new Set<number>();\n for (const declaration of declarations) {\n const wireKey = `${declaration.source}:${declaration.wireName}`;\n if (seenWire.has(wireKey)) {\n throw new Error(`${label} maps '${wireKey}' more than once.`);\n }\n seenWire.add(wireKey);\n mappedIndexes.add(declaration.index);\n }\n\n const declaredCount = Math.max(\n parameterTypes.length,\n declarations.reduce(\n (highest: number, declaration: HttpParameterDeclaration) =>\n Math.max(highest, declaration.index + 1),\n 0,\n ),\n );\n const unbound: number[] = [];\n for (let index = 0; index < declaredCount; index++) {\n if (!mappedIndexes.has(index)) unbound.push(index);\n }\n if (httpMethod === 'GET' && unbound.length > 0) {\n throw new Error(\n `${label} is GET, so every parameter must use @PathParam(...) or @QueryParam(...); argument ${unbound[0]} is unmapped.`,\n );\n }\n if (unbound.length > 1) {\n throw new Error(\n `${label} has ${unbound.length} unmapped parameters. POST permits exactly one unannotated request body; map every other parameter explicitly.`,\n );\n }\n return unbound[0];\n }\n\n // webpieces-disable no-function-outside-class -- private conversion of reflected constructors to wire types\n private static valueType(type: Function | undefined): HttpParameterValueType {\n if (type === String) return 'string';\n if (type === Number) return 'number';\n if (type === Boolean) return 'boolean';\n if (type === Array) return 'array';\n return 'unknown';\n }\n}\n"]}
1
+ {"version":3,"file":"RouteMetadataFactory.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadataFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n getApiPath,\n getAuthMeta,\n getEndpointOptions,\n getEndpoints,\n getMaskSpec,\n} from './decorators';\nimport { getHttpParameterDeclarations } from './http-parameter-decorators';\nimport {\n ContractHttpMethod,\n EndpointResponseType,\n HttpParameterBinding,\n HttpParameterDeclaration,\n HttpParameterValueType,\n} from './HttpContract';\nimport { RouteMetadata } from './RouteMetadata';\nimport { getStreamingEndpoint } from './StreamingContract';\n\n/**\n * Build the one runtime route model consumed by incoming routing, in-process clients, and both\n * generated HTTP clients. Keeping this join/validation here prevents four subtly different\n * interpretations of the same decorators.\n */\nexport class RouteMetadataFactory {\n // webpieces-disable no-function-outside-class -- deterministic decorator metadata factory shared by DI-free browser and server runtimes\n static create(\n apiClass: Function,\n methodName: string,\n controllerClassName?: string,\n ): RouteMetadata {\n const endpoints = getEndpoints(apiClass) ?? {};\n const endpointPath = endpoints[methodName];\n if (endpointPath === undefined) {\n throw new Error(\n `No @Endpoint metadata for ${apiClass.name || 'Unknown'}.${methodName}.`,\n );\n }\n const fullPath = this.joinPath(getApiPath(apiClass) ?? '', endpointPath);\n const options = getEndpointOptions(apiClass, methodName);\n const streaming = getStreamingEndpoint(apiClass, methodName);\n const httpMethod = this.httpMethod(options.httpMethod, apiClass, methodName);\n this.validateEndpointOptions(\n apiClass,\n methodName,\n httpMethod,\n options.formPost,\n options.rawBody,\n );\n const parameterTypes = this.parameterTypes(apiClass, methodName);\n const declarations = getHttpParameterDeclarations(apiClass, methodName);\n const bodyParameterIndex = streaming\n ? this.validateStreamingParameters(\n apiClass,\n methodName,\n httpMethod,\n parameterTypes,\n declarations,\n )\n : this.validateParameters(\n apiClass,\n methodName,\n fullPath,\n httpMethod,\n parameterTypes,\n declarations,\n );\n const bindings = declarations\n .map(\n (declaration: HttpParameterDeclaration) =>\n new HttpParameterBinding(\n declaration.index,\n declaration.source,\n declaration.wireName,\n this.valueType(parameterTypes[declaration.index]),\n ),\n )\n .sort((a: HttpParameterBinding, b: HttpParameterBinding) => a.index - b.index);\n\n return new RouteMetadata(\n httpMethod,\n fullPath,\n methodName,\n controllerClassName,\n getAuthMeta(apiClass, methodName),\n apiClass.name || 'UnknownApi',\n options.formPost === true,\n getMaskSpec(apiClass, methodName),\n options.rawBody === true,\n bindings,\n bodyParameterIndex,\n this.responseType(options.responseType, apiClass, methodName),\n streaming,\n );\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper keeps the existing static metadata factory below the method-size limit\n private static validateEndpointOptions(\n apiClass: Function,\n methodName: string,\n httpMethod: ContractHttpMethod,\n formPost?: boolean,\n rawBody?: boolean,\n ): void {\n if (httpMethod === 'GET' && formPost === true) {\n throw new Error(\n `${apiClass.name}.${methodName} is GET and cannot declare formPost:true because GET has no request body.`,\n );\n }\n if (httpMethod === 'GET' && rawBody === true) {\n throw new Error(\n `${apiClass.name}.${methodName} is GET and cannot declare rawBody:true because GET has no request body.`,\n );\n }\n }\n\n // webpieces-disable no-function-outside-class -- private pure validator alongside existing contract validators\n private static validateStreamingParameters(\n apiClass: Function,\n methodName: string,\n httpMethod: ContractHttpMethod,\n parameterTypes: readonly Function[],\n declarations: readonly HttpParameterDeclaration[],\n ): undefined {\n const label = `${apiClass.name || 'Unknown'}.${methodName}`;\n if (httpMethod !== 'POST') throw new Error(`${label} is streaming and must use POST.`);\n if (declarations.length > 0)\n throw new Error(`${label} is streaming and cannot declare path/query parameters.`);\n if (parameterTypes.length !== 1)\n throw new Error(`${label} must take exactly one ResponseStream parameter.`);\n return undefined;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static joinPath(basePath: string, endpointPath: string): string {\n const joined = `${basePath}/${endpointPath}`.replace(/\\/{2,}/g, '/');\n return joined.startsWith('/') ? joined : `/${joined}`;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static httpMethod(\n value: ContractHttpMethod | undefined,\n apiClass: Function,\n methodName: string,\n ): ContractHttpMethod {\n const method = value ?? 'POST';\n if (method === 'GET' || method === 'POST') return method;\n throw new Error(\n `${apiClass.name}.${methodName} declares unsupported httpMethod '${String(method)}'; use GET or POST.`,\n );\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static responseType(\n value: EndpointResponseType | undefined,\n apiClass: Function,\n methodName: string,\n ): EndpointResponseType {\n const responseType = value ?? 'body';\n if (responseType === 'body' || responseType === 'full') return responseType;\n throw new Error(\n `${apiClass.name}.${methodName} declares unsupported responseType '${String(responseType)}'; use body or full.`,\n );\n }\n\n // webpieces-disable no-function-outside-class -- Reflect metadata lookup has no injectable dependency\n private static parameterTypes(apiClass: Function, methodName: string): Function[] {\n return Reflect.getMetadata('design:paramtypes', apiClass.prototype, methodName) ?? [];\n }\n\n // webpieces-disable no-function-outside-class -- private deterministic contract validation helper\n private static validateParameters(\n apiClass: Function,\n methodName: string,\n fullPath: string,\n httpMethod: ContractHttpMethod,\n parameterTypes: readonly Function[],\n declarations: readonly HttpParameterDeclaration[],\n ): number | undefined {\n const label = `${apiClass.name || 'Unknown'}.${methodName}`;\n const placeholders = [...fullPath.matchAll(/\\{([^{}]+)\\}/g)].map(\n (match: RegExpMatchArray) => match[1],\n );\n const pathNames = declarations\n .filter((declaration: HttpParameterDeclaration) => declaration.source === 'path')\n .map((declaration: HttpParameterDeclaration) => declaration.wireName);\n for (const placeholder of new Set(placeholders)) {\n if (!pathNames.includes(placeholder)) {\n throw new Error(\n `${label} path '${fullPath}' contains {${placeholder}} but no @PathParam('${placeholder}') parameter.`,\n );\n }\n }\n for (const pathName of pathNames) {\n if (!placeholders.includes(pathName)) {\n throw new Error(\n `${label} declares @PathParam('${pathName}') but '${fullPath}' has no {${pathName}} placeholder.`,\n );\n }\n }\n\n const seenWire = new Set<string>();\n const mappedIndexes = new Set<number>();\n for (const declaration of declarations) {\n const wireKey = `${declaration.source}:${declaration.wireName}`;\n if (seenWire.has(wireKey)) {\n throw new Error(`${label} maps '${wireKey}' more than once.`);\n }\n seenWire.add(wireKey);\n mappedIndexes.add(declaration.index);\n }\n\n const declaredCount = Math.max(\n parameterTypes.length,\n declarations.reduce(\n (highest: number, declaration: HttpParameterDeclaration) =>\n Math.max(highest, declaration.index + 1),\n 0,\n ),\n );\n const unbound: number[] = [];\n for (let index = 0; index < declaredCount; index++) {\n if (!mappedIndexes.has(index)) unbound.push(index);\n }\n if (httpMethod === 'GET' && unbound.length > 0) {\n throw new Error(\n `${label} is GET, so every parameter must use @PathParam(...) or @QueryParam(...); argument ${unbound[0]} is unmapped.`,\n );\n }\n if (unbound.length > 1) {\n throw new Error(\n `${label} has ${unbound.length} unmapped parameters. POST permits exactly one unannotated request body; map every other parameter explicitly.`,\n );\n }\n return unbound[0];\n }\n\n // webpieces-disable no-function-outside-class -- private conversion of reflected constructors to wire types\n private static valueType(type: Function | undefined): HttpParameterValueType {\n if (type === String) return 'string';\n if (type === Number) return 'number';\n if (type === Boolean) return 'boolean';\n if (type === Array) return 'array';\n return 'unknown';\n }\n}\n"]}
@@ -0,0 +1,82 @@
1
+ import 'reflect-metadata';
2
+ import { ApiError, ApiConnectionError } from '../errors/ApiError';
3
+ import { ApiErrorPayload } from '../errors/ApiErrorCodec';
4
+ import { ApiJsonSchema, DtoClass, DtoValue } from '../mcp/DtoSchema';
5
+ /** Stable application-level key for relating stream events and failures. */
6
+ export declare class StreamCorrelation {
7
+ readonly key: string | number;
8
+ readonly requestId?: string | undefined;
9
+ constructor(key: string | number, requestId?: string | undefined);
10
+ }
11
+ export interface StreamFailureOptions {
12
+ /** Defaults to true. Wire adapters may promote false to true when continuation is unsafe. */
13
+ terminal?: boolean;
14
+ }
15
+ /** Server-to-client half of a typed stream. Every write is an explicit backpressure boundary. */
16
+ export interface ResponseStream<T> {
17
+ event(value: T, correlation?: StreamCorrelation): Promise<void>;
18
+ fail(error: ApiError, correlation?: StreamCorrelation, options?: StreamFailureOptions): Promise<void>;
19
+ complete(): Promise<void>;
20
+ onCancel(handler: (reason?: unknown) => void | Promise<void>): void;
21
+ }
22
+ /** Client-to-server half. Cancellation exists immediately and is independent of writer readiness. */
23
+ export interface RequestStream<T> {
24
+ event(value: T, correlation?: StreamCorrelation): Promise<void>;
25
+ fail(error: ApiError, correlation?: StreamCorrelation, options?: StreamFailureOptions): Promise<void>;
26
+ complete(): Promise<void>;
27
+ cancel(reason?: unknown): Promise<void>;
28
+ }
29
+ export type StreamEnvelopeKind = 'event' | 'failure' | 'complete';
30
+ /** Transport-neutral record used by NDJSON and SSE adapters. */
31
+ export declare class StreamEnvelope<T = DtoValue> {
32
+ readonly kind: StreamEnvelopeKind;
33
+ readonly value?: T | undefined;
34
+ readonly error?: ApiErrorPayload | undefined;
35
+ readonly correlation?: StreamCorrelation | undefined;
36
+ readonly terminal?: boolean | undefined;
37
+ constructor(kind: StreamEnvelopeKind, value?: T | undefined, error?: ApiErrorPayload | undefined, correlation?: StreamCorrelation | undefined, terminal?: boolean | undefined);
38
+ }
39
+ /** Local failure where no legal in-band envelope could be sent or decoded. */
40
+ export declare class StreamTransportError extends ApiConnectionError {
41
+ readonly correlation?: StreamCorrelation | undefined;
42
+ readonly requestId?: string | undefined;
43
+ constructor(message: string, correlation?: StreamCorrelation | undefined, requestId?: string | undefined, cause?: Error);
44
+ }
45
+ /** Runtime schema and wire-policy metadata for one streaming method. */
46
+ export declare class StreamingEndpointMetadata {
47
+ readonly requestEventClass: DtoClass;
48
+ readonly responseEventClass: DtoClass;
49
+ /** Generic NDJSON/SSE supports non-terminal failures in both directions. */
50
+ readonly supportsNonTerminalFailures: boolean;
51
+ readonly requestSchema: ApiJsonSchema;
52
+ readonly responseSchema: ApiJsonSchema;
53
+ constructor(requestEventClass: DtoClass, responseEventClass: DtoClass,
54
+ /** Generic NDJSON/SSE supports non-terminal failures in both directions. */
55
+ supportsNonTerminalFailures?: boolean);
56
+ }
57
+ /** Marks `(ResponseStream<ResponseEvent>) => Promise<RequestStream<RequestEvent>>`. */
58
+ export declare function WpStream(requestEvent: () => DtoClass, responseEvent: () => DtoClass): MethodDecorator;
59
+ export declare function getStreamingEndpoint(apiClass: Function, methodName: string): StreamingEndpointMetadata | undefined;
60
+ /** Validate a typed event at every adapter boundary, with one consistent diagnostic. */
61
+ export declare class StreamEventValidator {
62
+ private readonly schemas;
63
+ validate(dtoClass: DtoClass, value: unknown, direction: 'request' | 'response'): void;
64
+ }
65
+ /** Awaiting the sink acknowledges the write, so adapters cannot outrun their consumer. */
66
+ export declare class StreamWriter<T> implements RequestStream<T>, ResponseStream<T> {
67
+ private readonly sink;
68
+ private readonly validateEvent?;
69
+ private readonly allowNonTerminalFailures;
70
+ private terminal;
71
+ private cancelled;
72
+ private pending;
73
+ private readonly cancelHandlers;
74
+ constructor(sink: (envelope: StreamEnvelope<T>) => Promise<void>, validateEvent?: ((value: T) => void) | undefined, allowNonTerminalFailures?: boolean);
75
+ event(value: T, correlation?: StreamCorrelation): Promise<void>;
76
+ fail(error: ApiError, correlation?: StreamCorrelation, options?: StreamFailureOptions): Promise<void>;
77
+ complete(): Promise<void>;
78
+ onCancel(handler: (reason?: unknown) => void | Promise<void>): void;
79
+ cancel(reason?: unknown): Promise<void>;
80
+ private requireWritable;
81
+ private enqueue;
82
+ }