@webpieces/core-util 0.4.956 → 0.4.958

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/package.json +1 -1
  2. package/src/http/ApiCallInfo.d.ts +71 -11
  3. package/src/http/ApiCallInfo.js +81 -7
  4. package/src/http/ApiCallInfo.js.map +1 -1
  5. package/src/http/ApiMethodInfo.d.ts +14 -0
  6. package/src/http/ApiMethodInfo.js +3 -0
  7. package/src/http/ApiMethodInfo.js.map +1 -1
  8. package/src/http/CallRegistry.d.ts +5 -10
  9. package/src/http/CallRegistry.js +21 -43
  10. package/src/http/CallRegistry.js.map +1 -1
  11. package/src/http/CallRegistryCompileAssertions.js +1 -1
  12. package/src/http/CallRegistryCompileAssertions.js.map +1 -1
  13. package/src/http/CallScope.d.ts +33 -0
  14. package/src/http/CallScope.js +70 -0
  15. package/src/http/CallScope.js.map +1 -0
  16. package/src/http/CallStrategy.d.ts +13 -1
  17. package/src/http/CallStrategy.js +4 -1
  18. package/src/http/CallStrategy.js.map +1 -1
  19. package/src/http/ClientRegistry.d.ts +54 -58
  20. package/src/http/ClientRegistry.js +72 -73
  21. package/src/http/ClientRegistry.js.map +1 -1
  22. package/src/http/ContextMgr.js +12 -2
  23. package/src/http/ContextMgr.js.map +1 -1
  24. package/src/http/DeletedAuthCompileAssertions.js +3 -1
  25. package/src/http/DeletedAuthCompileAssertions.js.map +1 -1
  26. package/src/http/DestinationTrust.d.ts +33 -4
  27. package/src/http/DestinationTrust.js +54 -11
  28. package/src/http/DestinationTrust.js.map +1 -1
  29. package/src/http/EndpointCompileAssertions.js +12 -6
  30. package/src/http/EndpointCompileAssertions.js.map +1 -1
  31. package/src/http/ErrorClassifier.d.ts +20 -27
  32. package/src/http/ErrorClassifier.js +1 -15
  33. package/src/http/ErrorClassifier.js.map +1 -1
  34. package/src/http/ErrorLadderCompileAssertions.d.ts +1 -0
  35. package/src/http/ErrorLadderCompileAssertions.js +42 -0
  36. package/src/http/ErrorLadderCompileAssertions.js.map +1 -0
  37. package/src/http/ErrorTranslator.d.ts +8 -4
  38. package/src/http/ErrorTranslator.js.map +1 -1
  39. package/src/http/ExternalContextKeys.d.ts +31 -0
  40. package/src/http/ExternalContextKeys.js +77 -0
  41. package/src/http/ExternalContextKeys.js.map +1 -0
  42. package/src/http/HttpContract.d.ts +45 -7
  43. package/src/http/HttpContract.js +84 -6
  44. package/src/http/HttpContract.js.map +1 -1
  45. package/src/http/HttpEndpointOptions.d.ts +22 -3
  46. package/src/http/HttpEndpointOptions.js +8 -1
  47. package/src/http/HttpEndpointOptions.js.map +1 -1
  48. package/src/http/LogApiCall.d.ts +4 -1
  49. package/src/http/LogApiCall.js +9 -7
  50. package/src/http/LogApiCall.js.map +1 -1
  51. package/src/http/QueryParams.d.ts +47 -0
  52. package/src/http/QueryParams.js +89 -0
  53. package/src/http/QueryParams.js.map +1 -0
  54. package/src/http/RetryStrategy.js +1 -1
  55. package/src/http/RetryStrategy.js.map +1 -1
  56. package/src/http/RouteMetadata.d.ts +16 -2
  57. package/src/http/RouteMetadata.js +19 -3
  58. package/src/http/RouteMetadata.js.map +1 -1
  59. package/src/http/RouteMetadataFactory.d.ts +25 -0
  60. package/src/http/RouteMetadataFactory.js +97 -12
  61. package/src/http/RouteMetadataFactory.js.map +1 -1
  62. package/src/http/StreamErrorControl.d.ts +9 -1
  63. package/src/http/StreamErrorControl.js +14 -3
  64. package/src/http/StreamErrorControl.js.map +1 -1
  65. package/src/http/WebpiecesDefaultErrorClassifier.d.ts +2 -2
  66. package/src/http/WebpiecesDefaultErrorClassifier.js +2 -2
  67. package/src/http/WebpiecesDefaultErrorClassifier.js.map +1 -1
  68. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +2 -1
  69. package/src/http/WebpiecesDefaultErrorTranslator.js +2 -1
  70. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
  71. package/src/http/WpAuthApiKeyCompileAssertions.js +2 -2
  72. package/src/http/WpAuthApiKeyCompileAssertions.js.map +1 -1
  73. package/src/http/api-kind.d.ts +3 -2
  74. package/src/http/api-kind.js +5 -4
  75. package/src/http/api-kind.js.map +1 -1
  76. package/src/http/auth-mode.d.ts +8 -2
  77. package/src/http/auth-mode.js +11 -5
  78. package/src/http/auth-mode.js.map +1 -1
  79. package/src/http/decorators.d.ts +39 -21
  80. package/src/http/decorators.js +69 -37
  81. package/src/http/decorators.js.map +1 -1
  82. package/src/http/external-caller.d.ts +12 -17
  83. package/src/http/external-caller.js +12 -17
  84. package/src/http/external-caller.js.map +1 -1
  85. package/src/http/http-parameter-decorators.d.ts +17 -3
  86. package/src/http/http-parameter-decorators.js +27 -6
  87. package/src/http/http-parameter-decorators.js.map +1 -1
  88. package/src/index.d.ts +14 -9
  89. package/src/index.js +32 -13
  90. package/src/index.js.map +1 -1
  91. package/src/ipc/IpcConnection.js +1 -1
  92. package/src/ipc/IpcConnection.js.map +1 -1
  93. package/src/ipc/IpcLogging.js +1 -1
  94. package/src/ipc/IpcLogging.js.map +1 -1
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ClientRegistry_1 = require("./ClientRegistry");
4
+ const ApiMethodInfo_1 = require("./ApiMethodInfo");
5
+ const CallStrategy_1 = require("./CallStrategy");
6
+ const HttpEndpointOptions_1 = require("./HttpEndpointOptions");
7
+ /**
8
+ * Compiled by tsc (spec files are not), never executed. Pins the ONE spelling of the error ladder:
9
+ * every registration states its `CallScope`, and the unscoped spellings it replaced do not compile.
10
+ * tsc fails the build with TS2578 if any of these ever starts compiling again.
11
+ */
12
+ class ErrorLadderCompileAssertions {
13
+ scopeIsRequired(translator, classifier) {
14
+ // @ts-expect-error setErrorTranslator(t) became setErrorTranslator(t, 'ALL') — the scope is required
15
+ ClientRegistry_1.ClientRegistry.setErrorTranslator(translator);
16
+ // @ts-expect-error setDefaultErrorClassifier(c) became setErrorClassifier(c, 'ALL')
17
+ ClientRegistry_1.ClientRegistry.setDefaultErrorClassifier(classifier);
18
+ // @ts-expect-error addErrorClassifier(new KeyedErrorClassifier('XApi', c)) became setErrorClassifier(c, XApi)
19
+ ClientRegistry_1.ClientRegistry.addErrorClassifier(classifier);
20
+ // @ts-expect-error the scope is required — setErrorClassifier(c, 'ALL')
21
+ ClientRegistry_1.ClientRegistry.setErrorClassifier(classifier);
22
+ // @ts-expect-error getErrorTranslator() became getErrorTranslator(api, methodName)
23
+ ClientRegistry_1.ClientRegistry.getErrorTranslator();
24
+ }
25
+ allHasNoMethod(translator, classifier) {
26
+ // @ts-expect-error ALL cannot select a method
27
+ ClientRegistry_1.ClientRegistry.setErrorTranslator(translator, 'ALL', 'work');
28
+ // @ts-expect-error ALL cannot select a method
29
+ ClientRegistry_1.ClientRegistry.setErrorClassifier(classifier, 'ALL', 'work');
30
+ }
31
+ callIdentityStatesItsContract() {
32
+ // @ts-expect-error ApiMethodInfo's contract CLASS is required (pass undefined only when there is none)
33
+ void new ApiMethodInfo_1.ApiMethodInfo('client', 'SaveApi', 'save');
34
+ // @ts-expect-error CallContext's contract CLASS is required (pass undefined only when there is none)
35
+ void new CallStrategy_1.CallContext('SaveApi', 'save', HttpEndpointOptions_1.WRITE);
36
+ }
37
+ keyedByClassNeverByName(classifier) {
38
+ // @ts-expect-error the ladder is keyed by the contract CLASS, never its name
39
+ ClientRegistry_1.ClientRegistry.setErrorClassifier(classifier, 'SaveApi');
40
+ }
41
+ }
42
+ //# sourceMappingURL=ErrorLadderCompileAssertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ErrorLadderCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorLadderCompileAssertions.ts"],"names":[],"mappings":";;AAAA,qDAAkD;AAGlD,mDAAgD;AAChD,iDAA6C;AAC7C,+DAA8C;AAE9C;;;;GAIG;AACH,MAAM,4BAA4B;IAC9B,eAAe,CAAC,UAA2B,EAAE,UAA2B;QACpE,qGAAqG;QACrG,+BAAc,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC9C,oFAAoF;QACpF,+BAAc,CAAC,yBAAyB,CAAC,UAAU,CAAC,CAAC;QACrD,8GAA8G;QAC9G,+BAAc,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC9C,wEAAwE;QACxE,+BAAc,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC9C,mFAAmF;QACnF,+BAAc,CAAC,kBAAkB,EAAE,CAAC;IACxC,CAAC;IAED,cAAc,CAAC,UAA2B,EAAE,UAA2B;QACnE,8CAA8C;QAC9C,+BAAc,CAAC,kBAAkB,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QAC7D,8CAA8C;QAC9C,+BAAc,CAAC,kBAAkB,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACjE,CAAC;IAED,6BAA6B;QACzB,uGAAuG;QACvG,KAAK,IAAI,6BAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QACpD,qGAAqG;QACrG,KAAK,IAAI,0BAAW,CAAC,SAAS,EAAE,MAAM,EAAE,2BAAK,CAAC,CAAC;IACnD,CAAC;IAED,uBAAuB,CAAC,UAA2B;QAC/C,6EAA6E;QAC7E,+BAAc,CAAC,kBAAkB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC7D,CAAC;CACJ","sourcesContent":["import { ClientRegistry } from './ClientRegistry';\nimport { ErrorClassifier } from './ErrorClassifier';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { CallContext } from './CallStrategy';\nimport { WRITE } from './HttpEndpointOptions';\n\n/**\n * Compiled by tsc (spec files are not), never executed. Pins the ONE spelling of the error ladder:\n * every registration states its `CallScope`, and the unscoped spellings it replaced do not compile.\n * tsc fails the build with TS2578 if any of these ever starts compiling again.\n */\nclass ErrorLadderCompileAssertions {\n scopeIsRequired(translator: ErrorTranslator, classifier: ErrorClassifier): void {\n // @ts-expect-error setErrorTranslator(t) became setErrorTranslator(t, 'ALL') — the scope is required\n ClientRegistry.setErrorTranslator(translator);\n // @ts-expect-error setDefaultErrorClassifier(c) became setErrorClassifier(c, 'ALL')\n ClientRegistry.setDefaultErrorClassifier(classifier);\n // @ts-expect-error addErrorClassifier(new KeyedErrorClassifier('XApi', c)) became setErrorClassifier(c, XApi)\n ClientRegistry.addErrorClassifier(classifier);\n // @ts-expect-error the scope is required — setErrorClassifier(c, 'ALL')\n ClientRegistry.setErrorClassifier(classifier);\n // @ts-expect-error getErrorTranslator() became getErrorTranslator(api, methodName)\n ClientRegistry.getErrorTranslator();\n }\n\n allHasNoMethod(translator: ErrorTranslator, classifier: ErrorClassifier): void {\n // @ts-expect-error ALL cannot select a method\n ClientRegistry.setErrorTranslator(translator, 'ALL', 'work');\n // @ts-expect-error ALL cannot select a method\n ClientRegistry.setErrorClassifier(classifier, 'ALL', 'work');\n }\n\n callIdentityStatesItsContract(): void {\n // @ts-expect-error ApiMethodInfo's contract CLASS is required (pass undefined only when there is none)\n void new ApiMethodInfo('client', 'SaveApi', 'save');\n // @ts-expect-error CallContext's contract CLASS is required (pass undefined only when there is none)\n void new CallContext('SaveApi', 'save', WRITE);\n }\n\n keyedByClassNeverByName(classifier: ErrorClassifier): void {\n // @ts-expect-error the ladder is keyed by the contract CLASS, never its name\n ClientRegistry.setErrorClassifier(classifier, 'SaveApi');\n }\n}\n"]}
@@ -4,8 +4,12 @@ import { HttpResponseDto } from './HttpResponseDto';
4
4
  * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over
5
5
  * the WHOLE response.
6
6
  *
7
- * An app implements this ONCE and registers it ONCE per process via
8
- * {@link ClientRegistry.setErrorTranslator} — on the server AND in the browser. Its `toWire` runs on
7
+ * An app implements this ONCE and registers it per process via
8
+ * `ClientRegistry.setErrorTranslator(t, 'ALL')` — on the server AND in the browser. A contract whose
9
+ * errors travel in a DIFFERENT wire format (a vendor's own 4xx body, or the error body an inbound
10
+ * vendor expects back) gets its own translator on the same `CallScope` ladder as `CallRegistry`'s
11
+ * strategy and timeout: `setErrorTranslator(t, VendorApi)` or `setErrorTranslator(t, VendorApi,
12
+ * 'method')`, resolved method -> API -> ALL -> webpieces default. Its `toWire` runs on
9
13
  * the SERVER (`ExpressWrapper.handleError`) and its `fromWire` runs on every CLIENT in that process
10
14
  * (`ClientErrorTranslator.throwIfFailure`, shared by `http-client-node` and `http-client-browser`).
11
15
  * The payoff is type symmetry across the wire: the server throws `OrderNotFoundError` and the caller
@@ -13,7 +17,7 @@ import { HttpResponseDto } from './HttpResponseDto';
13
17
  *
14
18
  * # THERE IS ALWAYS A TRANSLATOR
15
19
  *
16
- * {@link ClientRegistry.getErrorTranslator} is non-optional and starts life holding
20
+ * {@link ClientRegistry.getErrorTranslator} is non-optional: below every rung of the ladder sits
17
21
  * {@link WebpiecesDefaultErrorTranslator}. So no caller anywhere asks "did anyone register one" —
18
22
  * every call site is ONE unconditional line. A registered app translator REPLACES the default and
19
23
  * declines by DELEGATING to it:
@@ -89,7 +93,7 @@ import { HttpResponseDto } from './HttpResponseDto';
89
93
  * }
90
94
  *
91
95
  * // startup, ONCE per process (server AND browser)
92
- * ClientRegistry.setErrorTranslator(new OrderErrorTranslator());
96
+ * ClientRegistry.setErrorTranslator(new OrderErrorTranslator(), 'ALL');
93
97
  * ```
94
98
  */
95
99
  export interface ErrorTranslator {
@@ -1 +1 @@
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"]}
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 per process via\n * `ClientRegistry.setErrorTranslator(t, 'ALL')` — on the server AND in the browser. A contract whose\n * errors travel in a DIFFERENT wire format (a vendor's own 4xx body, or the error body an inbound\n * vendor expects back) gets its own translator on the same `CallScope` ladder as `CallRegistry`'s\n * strategy and timeout: `setErrorTranslator(t, VendorApi)` or `setErrorTranslator(t, VendorApi,\n * 'method')`, resolved method -> API -> ALL -> webpieces default. 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: below every rung of the ladder sits\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(), 'ALL');\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"]}
@@ -0,0 +1,31 @@
1
+ import 'reflect-metadata';
2
+ import { AnyContextKey } from '../ContextKey';
3
+ /**
4
+ * `@ExternalContextKeys(...keys)` — the ONLY request-context keys an `EXTERNAL_OUTBOUND` client sends
5
+ * to the outside system it calls.
6
+ *
7
+ * WHY a per-CONTRACT list and not a flag on the `ContextKey`: a global "send to vendors" flag would
8
+ * send Toast's restaurant id to Clover. Which context a vendor may see is a fact about THAT vendor's
9
+ * API, so it is declared on that vendor's contract. Without this, an outbound client sends nothing
10
+ * of ours — not `x-request-id`, not any `x-webpieces-*` key, not an app's untrusted transferred keys —
11
+ * because a vendor is somebody else's process.
12
+ *
13
+ * On the CLASS it applies to every endpoint; on a METHOD it ADDS keys for that method only. A listed
14
+ * key with no value in the request context makes the client throw BEFORE any I/O: the vendor needs it
15
+ * (it is how the call is routed, e.g. a merchant id), and a request without it is a bug in our code.
16
+ * Every listed key must declare an `httpHeader` — that is its wire name.
17
+ *
18
+ * ```ts
19
+ * @ApiPath('')
20
+ * @ExternalContextKeys(WebpiecesCoreHeaders.REQUEST_ID)
21
+ * export abstract class PartnerWebhookApi { ... }
22
+ * ```
23
+ */
24
+ export declare function ExternalContextKeys(...keys: AnyContextKey[]): ClassDecorator & MethodDecorator;
25
+ /**
26
+ * The keys one method sends: the class-level list, then the method's additions, de-duplicated.
27
+ * Empty when the contract declares none.
28
+ */
29
+ export declare function getExternalContextKeys(apiClass: Function, methodName: string): AnyContextKey[];
30
+ /** True when the class or any of its methods carries `@ExternalContextKeys`. */
31
+ export declare function declaresExternalContextKeys(apiClass: Function): boolean;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExternalContextKeys = ExternalContextKeys;
4
+ exports.getExternalContextKeys = getExternalContextKeys;
5
+ exports.declaresExternalContextKeys = declaresExternalContextKeys;
6
+ require("reflect-metadata");
7
+ /** Class-level keys: every endpoint of the contract sends them. */
8
+ const CLASS_KEYS = 'webpieces:external-context-keys:class';
9
+ /** Method-level keys, `methodName -> keys`: ADDED to the class-level ones for that method only. */
10
+ const METHOD_KEYS = 'webpieces:external-context-keys:method';
11
+ /**
12
+ * `@ExternalContextKeys(...keys)` — the ONLY request-context keys an `EXTERNAL_OUTBOUND` client sends
13
+ * to the outside system it calls.
14
+ *
15
+ * WHY a per-CONTRACT list and not a flag on the `ContextKey`: a global "send to vendors" flag would
16
+ * send Toast's restaurant id to Clover. Which context a vendor may see is a fact about THAT vendor's
17
+ * API, so it is declared on that vendor's contract. Without this, an outbound client sends nothing
18
+ * of ours — not `x-request-id`, not any `x-webpieces-*` key, not an app's untrusted transferred keys —
19
+ * because a vendor is somebody else's process.
20
+ *
21
+ * On the CLASS it applies to every endpoint; on a METHOD it ADDS keys for that method only. A listed
22
+ * key with no value in the request context makes the client throw BEFORE any I/O: the vendor needs it
23
+ * (it is how the call is routed, e.g. a merchant id), and a request without it is a bug in our code.
24
+ * Every listed key must declare an `httpHeader` — that is its wire name.
25
+ *
26
+ * ```ts
27
+ * @ApiPath('')
28
+ * @ExternalContextKeys(WebpiecesCoreHeaders.REQUEST_ID)
29
+ * export abstract class PartnerWebhookApi { ... }
30
+ * ```
31
+ */
32
+ // webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope
33
+ function ExternalContextKeys(...keys) {
34
+ if (keys.length === 0) {
35
+ throw new Error('@ExternalContextKeys(...) requires at least one ContextKey.');
36
+ }
37
+ for (const key of keys) {
38
+ if (key.httpHeader === undefined || key.httpHeader === '') {
39
+ throw new Error(`@ExternalContextKeys: ContextKey '${key.name}' declares no httpHeader, so it has no wire name to send it under.`);
40
+ }
41
+ }
42
+ // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any
43
+ return (target, propertyKey) => {
44
+ if (propertyKey === undefined) {
45
+ Reflect.defineMetadata(CLASS_KEYS, [...keys], target);
46
+ return;
47
+ }
48
+ const metadataTarget = typeof target === 'function' ? target : target.constructor;
49
+ const all = {
50
+ ...(Reflect.getMetadata(METHOD_KEYS, metadataTarget) ?? {}),
51
+ };
52
+ all[propertyKey] = [...keys];
53
+ Reflect.defineMetadata(METHOD_KEYS, all, metadataTarget);
54
+ };
55
+ }
56
+ /**
57
+ * The keys one method sends: the class-level list, then the method's additions, de-duplicated.
58
+ * Empty when the contract declares none.
59
+ */
60
+ // webpieces-disable no-function-outside-class -- reflect-metadata reader paired with the decorator
61
+ function getExternalContextKeys(apiClass, methodName) {
62
+ const classKeys = Reflect.getMetadata(CLASS_KEYS, apiClass) ?? [];
63
+ const methodKeys = (Reflect.getMetadata(METHOD_KEYS, apiClass) ?? {})[methodName] ?? [];
64
+ const merged = [];
65
+ for (const key of [...classKeys, ...methodKeys]) {
66
+ if (!merged.includes(key))
67
+ merged.push(key);
68
+ }
69
+ return merged;
70
+ }
71
+ /** True when the class or any of its methods carries `@ExternalContextKeys`. */
72
+ // webpieces-disable no-function-outside-class -- reflect-metadata reader paired with the decorator
73
+ function declaresExternalContextKeys(apiClass) {
74
+ return (Reflect.hasMetadata(CLASS_KEYS, apiClass) ||
75
+ Object.keys(Reflect.getMetadata(METHOD_KEYS, apiClass) ?? {}).length > 0);
76
+ }
77
+ //# sourceMappingURL=ExternalContextKeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExternalContextKeys.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ExternalContextKeys.ts"],"names":[],"mappings":";;AA8BA,kDAwBC;AAOD,wDASC;AAID,kEAKC;AA/ED,4BAA0B;AAG1B,mEAAmE;AACnE,MAAM,UAAU,GAAG,uCAAuC,CAAC;AAC3D,mGAAmG;AACnG,MAAM,WAAW,GAAG,wCAAwC,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,2GAA2G;AAC3G,SAAgB,mBAAmB,CAAC,GAAG,IAAqB;IACxD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACnF,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS,IAAI,GAAG,CAAC,UAAU,KAAK,EAAE,EAAE,CAAC;YACxD,MAAM,IAAI,KAAK,CACX,qCAAqC,GAAG,CAAC,IAAI,oEAAoE,CACpH,CAAC;QACN,CAAC;IACL,CAAC;IACD,kFAAkF;IAClF,OAAO,CAAC,MAAW,EAAE,WAA6B,EAAQ,EAAE;QACxD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,CAAC,cAAc,CAAC,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;YACtD,OAAO;QACX,CAAC;QACD,MAAM,cAAc,GAAG,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC;QAClF,MAAM,GAAG,GAAoC;YACzC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,WAAW,EAAE,cAAc,CAAC,IAAI,EAAE,CAAC;SAC9D,CAAC;QACF,GAAG,CAAC,WAAqB,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;QACvC,OAAO,CAAC,cAAc,CAAC,WAAW,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC;IAC7D,CAAC,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,mGAAmG;AACnG,SAAgB,sBAAsB,CAAC,QAAkB,EAAE,UAAkB;IACzE,MAAM,SAAS,GAAoB,OAAO,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnF,MAAM,UAAU,GACZ,CAAC,OAAO,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;IACzE,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,SAAS,EAAE,GAAG,UAAU,CAAC,EAAE,CAAC;QAC9C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,gFAAgF;AAChF,mGAAmG;AACnG,SAAgB,2BAA2B,CAAC,QAAkB;IAC1D,OAAO,CACH,OAAO,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAC3E,CAAC;AACN,CAAC","sourcesContent":["import 'reflect-metadata';\nimport { AnyContextKey } from '../ContextKey';\n\n/** Class-level keys: every endpoint of the contract sends them. */\nconst CLASS_KEYS = 'webpieces:external-context-keys:class';\n/** Method-level keys, `methodName -> keys`: ADDED to the class-level ones for that method only. */\nconst METHOD_KEYS = 'webpieces:external-context-keys:method';\n\n/**\n * `@ExternalContextKeys(...keys)` — the ONLY request-context keys an `EXTERNAL_OUTBOUND` client sends\n * to the outside system it calls.\n *\n * WHY a per-CONTRACT list and not a flag on the `ContextKey`: a global \"send to vendors\" flag would\n * send Toast's restaurant id to Clover. Which context a vendor may see is a fact about THAT vendor's\n * API, so it is declared on that vendor's contract. Without this, an outbound client sends nothing\n * of ours — not `x-request-id`, not any `x-webpieces-*` key, not an app's untrusted transferred keys —\n * because a vendor is somebody else's process.\n *\n * On the CLASS it applies to every endpoint; on a METHOD it ADDS keys for that method only. A listed\n * key with no value in the request context makes the client throw BEFORE any I/O: the vendor needs it\n * (it is how the call is routed, e.g. a merchant id), and a request without it is a bug in our code.\n * Every listed key must declare an `httpHeader` — that is its wire name.\n *\n * ```ts\n * @ApiPath('')\n * @ExternalContextKeys(WebpiecesCoreHeaders.REQUEST_ID)\n * export abstract class PartnerWebhookApi { ... }\n * ```\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function ExternalContextKeys(...keys: AnyContextKey[]): ClassDecorator & MethodDecorator {\n if (keys.length === 0) {\n throw new Error('@ExternalContextKeys(...) requires at least one ContextKey.');\n }\n for (const key of keys) {\n if (key.httpHeader === undefined || key.httpHeader === '') {\n throw new Error(\n `@ExternalContextKeys: ContextKey '${key.name}' declares no httpHeader, so it has no wire name to send it under.`,\n );\n }\n }\n // webpieces-disable no-any-unknown -- reflect-metadata decorator API requires any\n return (target: any, propertyKey?: string | symbol): void => {\n if (propertyKey === undefined) {\n Reflect.defineMetadata(CLASS_KEYS, [...keys], target);\n return;\n }\n const metadataTarget = typeof target === 'function' ? target : target.constructor;\n const all: Record<string, AnyContextKey[]> = {\n ...(Reflect.getMetadata(METHOD_KEYS, metadataTarget) ?? {}),\n };\n all[propertyKey as string] = [...keys];\n Reflect.defineMetadata(METHOD_KEYS, all, metadataTarget);\n };\n}\n\n/**\n * The keys one method sends: the class-level list, then the method's additions, de-duplicated.\n * Empty when the contract declares none.\n */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader paired with the decorator\nexport function getExternalContextKeys(apiClass: Function, methodName: string): AnyContextKey[] {\n const classKeys: AnyContextKey[] = Reflect.getMetadata(CLASS_KEYS, apiClass) ?? [];\n const methodKeys: AnyContextKey[] =\n (Reflect.getMetadata(METHOD_KEYS, apiClass) ?? {})[methodName] ?? [];\n const merged: AnyContextKey[] = [];\n for (const key of [...classKeys, ...methodKeys]) {\n if (!merged.includes(key)) merged.push(key);\n }\n return merged;\n}\n\n/** True when the class or any of its methods carries `@ExternalContextKeys`. */\n// webpieces-disable no-function-outside-class -- reflect-metadata reader paired with the decorator\nexport function declaresExternalContextKeys(apiClass: Function): boolean {\n return (\n Reflect.hasMetadata(CLASS_KEYS, apiClass) ||\n Object.keys(Reflect.getMetadata(METHOD_KEYS, apiClass) ?? {}).length > 0\n );\n}\n"]}
@@ -1,30 +1,61 @@
1
1
  /** Nominal backing keeps raw string literals out of endpoint declarations. */
2
2
  declare enum HttpMethodValue {
3
3
  GET = "GET",
4
- POST = "POST"
4
+ POST = "POST",
5
+ PUT = "PUT",
6
+ PATCH = "PATCH",
7
+ DELETE = "DELETE"
5
8
  }
6
9
  /** Short, statically importable decorator arguments. Raw strings do not satisfy {@link HttpMethod}. */
7
10
  export declare const GET = HttpMethodValue.GET;
8
11
  export declare const POST = HttpMethodValue.POST;
9
- /** HTTP verbs currently supported by generated Webpieces request/response contracts. */
10
- export type HttpMethod = typeof GET | typeof POST;
12
+ export declare const PUT = HttpMethodValue.PUT;
13
+ export declare const PATCH = HttpMethodValue.PATCH;
14
+ export declare const DELETE = HttpMethodValue.DELETE;
15
+ /** HTTP verbs supported by generated Webpieces request/response contracts. */
16
+ export type HttpMethod = typeof GET | typeof POST | typeof PUT | typeof PATCH | typeof DELETE;
17
+ /** Every verb, in declaration order — the one list validators and messages enumerate. */
18
+ export declare const HTTP_METHODS: readonly HttpMethod[];
19
+ /**
20
+ * The ONE rule for "may this verb carry a request body": POST, PUT and PATCH do; GET and DELETE do
21
+ * not. Every client serializer, route validator, scanner and document renderer asks this, instead of
22
+ * each re-deciding it with its own `=== 'POST'` check (which is how a sixth copy drifts).
23
+ */
24
+ export declare function verbCarriesBody(method: string): boolean;
25
+ /** Runtime backstop for JavaScript and `as any` callers of `@Endpoint`. */
26
+ export declare function isHttpMethod(value: unknown): value is HttpMethod;
11
27
  /** Whether a generated client returns the response body or the whole transport-neutral response. */
12
28
  export type EndpointResponseType = 'body' | 'full';
13
- /** The wire source for one explicitly mapped scalar/list method parameter. */
14
- export type HttpParameterSource = 'path' | 'query';
29
+ /**
30
+ * The wire source for one explicitly mapped method parameter: a `{placeholder}`, one named query key,
31
+ * or (`@QueryString()`, EXTERNAL_OUTBOUND contracts only) the whole query string as a {@link QueryParams}.
32
+ */
33
+ export type HttpParameterSource = 'path' | 'query' | 'query-map';
34
+ /**
35
+ * The ITEM type of a list query parameter, declared as `@QueryParam('ids', Number)`: TypeScript's
36
+ * design metadata erases `number[]` to `Array`, so without it every item arrives as a string.
37
+ */
38
+ export type HttpQueryItemType = typeof String | typeof Number | typeof Boolean;
15
39
  /** Runtime conversion supported for path/query values after TypeScript design metadata is read. */
16
40
  export type HttpParameterValueType = 'string' | 'number' | 'boolean' | 'array' | 'unknown';
17
41
  /** Metadata written by `@PathParam` / `@QueryParam` before route construction knows the value type. */
18
42
  export declare class HttpParameterDeclaration {
19
43
  readonly index: number;
20
44
  readonly source: HttpParameterSource;
45
+ /** The wire name; `''` for a `query-map`, which binds every key. */
21
46
  readonly wireName: string;
22
- constructor(index: number, source: HttpParameterSource, wireName: string);
47
+ /** `@QueryParam('ids', Number)`: the item type of a list query parameter. */
48
+ readonly itemType?: HttpQueryItemType | undefined;
49
+ constructor(index: number, source: HttpParameterSource,
50
+ /** The wire name; `''` for a `query-map`, which binds every key. */
51
+ wireName: string,
52
+ /** `@QueryParam('ids', Number)`: the item type of a list query parameter. */
53
+ itemType?: HttpQueryItemType | undefined);
23
54
  }
24
55
  /** Fully resolved parameter metadata carried by every server and client route. */
25
56
  export declare class HttpParameterBinding extends HttpParameterDeclaration {
26
57
  readonly valueType: HttpParameterValueType;
27
- constructor(index: number, source: HttpParameterSource, wireName: string, valueType: HttpParameterValueType);
58
+ constructor(index: number, source: HttpParameterSource, wireName: string, valueType: HttpParameterValueType, itemType?: HttpQueryItemType);
28
59
  }
29
60
  /** Result of mapping one API method call to its HTTP path/query/body. */
30
61
  export declare class BoundHttpRequest {
@@ -41,8 +72,15 @@ export declare class BoundHttpRequest {
41
72
  export declare class HttpContractMapper {
42
73
  /** API arguments -> encoded relative URL + body. */
43
74
  static toWire(pathTemplate: string, bindings: readonly HttpParameterBinding[], bodyParameterIndex: number | undefined, args: readonly unknown[]): BoundHttpRequest;
75
+ /** One `@QueryString()` argument -> repeated `key=value` pairs, in insertion order. */
76
+ private static appendQueryMap;
44
77
  /** Decoded path/query/body -> API arguments in declaration order. */
45
78
  static fromWire(bindings: readonly HttpParameterBinding[], bodyParameterIndex: number | undefined, body: unknown, pathValues: ReadonlyMap<string, string | readonly string[]>, queryValues: ReadonlyMap<string, string | readonly string[]>): unknown[];
79
+ /** Every decoded query key -> a {@link QueryParams}, the inverse of {@link appendQueryMap}. */
80
+ private static queryMapOf;
46
81
  private static convert;
82
+ /** A list query parameter's items, as its declared item type (`@QueryParam('ids', Number)`). */
83
+ private static convertItems;
84
+ private static convertScalar;
47
85
  }
48
86
  export {};
@@ -1,33 +1,64 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HttpContractMapper = exports.BoundHttpRequest = exports.HttpParameterBinding = exports.HttpParameterDeclaration = exports.POST = exports.GET = void 0;
3
+ exports.HttpContractMapper = exports.BoundHttpRequest = exports.HttpParameterBinding = exports.HttpParameterDeclaration = exports.HTTP_METHODS = exports.DELETE = exports.PATCH = exports.PUT = exports.POST = exports.GET = void 0;
4
+ exports.verbCarriesBody = verbCarriesBody;
5
+ exports.isHttpMethod = isHttpMethod;
4
6
  const ApiError_1 = require("../errors/ApiError");
7
+ const QueryParams_1 = require("./QueryParams");
5
8
  /** Nominal backing keeps raw string literals out of endpoint declarations. */
6
9
  var HttpMethodValue;
7
10
  (function (HttpMethodValue) {
8
11
  HttpMethodValue["GET"] = "GET";
9
12
  HttpMethodValue["POST"] = "POST";
13
+ HttpMethodValue["PUT"] = "PUT";
14
+ HttpMethodValue["PATCH"] = "PATCH";
15
+ HttpMethodValue["DELETE"] = "DELETE";
10
16
  })(HttpMethodValue || (HttpMethodValue = {}));
11
17
  /** Short, statically importable decorator arguments. Raw strings do not satisfy {@link HttpMethod}. */
12
18
  exports.GET = HttpMethodValue.GET;
13
19
  exports.POST = HttpMethodValue.POST;
20
+ exports.PUT = HttpMethodValue.PUT;
21
+ exports.PATCH = HttpMethodValue.PATCH;
22
+ exports.DELETE = HttpMethodValue.DELETE;
23
+ /** Every verb, in declaration order — the one list validators and messages enumerate. */
24
+ exports.HTTP_METHODS = [exports.GET, exports.POST, exports.PUT, exports.PATCH, exports.DELETE];
25
+ /**
26
+ * The ONE rule for "may this verb carry a request body": POST, PUT and PATCH do; GET and DELETE do
27
+ * not. Every client serializer, route validator, scanner and document renderer asks this, instead of
28
+ * each re-deciding it with its own `=== 'POST'` check (which is how a sixth copy drifts).
29
+ */
30
+ // webpieces-disable no-function-outside-class -- pure predicate shared by DI-free browser and server runtimes
31
+ function verbCarriesBody(method) {
32
+ return method === exports.POST || method === exports.PUT || method === exports.PATCH;
33
+ }
34
+ /** Runtime backstop for JavaScript and `as any` callers of `@Endpoint`. */
35
+ // webpieces-disable no-function-outside-class -- pure guard shared by decorators and readers; webpieces-disable no-any-unknown -- untrusted runtime value is narrowed to an HTTP method
36
+ function isHttpMethod(value) {
37
+ return exports.HTTP_METHODS.includes(value);
38
+ }
14
39
  /** Metadata written by `@PathParam` / `@QueryParam` before route construction knows the value type. */
15
40
  class HttpParameterDeclaration {
16
41
  index;
17
42
  source;
18
43
  wireName;
19
- constructor(index, source, wireName) {
44
+ itemType;
45
+ constructor(index, source,
46
+ /** The wire name; `''` for a `query-map`, which binds every key. */
47
+ wireName,
48
+ /** `@QueryParam('ids', Number)`: the item type of a list query parameter. */
49
+ itemType) {
20
50
  this.index = index;
21
51
  this.source = source;
22
52
  this.wireName = wireName;
53
+ this.itemType = itemType;
23
54
  }
24
55
  }
25
56
  exports.HttpParameterDeclaration = HttpParameterDeclaration;
26
57
  /** Fully resolved parameter metadata carried by every server and client route. */
27
58
  class HttpParameterBinding extends HttpParameterDeclaration {
28
59
  valueType;
29
- constructor(index, source, wireName, valueType) {
30
- super(index, source, wireName);
60
+ constructor(index, source, wireName, valueType, itemType) {
61
+ super(index, source, wireName, itemType);
31
62
  this.valueType = valueType;
32
63
  }
33
64
  }
@@ -70,6 +101,10 @@ class HttpContractMapper {
70
101
  }
71
102
  if (value === undefined || value === null)
72
103
  continue;
104
+ if (binding.source === 'query-map') {
105
+ this.appendQueryMap(binding, value, query);
106
+ continue;
107
+ }
73
108
  const values = Array.isArray(value) ? value : [value];
74
109
  for (const item of values) {
75
110
  if (item === undefined || item === null)
@@ -83,6 +118,18 @@ class HttpContractMapper {
83
118
  const suffix = query.length === 0 ? '' : `${path.includes('?') ? '&' : '?'}${query.join('&')}`;
84
119
  return new BoundHttpRequest(`${path}${suffix}`, bodyParameterIndex === undefined ? undefined : args[bodyParameterIndex]);
85
120
  }
121
+ /** One `@QueryString()` argument -> repeated `key=value` pairs, in insertion order. */
122
+ // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper; webpieces-disable no-any-unknown -- the argument's type is erased at the generic boundary
123
+ static appendQueryMap(binding, value, query) {
124
+ if (!(value instanceof QueryParams_1.QueryParams)) {
125
+ throw new ApiError_1.ApiBadRequestError(`Argument ${binding.index} is @QueryString() and must be a QueryParams, received ${typeof value}.`);
126
+ }
127
+ for (const key of value.keys()) {
128
+ for (const item of value.getAll(key)) {
129
+ query.push(`${encodeURIComponent(key)}=${encodeURIComponent(item)}`);
130
+ }
131
+ }
132
+ }
86
133
  /** Decoded path/query/body -> API arguments in declaration order. */
87
134
  // webpieces-disable no-function-outside-class -- pure inverse mapper paired with toWire
88
135
  static fromWire(bindings, bodyParameterIndex,
@@ -95,6 +142,10 @@ class HttpContractMapper {
95
142
  if (bodyParameterIndex !== undefined)
96
143
  args[bodyParameterIndex] = body;
97
144
  for (const binding of bindings) {
145
+ if (binding.source === 'query-map') {
146
+ args[binding.index] = this.queryMapOf(queryValues);
147
+ continue;
148
+ }
98
149
  const values = binding.source === 'path' ? pathValues : queryValues;
99
150
  const raw = values.get(binding.wireName);
100
151
  if (raw === undefined) {
@@ -108,18 +159,45 @@ class HttpContractMapper {
108
159
  }
109
160
  return args;
110
161
  }
162
+ /** Every decoded query key -> a {@link QueryParams}, the inverse of {@link appendQueryMap}. */
163
+ // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper
164
+ static queryMapOf(queryValues) {
165
+ const params = new QueryParams_1.QueryParams();
166
+ for (const entry of queryValues.entries()) {
167
+ const raw = entry[1];
168
+ for (const item of Array.isArray(raw) ? raw : [raw])
169
+ params.add(entry[0], item);
170
+ }
171
+ return params;
172
+ }
111
173
  // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper
112
174
  static convert(binding, raw) {
113
175
  const values = Array.isArray(raw) ? raw : [raw];
114
176
  if (binding.valueType === 'array')
115
- return [...values];
177
+ return this.convertItems(binding, values);
116
178
  if (values.length !== 1) {
117
179
  throw new ApiError_1.ApiBadRequestError(`Parameter '${binding.wireName}' was repeated but is not declared as an array.`);
118
180
  }
119
181
  const value = values[0];
120
- switch (binding.valueType) {
182
+ return this.convertScalar(binding, value);
183
+ }
184
+ /** A list query parameter's items, as its declared item type (`@QueryParam('ids', Number)`). */
185
+ // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper
186
+ static convertItems(binding, values) {
187
+ if (binding.itemType === Number) {
188
+ return values.map((item) => this.convertScalar(binding, item, 'number'));
189
+ }
190
+ if (binding.itemType === Boolean) {
191
+ return values.map((item) => this.convertScalar(binding, item, 'boolean'));
192
+ }
193
+ return [...values];
194
+ }
195
+ // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper
196
+ static convertScalar(binding, value, valueType = binding.valueType) {
197
+ switch (valueType) {
121
198
  case 'string':
122
199
  case 'unknown':
200
+ case 'array':
123
201
  return value;
124
202
  case 'number': {
125
203
  if (value.trim() === '') {
@@ -1 +1 @@
1
- {"version":3,"file":"HttpContract.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpContract.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiBadRequestError } from '../errors/ApiError';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum HttpMethodValue {\n GET = 'GET',\n POST = 'POST',\n}\n\n/** Short, statically importable decorator arguments. Raw strings do not satisfy {@link HttpMethod}. */\nexport const GET = HttpMethodValue.GET;\nexport const POST = HttpMethodValue.POST;\n\n/** HTTP verbs currently supported by generated Webpieces request/response contracts. */\nexport type HttpMethod = typeof GET | typeof POST;\n\n/** Whether a generated client returns the response body or the whole transport-neutral response. */\nexport type EndpointResponseType = 'body' | 'full';\n\n/** The wire source for one explicitly mapped scalar/list method parameter. */\nexport type HttpParameterSource = 'path' | 'query';\n\n/** Runtime conversion supported for path/query values after TypeScript design metadata is read. */\nexport type HttpParameterValueType = 'string' | 'number' | 'boolean' | 'array' | 'unknown';\n\n/** Concrete runtime values produced for path/query parameters. */\ntype ConvertedHttpParameter = string | number | boolean | readonly string[];\n\n/** Metadata written by `@PathParam` / `@QueryParam` before route construction knows the value type. */\nexport class HttpParameterDeclaration {\n constructor(\n public readonly index: number,\n public readonly source: HttpParameterSource,\n public readonly wireName: string,\n ) {}\n}\n\n/** Fully resolved parameter metadata carried by every server and client route. */\nexport class HttpParameterBinding extends HttpParameterDeclaration {\n constructor(\n index: number,\n source: HttpParameterSource,\n wireName: string,\n public readonly valueType: HttpParameterValueType,\n ) {\n super(index, source, wireName);\n }\n}\n\n/** Result of mapping one API method call to its HTTP path/query/body. */\nexport class BoundHttpRequest {\n constructor(\n public readonly path: string,\n // webpieces-disable no-any-unknown -- the contract owns the request body type\n public readonly body: unknown,\n ) {}\n}\n\n/**\n * The single path/query/body mapper shared by generated clients and the incoming server adapter.\n * Java Webpieces' `HttpsJsonClientInvokeHandler` is the reference model: explicit wire names,\n * encoded placeholders, omitted null query values, and repeated list keys. This port deliberately\n * fixes Java's list-query overwrite bug and never depends on compiled JavaScript parameter names.\n */\nexport class HttpContractMapper {\n /** API arguments -> encoded relative URL + body. */\n // webpieces-disable no-function-outside-class -- pure cross-runtime mapper used by DI-free browser clients and server adapters\n static toWire(\n pathTemplate: string,\n bindings: readonly HttpParameterBinding[],\n bodyParameterIndex: number | undefined,\n // webpieces-disable no-any-unknown -- arbitrary API method parameters cross this generic boundary\n args: readonly unknown[],\n ): BoundHttpRequest {\n let path = pathTemplate;\n const query: string[] = [];\n\n for (const binding of bindings) {\n const value = args[binding.index];\n if (binding.source === 'path') {\n if (value === undefined || value === null) {\n throw new ApiBadRequestError(\n `Missing path parameter '${binding.wireName}' at argument ${binding.index}.`,\n );\n }\n const token = `{${binding.wireName}}`;\n path = path.split(token).join(encodeURIComponent(String(value)));\n continue;\n }\n\n if (value === undefined || value === null) continue;\n const values = Array.isArray(value) ? value : [value];\n for (const item of values) {\n if (item === undefined || item === null) continue;\n query.push(\n `${encodeURIComponent(binding.wireName)}=${encodeURIComponent(String(item))}`,\n );\n }\n }\n\n if (/\\{[^{}]+\\}/.test(path)) {\n throw new ApiBadRequestError(\n `Not every path placeholder was supplied for '${pathTemplate}'.`,\n );\n }\n const suffix =\n query.length === 0 ? '' : `${path.includes('?') ? '&' : '?'}${query.join('&')}`;\n return new BoundHttpRequest(\n `${path}${suffix}`,\n bodyParameterIndex === undefined ? undefined : args[bodyParameterIndex],\n );\n }\n\n /** Decoded path/query/body -> API arguments in declaration order. */\n // webpieces-disable no-function-outside-class -- pure inverse mapper paired with toWire\n static fromWire(\n bindings: readonly HttpParameterBinding[],\n bodyParameterIndex: number | undefined,\n // webpieces-disable no-any-unknown -- the contract owns the request body type\n body: unknown,\n pathValues: ReadonlyMap<string, string | readonly string[]>,\n queryValues: ReadonlyMap<string, string | readonly string[]>,\n ): unknown[] {\n const highestBinding = bindings.reduce(\n (highest: number, binding: HttpParameterBinding) => Math.max(highest, binding.index),\n -1,\n );\n const argCount = Math.max(highestBinding, bodyParameterIndex ?? -1) + 1;\n // webpieces-disable no-any-unknown -- API argument types are erased at the shared wire boundary\n const args = new Array<unknown>(argCount);\n if (bodyParameterIndex !== undefined) args[bodyParameterIndex] = body;\n\n for (const binding of bindings) {\n const values = binding.source === 'path' ? pathValues : queryValues;\n const raw = values.get(binding.wireName);\n if (raw === undefined) {\n if (binding.source === 'path') {\n throw new ApiBadRequestError(`Missing path parameter '${binding.wireName}'.`);\n }\n args[binding.index] = undefined;\n continue;\n }\n args[binding.index] = this.convert(binding, raw);\n }\n return args;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper\n private static convert(\n binding: HttpParameterBinding,\n raw: string | readonly string[],\n ): ConvertedHttpParameter {\n const values = Array.isArray(raw) ? raw : [raw];\n if (binding.valueType === 'array') return [...values];\n if (values.length !== 1) {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' was repeated but is not declared as an array.`,\n );\n }\n const value = values[0];\n switch (binding.valueType) {\n case 'string':\n case 'unknown':\n return value;\n case 'number': {\n if (value.trim() === '') {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be a number.`,\n );\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be a number.`,\n );\n }\n return parsed;\n }\n case 'boolean':\n if (value === 'true') return true;\n if (value === 'false') return false;\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be 'true' or 'false'.`,\n );\n }\n }\n}\n"]}
1
+ {"version":3,"file":"HttpContract.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpContract.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiBadRequestError } from '../errors/ApiError';\nimport { QueryParams } from './QueryParams';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum HttpMethodValue {\n GET = 'GET',\n POST = 'POST',\n PUT = 'PUT',\n PATCH = 'PATCH',\n DELETE = 'DELETE',\n}\n\n/** Short, statically importable decorator arguments. Raw strings do not satisfy {@link HttpMethod}. */\nexport const GET = HttpMethodValue.GET;\nexport const POST = HttpMethodValue.POST;\nexport const PUT = HttpMethodValue.PUT;\nexport const PATCH = HttpMethodValue.PATCH;\nexport const DELETE = HttpMethodValue.DELETE;\n\n/** HTTP verbs supported by generated Webpieces request/response contracts. */\nexport type HttpMethod = typeof GET | typeof POST | typeof PUT | typeof PATCH | typeof DELETE;\n\n/** Every verb, in declaration order — the one list validators and messages enumerate. */\nexport const HTTP_METHODS: readonly HttpMethod[] = [GET, POST, PUT, PATCH, DELETE];\n\n/**\n * The ONE rule for \"may this verb carry a request body\": POST, PUT and PATCH do; GET and DELETE do\n * not. Every client serializer, route validator, scanner and document renderer asks this, instead of\n * each re-deciding it with its own `=== 'POST'` check (which is how a sixth copy drifts).\n */\n// webpieces-disable no-function-outside-class -- pure predicate shared by DI-free browser and server runtimes\nexport function verbCarriesBody(method: string): boolean {\n return method === POST || method === PUT || method === PATCH;\n}\n\n/** Runtime backstop for JavaScript and `as any` callers of `@Endpoint`. */\n// webpieces-disable no-function-outside-class -- pure guard shared by decorators and readers; webpieces-disable no-any-unknown -- untrusted runtime value is narrowed to an HTTP method\nexport function isHttpMethod(value: unknown): value is HttpMethod {\n return (HTTP_METHODS as readonly unknown[]).includes(value);\n}\n\n/** Whether a generated client returns the response body or the whole transport-neutral response. */\nexport type EndpointResponseType = 'body' | 'full';\n\n/**\n * The wire source for one explicitly mapped method parameter: a `{placeholder}`, one named query key,\n * or (`@QueryString()`, EXTERNAL_OUTBOUND contracts only) the whole query string as a {@link QueryParams}.\n */\nexport type HttpParameterSource = 'path' | 'query' | 'query-map';\n\n/**\n * The ITEM type of a list query parameter, declared as `@QueryParam('ids', Number)`: TypeScript's\n * design metadata erases `number[]` to `Array`, so without it every item arrives as a string.\n */\nexport type HttpQueryItemType = typeof String | typeof Number | typeof Boolean;\n\n/** Runtime conversion supported for path/query values after TypeScript design metadata is read. */\nexport type HttpParameterValueType = 'string' | 'number' | 'boolean' | 'array' | 'unknown';\n\n/** Concrete runtime values produced for path/query parameters. */\ntype ConvertedHttpParameter =\n | string\n | number\n | boolean\n | readonly string[]\n | readonly number[]\n | readonly boolean[]\n | QueryParams;\n\n/** Metadata written by `@PathParam` / `@QueryParam` before route construction knows the value type. */\nexport class HttpParameterDeclaration {\n constructor(\n public readonly index: number,\n public readonly source: HttpParameterSource,\n /** The wire name; `''` for a `query-map`, which binds every key. */\n public readonly wireName: string,\n /** `@QueryParam('ids', Number)`: the item type of a list query parameter. */\n public readonly itemType?: HttpQueryItemType,\n ) {}\n}\n\n/** Fully resolved parameter metadata carried by every server and client route. */\nexport class HttpParameterBinding extends HttpParameterDeclaration {\n constructor(\n index: number,\n source: HttpParameterSource,\n wireName: string,\n public readonly valueType: HttpParameterValueType,\n itemType?: HttpQueryItemType,\n ) {\n super(index, source, wireName, itemType);\n }\n}\n\n/** Result of mapping one API method call to its HTTP path/query/body. */\nexport class BoundHttpRequest {\n constructor(\n public readonly path: string,\n // webpieces-disable no-any-unknown -- the contract owns the request body type\n public readonly body: unknown,\n ) {}\n}\n\n/**\n * The single path/query/body mapper shared by generated clients and the incoming server adapter.\n * Java Webpieces' `HttpsJsonClientInvokeHandler` is the reference model: explicit wire names,\n * encoded placeholders, omitted null query values, and repeated list keys. This port deliberately\n * fixes Java's list-query overwrite bug and never depends on compiled JavaScript parameter names.\n */\nexport class HttpContractMapper {\n /** API arguments -> encoded relative URL + body. */\n // webpieces-disable no-function-outside-class -- pure cross-runtime mapper used by DI-free browser clients and server adapters\n static toWire(\n pathTemplate: string,\n bindings: readonly HttpParameterBinding[],\n bodyParameterIndex: number | undefined,\n // webpieces-disable no-any-unknown -- arbitrary API method parameters cross this generic boundary\n args: readonly unknown[],\n ): BoundHttpRequest {\n let path = pathTemplate;\n const query: string[] = [];\n\n for (const binding of bindings) {\n const value = args[binding.index];\n if (binding.source === 'path') {\n if (value === undefined || value === null) {\n throw new ApiBadRequestError(\n `Missing path parameter '${binding.wireName}' at argument ${binding.index}.`,\n );\n }\n const token = `{${binding.wireName}}`;\n path = path.split(token).join(encodeURIComponent(String(value)));\n continue;\n }\n\n if (value === undefined || value === null) continue;\n if (binding.source === 'query-map') {\n this.appendQueryMap(binding, value, query);\n continue;\n }\n const values = Array.isArray(value) ? value : [value];\n for (const item of values) {\n if (item === undefined || item === null) continue;\n query.push(\n `${encodeURIComponent(binding.wireName)}=${encodeURIComponent(String(item))}`,\n );\n }\n }\n\n if (/\\{[^{}]+\\}/.test(path)) {\n throw new ApiBadRequestError(\n `Not every path placeholder was supplied for '${pathTemplate}'.`,\n );\n }\n const suffix =\n query.length === 0 ? '' : `${path.includes('?') ? '&' : '?'}${query.join('&')}`;\n return new BoundHttpRequest(\n `${path}${suffix}`,\n bodyParameterIndex === undefined ? undefined : args[bodyParameterIndex],\n );\n }\n\n /** One `@QueryString()` argument -> repeated `key=value` pairs, in insertion order. */\n // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper; webpieces-disable no-any-unknown -- the argument's type is erased at the generic boundary\n private static appendQueryMap(binding: HttpParameterBinding, value: unknown, query: string[]): void {\n if (!(value instanceof QueryParams)) {\n throw new ApiBadRequestError(\n `Argument ${binding.index} is @QueryString() and must be a QueryParams, received ${typeof value}.`,\n );\n }\n for (const key of value.keys()) {\n for (const item of value.getAll(key)) {\n query.push(`${encodeURIComponent(key)}=${encodeURIComponent(item)}`);\n }\n }\n }\n\n /** Decoded path/query/body -> API arguments in declaration order. */\n // webpieces-disable no-function-outside-class -- pure inverse mapper paired with toWire\n static fromWire(\n bindings: readonly HttpParameterBinding[],\n bodyParameterIndex: number | undefined,\n // webpieces-disable no-any-unknown -- the contract owns the request body type\n body: unknown,\n pathValues: ReadonlyMap<string, string | readonly string[]>,\n queryValues: ReadonlyMap<string, string | readonly string[]>,\n ): unknown[] {\n const highestBinding = bindings.reduce(\n (highest: number, binding: HttpParameterBinding) => Math.max(highest, binding.index),\n -1,\n );\n const argCount = Math.max(highestBinding, bodyParameterIndex ?? -1) + 1;\n // webpieces-disable no-any-unknown -- API argument types are erased at the shared wire boundary\n const args = new Array<unknown>(argCount);\n if (bodyParameterIndex !== undefined) args[bodyParameterIndex] = body;\n\n for (const binding of bindings) {\n if (binding.source === 'query-map') {\n args[binding.index] = this.queryMapOf(queryValues);\n continue;\n }\n const values = binding.source === 'path' ? pathValues : queryValues;\n const raw = values.get(binding.wireName);\n if (raw === undefined) {\n if (binding.source === 'path') {\n throw new ApiBadRequestError(`Missing path parameter '${binding.wireName}'.`);\n }\n args[binding.index] = undefined;\n continue;\n }\n args[binding.index] = this.convert(binding, raw);\n }\n return args;\n }\n\n /** Every decoded query key -> a {@link QueryParams}, the inverse of {@link appendQueryMap}. */\n // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper\n private static queryMapOf(queryValues: ReadonlyMap<string, string | readonly string[]>): QueryParams {\n const params = new QueryParams();\n for (const entry of queryValues.entries()) {\n const raw = entry[1];\n for (const item of Array.isArray(raw) ? raw : [raw]) params.add(entry[0], item);\n }\n return params;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper\n private static convert(\n binding: HttpParameterBinding,\n raw: string | readonly string[],\n ): ConvertedHttpParameter {\n const values = Array.isArray(raw) ? raw : [raw];\n if (binding.valueType === 'array') return this.convertItems(binding, values);\n if (values.length !== 1) {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' was repeated but is not declared as an array.`,\n );\n }\n const value = values[0];\n return this.convertScalar(binding, value);\n }\n\n /** A list query parameter's items, as its declared item type (`@QueryParam('ids', Number)`). */\n // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper\n private static convertItems(\n binding: HttpParameterBinding,\n values: readonly string[],\n ): readonly string[] | readonly number[] | readonly boolean[] {\n if (binding.itemType === Number) {\n return values.map((item: string) => this.convertScalar(binding, item, 'number') as number);\n }\n if (binding.itemType === Boolean) {\n return values.map((item: string) => this.convertScalar(binding, item, 'boolean') as boolean);\n }\n return [...values];\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper\n private static convertScalar(\n binding: HttpParameterBinding,\n value: string,\n valueType: HttpParameterValueType = binding.valueType,\n ): string | number | boolean {\n switch (valueType) {\n case 'string':\n case 'unknown':\n case 'array':\n return value;\n case 'number': {\n if (value.trim() === '') {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be a number.`,\n );\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be a number.`,\n );\n }\n return parsed;\n }\n case 'boolean':\n if (value === 'true') return true;\n if (value === 'false') return false;\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be 'true' or 'false'.`,\n );\n }\n }\n}\n"]}
@@ -12,20 +12,39 @@ export declare const WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;
12
12
  export declare const WRITE = EndpointOperationValue.WRITE;
13
13
  /** The side-effect contract callers use to decide whether repeating an operation is safe. */
14
14
  export type EndpointOperation = typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE;
15
+ /** Nominal backing for {@link EndpointOptions.responseBody}. */
16
+ declare enum ResponseBodyValue {
17
+ NONE = "none"
18
+ }
19
+ /** `@Endpoint(..., { responseBody: NONE })` — the success response carries 0 bytes. */
20
+ export declare const NONE = ResponseBodyValue.NONE;
21
+ /** The one response-body declaration an endpoint can make; absent means a JSON body. */
22
+ export type ResponseBodyKind = typeof NONE;
15
23
  /** Options for one `@Endpoint`, stored parallel to the method-to-path metadata. */
16
24
  export interface EndpointOptions {
17
25
  /** Return only a JSON body (default), or the full transport-neutral status/header/body value. */
18
26
  responseType?: EndpointResponseType;
27
+ /**
28
+ * `NONE`: this method returns `Promise<void>` and its success response is 0 BYTES. Our server
29
+ * answers `204 No Content` with no body and no Content-Type, and every client (node, browser,
30
+ * in-process) resolves `undefined` — while still ACCEPTING `{}`, an empty `200` or a `204`,
31
+ * because a vendor whose spec documents no body may still send one.
32
+ *
33
+ * WHY a declaration: `design:returntype` of an async method is just `Promise`, so the runtime
34
+ * cannot see `void`. The architecture scan checks this option agrees with the declared
35
+ * `Promise<void>` in both directions, so it cannot drift from the signature.
36
+ */
37
+ responseBody?: ResponseBodyKind;
19
38
  /**
20
39
  * Parse the request body as application/x-www-form-urlencoded instead of JSON. The request
21
40
  * DTO must be flat; urlencoded has no nesting. Default false = JSON.
22
41
  */
23
42
  formPost?: boolean;
24
43
  /**
25
- * Retain the verbatim request bytes and absolute URL for a `webhook(...)` callback to verify.
44
+ * Retain the verbatim request bytes and absolute URL for a `vendorAuth(...)` hook to verify.
26
45
  * This is retention, not new buffering: the Express adapter already accumulates the body.
27
46
  *
28
- * `webhook(...)` requires this option at wiring time. It combines with `formPost` for vendors
47
+ * A served `vendorAuth(...)` endpoint requires this option at wiring time. It combines with `formPost` for vendors
29
48
  * that sign flat form bodies, e.g. `{ formPost: true, rawBody: true }`.
30
49
  */
31
50
  rawBody?: boolean;
@@ -134,7 +153,7 @@ export interface EndpointOptions {
134
153
  */
135
154
  openWorld?: boolean;
136
155
  }
137
- /** An external endpoint must also identify the outside system that calls it. */
156
+ /** An EXTERNAL_INBOUND endpoint must also identify the outside system that calls it. */
138
157
  export interface ExternalEndpointOptions extends EndpointOptions {
139
158
  /** The outside system (`'twilio'`) — graph identity, not display text. */
140
159
  calledBy: string;