@webpieces/core-util 0.4.957 → 0.4.959
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/http/ApiCallInfo.d.ts +71 -11
- package/src/http/ApiCallInfo.js +81 -7
- package/src/http/ApiCallInfo.js.map +1 -1
- package/src/http/ContextMgr.js +12 -2
- package/src/http/ContextMgr.js.map +1 -1
- package/src/http/DeletedAuthCompileAssertions.js +3 -1
- package/src/http/DeletedAuthCompileAssertions.js.map +1 -1
- package/src/http/DestinationTrust.d.ts +33 -4
- package/src/http/DestinationTrust.js +54 -11
- package/src/http/DestinationTrust.js.map +1 -1
- package/src/http/EndpointCompileAssertions.js +12 -6
- package/src/http/EndpointCompileAssertions.js.map +1 -1
- package/src/http/ExternalContextKeys.d.ts +31 -0
- package/src/http/ExternalContextKeys.js +77 -0
- package/src/http/ExternalContextKeys.js.map +1 -0
- package/src/http/HttpContract.d.ts +45 -7
- package/src/http/HttpContract.js +84 -6
- package/src/http/HttpContract.js.map +1 -1
- package/src/http/HttpEndpointOptions.d.ts +22 -3
- package/src/http/HttpEndpointOptions.js +8 -1
- package/src/http/HttpEndpointOptions.js.map +1 -1
- package/src/http/LogApiCall.d.ts +4 -1
- package/src/http/LogApiCall.js +7 -5
- package/src/http/LogApiCall.js.map +1 -1
- package/src/http/QueryParams.d.ts +47 -0
- package/src/http/QueryParams.js +89 -0
- package/src/http/QueryParams.js.map +1 -0
- package/src/http/RouteMetadata.d.ts +16 -2
- package/src/http/RouteMetadata.js +19 -3
- package/src/http/RouteMetadata.js.map +1 -1
- package/src/http/RouteMetadataFactory.d.ts +25 -0
- package/src/http/RouteMetadataFactory.js +97 -12
- package/src/http/RouteMetadataFactory.js.map +1 -1
- package/src/http/WpAuthApiKeyCompileAssertions.js +2 -2
- package/src/http/WpAuthApiKeyCompileAssertions.js.map +1 -1
- package/src/http/api-kind.d.ts +3 -2
- package/src/http/api-kind.js +5 -4
- package/src/http/api-kind.js.map +1 -1
- package/src/http/auth-mode.d.ts +8 -2
- package/src/http/auth-mode.js +11 -5
- package/src/http/auth-mode.js.map +1 -1
- package/src/http/decorators.d.ts +39 -21
- package/src/http/decorators.js +69 -37
- package/src/http/decorators.js.map +1 -1
- package/src/http/external-caller.d.ts +12 -17
- package/src/http/external-caller.js +12 -17
- package/src/http/external-caller.js.map +1 -1
- package/src/http/http-parameter-decorators.d.ts +17 -3
- package/src/http/http-parameter-decorators.js +27 -6
- package/src/http/http-parameter-decorators.js.map +1 -1
- package/src/index.d.ts +11 -7
- package/src/index.js +30 -12
- package/src/index.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DestinationTrust.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DestinationTrust.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAa,gBAAgB;IAeY;IAdrC;;;;OAIG;IACK,MAAM,CAAU,eAAe,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAErE;;;;OAIG;IACK,MAAM,CAAU,oBAAoB,GAAG,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAE3E,YAAqC,cAAuB;QAAvB,mBAAc,GAAd,cAAc,CAAS;IAAG,CAAC;IAEhE;;;;OAIG;IACH,qSAAqS;IACrS,MAAM,CAAC,WAAW,CAAC,IAA0B;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACjD,CAAC;QACD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe;gBAChB,OAAO,gBAAgB,CAAC,eAAe,CAAC;YAC5C,KAAK,KAAK,CAAC;YACX,KAAK,QAAQ,CAAC;YACd,+EAA+E;YAC/E,oFAAoF;YACpF,mFAAmF;YACnF,yFAAyF;YACzF,mDAAmD;YACnD,KAAK,SAAS,CAAC;YACf,qFAAqF;YACrF,uFAAuF;YACvF,uFAAuF;YACvF,4FAA4F;YAC5F,iCAAiC;YACjC,KAAK,QAAQ;gBACb,4EAA4E;gBAC5E,uFAAuF;gBACvF,wFAAwF;gBACxF,iFAAiF;gBAC7E,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACrD,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,GAAkB;QACrB,OAAO,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;;AAzEL,4CA0EC","sourcesContent":["import { AnyContextKey } from '../ContextKey';\nimport { AuthMode } from './auth-mode';\n\n/**\n * DestinationTrust - the OUTBOUND half of the trust model: may a TRUSTED context key\n * ({@link ContextKey.trusted}) ride to the endpoint we are about to call?\n *\n * ## Why the client has to answer this at all\n *\n * The server already decided (see `PendingWireTrust`): an inbound `x-user-id` is admitted only on a\n * route that verified WHO called it — `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)`. On a `jwt()` or `@WpAuthPublic`\n * route the same header must match what the authenticator independently derived, or the request is\n * REJECTED with a 401.\n *\n * That rule is correct, and it means a client that ships `x-user-id` to a `@WpAuthPublic` endpoint is\n * building a request the callee is obliged to reject. Before this class the outbound builders\n * forwarded EVERY transferred key with no idea what the destination was, so an internal service\n * calling another service's public or JWT endpoint 401'd itself. The fix belongs on the producing\n * side: don't send what cannot possibly be believed.\n *\n * ## Why it is a class with a private constructor and no boolean parameter\n *\n * `buildOutboundHeaders(sendTrusted = true)` would have been three characters of work and exactly the\n * \"widening that is an ABSENCE rather than a token\" `.claude/rules/no-backwards-compat.md` rejects — the\n * permissive answer would be\n * what you get by not typing anything. There is no way to build a DestinationTrust except from the\n * destination endpoint's own {@link AuthMode}, so the caller cannot assert a posture the route does\n * not actually have, and `grep -rn DestinationTrust.forAuthMode` lists every place the question is\n * asked. The two instances are PRIVATE for the same reason: exposing them would be a second spelling\n * that skips the derivation.\n *\n * Per CLAUDE.md: data-only structure, so a class rather than an interface or a bare boolean.\n */\nexport class DestinationTrust {\n /**\n * The destination authenticates its CALLER (oidcSameProject() / oidcSvcs(...) / sharedSecret(...)), so it is entitled to\n * believe context WE vouch for — this is the service-to-service identity propagation that trusted\n * keys keep an `httpHeader` for.\n */\n private static readonly VERIFIES_CALLER = new DestinationTrust(true);\n\n /**\n * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / webhook(...) /\n * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.\n * Untrusted keys still travel.\n */\n private static readonly CANNOT_VERIFY_CALLER = new DestinationTrust(false);\n\n private constructor(private readonly verifiesCaller: boolean) {}\n\n /**\n * The ONLY way to obtain one: state the destination endpoint's auth mode. `undefined` (an\n * endpoint that declared no mode) is treated as un-verifying, i.e. the SAFE answer — an absent\n * declaration must never be the widest one.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor, exactly as ContextKey.trusted/untrusted do: the destination's auth mode must be part of the CALL, and a DI-injected instance method would let a caller hold one without ever naming a route\n static forAuthMode(mode: AuthMode | undefined): DestinationTrust {\n if (mode === undefined) {\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return DestinationTrust.VERIFIES_CALLER;\n case 'jwt':\n case 'public':\n // webhook(...) authenticates an OUTSIDE VENDOR, which is not the same thing as\n // authenticating a peer in this repo. The vendor knows nothing of webpieces context\n // headers and would never send one, so there is no identity to propagate in either\n // direction — and a webpieces client cannot call such an endpoint anyway (it cannot mint\n // the vendor's signature). Trusted keys stay home.\n case 'webhook':\n // apiKey(...) authenticates a CUSTOMER, not a peer service. The holder of the key is\n // another company's codebase, so nothing it forwards may be believed, and no webpieces\n // client can call it anyway (the framework extracts no api-key header — the app's hook\n // owns which headers carry the credential; the contract's `credentials` list only DESCRIBES\n // them). Trusted keys stay home.\n case 'apikey':\n // @WpLocalOnly authenticates NOBODY — it gates on the environment, not on a\n // credential — so a browser with curl on the same laptop is indistinguishable from us.\n // Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to\n // AuthMode is a compile error here rather than a silent permissive fallthrough.)\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n }\n\n /**\n * May this key cross the boundary between us and this destination? Untrusted keys always may —\n * nobody was ever going to make a security decision on them. A trusted key may only when we and\n * the destination authenticate each other.\n *\n * ONE method answers BOTH directions, deliberately, because it is one question:\n *\n * - OUTBOUND (`buildOutboundHeaders`): may we SEND a trusted key there? Only when the destination\n * will authenticate US, because that is the only case its `AuthFilter` will admit it.\n * - RESPONSE (`acceptResponseHeaders`): may we BELIEVE a trusted key that came BACK? Only from a\n * destination this client authenticated to — an `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)` peer is a\n * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is\n * somebody else's process whose response headers are an assertion, not a proof.\n *\n * A second, identically-bodied `believesResponsesFrom(key)` would be two spellings of one\n * decision — the shim shape `.claude/rules/no-backwards-compat.md` rejects — so the direction\n * lives in the CALLER's name and the rule lives here, once.\n */\n allows(key: AnyContextKey): boolean {\n return this.verifiesCaller || !key.isTrusted();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"DestinationTrust.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DestinationTrust.ts"],"names":[],"mappings":";;;AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAa,gBAAgB;IAgBJ;IAMA;IArBrB;;;;OAIG;IACK,MAAM,CAAU,eAAe,GAAG,IAAI,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAEhF;;;;OAIG;IACK,MAAM,CAAU,oBAAoB,GAAG,IAAI,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAEtF,YACqB,cAAuB;IACxC;;;;OAIG;IACc,YAAkD;QANlD,mBAAc,GAAd,cAAc,CAAS;QAMvB,iBAAY,GAAZ,YAAY,CAAsC;IACpE,CAAC;IAEJ;;;;;;OAMG;IACH,wGAAwG;IACxG,MAAM,CAAC,QAAQ,CAAC,KAAoB;QAChC,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;YACzB,OAAO,IAAI,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAClE,CAAC;QACD,OAAO,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC;IAED;;;;OAIG;IACH,UAAU;QACN,OAAO,IAAI,CAAC,YAAY,CAAC;IAC7B,CAAC;IAED;;;;OAIG;IACH,qSAAqS;IACrS,MAAM,CAAC,WAAW,CAAC,IAA0B;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACjD,CAAC;QACD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC;YACZ,KAAK,eAAe;gBAChB,OAAO,gBAAgB,CAAC,eAAe,CAAC;YAC5C,KAAK,KAAK,CAAC;YACX,KAAK,QAAQ,CAAC;YACd,kFAAkF;YAClF,oFAAoF;YACpF,mFAAmF;YACnF,qCAAqC;YACrC,KAAK,YAAY,CAAC;YAClB,qFAAqF;YACrF,uFAAuF;YACvF,uFAAuF;YACvF,4FAA4F;YAC5F,iCAAiC;YACjC,KAAK,QAAQ;gBACb,4EAA4E;gBAC5E,uFAAuF;gBACvF,wFAAwF;gBACxF,iFAAiF;gBAC7E,OAAO,gBAAgB,CAAC,oBAAoB,CAAC;QACrD,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,GAAkB;QACrB,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC5E,OAAO,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;IAED;;;;;;OAMG;IACH,gBAAgB,CAAC,GAAkB;QAC/B,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAClD,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;;AArHL,4CAsHC","sourcesContent":["import { AnyContextKey } from '../ContextKey';\nimport { AuthMode } from './auth-mode';\nimport { RouteMetadata } from './RouteMetadata';\n\n/**\n * DestinationTrust - the OUTBOUND half of the trust model: may a TRUSTED context key\n * ({@link ContextKey.trusted}) ride to the endpoint we are about to call?\n *\n * ## Why the client has to answer this at all\n *\n * The server already decided (see `PendingWireTrust`): an inbound `x-user-id` is admitted only on a\n * route that verified WHO called it — `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)`. On a `jwt()` or `@WpAuthPublic`\n * route the same header must match what the authenticator independently derived, or the request is\n * REJECTED with a 401.\n *\n * That rule is correct, and it means a client that ships `x-user-id` to a `@WpAuthPublic` endpoint is\n * building a request the callee is obliged to reject. Before this class the outbound builders\n * forwarded EVERY transferred key with no idea what the destination was, so an internal service\n * calling another service's public or JWT endpoint 401'd itself. The fix belongs on the producing\n * side: don't send what cannot possibly be believed.\n *\n * ## Why it is a class with a private constructor and no boolean parameter\n *\n * `buildOutboundHeaders(sendTrusted = true)` would have been three characters of work and exactly the\n * \"widening that is an ABSENCE rather than a token\" `.claude/rules/no-backwards-compat.md` rejects — the\n * permissive answer would be\n * what you get by not typing anything. There is no way to build a DestinationTrust except from the\n * destination endpoint's own {@link AuthMode}, so the caller cannot assert a posture the route does\n * not actually have, and `grep -rn DestinationTrust.forAuthMode` lists every place the question is\n * asked. The two instances are PRIVATE for the same reason: exposing them would be a second spelling\n * that skips the derivation.\n *\n * Per CLAUDE.md: data-only structure, so a class rather than an interface or a bare boolean.\n */\nexport class DestinationTrust {\n /**\n * The destination authenticates its CALLER (oidcSameProject() / oidcSvcs(...) / sharedSecret(...)), so it is entitled to\n * believe context WE vouch for — this is the service-to-service identity propagation that trusted\n * keys keep an `httpHeader` for.\n */\n private static readonly VERIFIES_CALLER = new DestinationTrust(true, undefined);\n\n /**\n * The destination cannot tell us from a browser with curl (jwt() / @WpAuthPublic / vendorAuth(...) /\n * apiKey(...) / @WpLocalOnly / an endpoint with no declared mode), so trusted keys are omitted.\n * Untrusted keys still travel.\n */\n private static readonly CANNOT_VERIFY_CALLER = new DestinationTrust(false, undefined);\n\n private constructor(\n private readonly verifiesCaller: boolean,\n /**\n * Set ONLY for an `EXTERNAL_OUTBOUND` destination: the `@ExternalContextKeys` list, which is\n * then the WHOLE of what may be sent. Undefined for every destination in this repo, whose wire\n * is governed by {@link forAuthMode} exactly as before.\n */\n private readonly externalKeys: readonly AnyContextKey[] | undefined,\n ) {}\n\n /**\n * The destination of ONE route. An `EXTERNAL_OUTBOUND` route (an outside system's API) gets a\n * destination that sends ONLY its `@ExternalContextKeys` and believes NO response header: a vendor\n * is somebody else's process, so our `x-request-id`, `x-webpieces-*` and app keys stay home unless\n * the contract lists them, and nothing it answers may write into our context. Every other route is\n * {@link forAuthMode}, unchanged — so the wire to our own servers is identical.\n */\n // webpieces-disable no-function-outside-class -- static factory beside forAuthMode, for the same reason\n static forRoute(route: RouteMetadata): DestinationTrust {\n if (route.externalOutbound) {\n return new DestinationTrust(false, route.externalContextKeys);\n }\n return DestinationTrust.forAuthMode(route.authMeta?.methods[0]);\n }\n\n /**\n * The keys an `EXTERNAL_OUTBOUND` destination sends — every one is REQUIRED, so a builder throws\n * before any I/O when one has no value. Undefined for every other destination, whose builder\n * iterates the registry's transferred keys and filters them with {@link allows}.\n */\n listedKeys(): readonly AnyContextKey[] | undefined {\n return this.externalKeys;\n }\n\n /**\n * The ONLY way to obtain one: state the destination endpoint's auth mode. `undefined` (an\n * endpoint that declared no mode) is treated as un-verifying, i.e. the SAFE answer — an absent\n * declaration must never be the widest one.\n */\n // webpieces-disable no-function-outside-class -- static factory replacing the (now private) constructor, exactly as ContextKey.trusted/untrusted do: the destination's auth mode must be part of the CALL, and a DI-injected instance method would let a caller hold one without ever naming a route\n static forAuthMode(mode: AuthMode | undefined): DestinationTrust {\n if (mode === undefined) {\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return DestinationTrust.VERIFIES_CALLER;\n case 'jwt':\n case 'public':\n // vendorAuth(...) authenticates an OUTSIDE VENDOR, which is not the same thing as\n // authenticating a peer in this repo. The vendor knows nothing of webpieces context\n // headers and would never send one, so there is no identity to propagate in either\n // direction. Trusted keys stay home.\n case 'vendorAuth':\n // apiKey(...) authenticates a CUSTOMER, not a peer service. The holder of the key is\n // another company's codebase, so nothing it forwards may be believed, and no webpieces\n // client can call it anyway (the framework extracts no api-key header — the app's hook\n // owns which headers carry the credential; the contract's `credentials` list only DESCRIBES\n // them). Trusted keys stay home.\n case 'apikey':\n // @WpLocalOnly authenticates NOBODY — it gates on the environment, not on a\n // credential — so a browser with curl on the same laptop is indistinguishable from us.\n // Same bucket as public/jwt. (This switch has NO `default` on purpose: adding a kind to\n // AuthMode is a compile error here rather than a silent permissive fallthrough.)\n return DestinationTrust.CANNOT_VERIFY_CALLER;\n }\n }\n\n /**\n * May this key cross the boundary between us and this destination? Untrusted keys always may —\n * nobody was ever going to make a security decision on them. A trusted key may only when we and\n * the destination authenticate each other.\n *\n * ONE method answers BOTH directions, deliberately, because it is one question:\n *\n * - OUTBOUND (`buildOutboundHeaders`): may we SEND a trusted key there? Only when the destination\n * will authenticate US, because that is the only case its `AuthFilter` will admit it.\n * - RESPONSE (`acceptResponseHeaders`): may we BELIEVE a trusted key that came BACK? Only from a\n * destination this client authenticated to — an `oidcSameProject()` / `oidcSvcs(...)` / `sharedSecret(...)` peer is a\n * service in our own trust domain, while a `@WpAuthPublic` / `jwt()` / partner endpoint is\n * somebody else's process whose response headers are an assertion, not a proof.\n *\n * For every destination in this repo the two directions are one question, so the response side\n * ({@link believesResponse}) delegates here; only an `EXTERNAL_OUTBOUND` destination answers them\n * differently (it sends listed keys and believes none).\n */\n allows(key: AnyContextKey): boolean {\n if (this.externalKeys !== undefined) return this.externalKeys.includes(key);\n return this.verifiesCaller || !key.isTrusted();\n }\n\n /**\n * May a key that came BACK on this destination's response be merged into our context? Exactly\n * {@link allows} for a destination in this repo — and NOTHING from an `EXTERNAL_OUTBOUND` one,\n * whose response headers are an outside system's, not a peer's. A separate method because for\n * that destination the two directions genuinely differ: a key we LIST to send (`x-request-id`)\n * is not thereby a key we let the vendor write back.\n */\n believesResponse(key: AnyContextKey): boolean {\n if (this.externalKeys !== undefined) return false;\n return this.allows(key);\n }\n}\n"]}
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const decorators_1 = require("./decorators");
|
|
4
4
|
(0, decorators_1.Endpoint)(decorators_1.POST, '/valid-rpc', decorators_1.WRITE, decorators_1.RPC);
|
|
5
|
-
(0, decorators_1.Endpoint)(decorators_1.
|
|
6
|
-
|
|
7
|
-
(0, decorators_1.Endpoint)(decorators_1.
|
|
8
|
-
|
|
9
|
-
(0, decorators_1.Endpoint)(decorators_1.
|
|
5
|
+
(0, decorators_1.Endpoint)(decorators_1.PUT, '/valid-put', decorators_1.WRITE_IDEMPOTENT, decorators_1.RPC);
|
|
6
|
+
(0, decorators_1.Endpoint)(decorators_1.PATCH, '/valid-patch', decorators_1.WRITE, decorators_1.RPC);
|
|
7
|
+
(0, decorators_1.Endpoint)(decorators_1.DELETE, '/valid-delete', decorators_1.WRITE_IDEMPOTENT, decorators_1.RPC);
|
|
8
|
+
(0, decorators_1.Endpoint)(decorators_1.POST, '/valid-external', decorators_1.WRITE, decorators_1.EXTERNAL_INBOUND, { calledBy: 'twilio' });
|
|
9
|
+
(0, decorators_1.Endpoint)(decorators_1.GET, '/valid-outbound', decorators_1.READ, decorators_1.EXTERNAL_OUTBOUND);
|
|
10
|
+
// @ts-expect-error inbound external endpoints require options containing calledBy
|
|
11
|
+
(0, decorators_1.Endpoint)(decorators_1.POST, '/missing-external-options', decorators_1.WRITE, decorators_1.EXTERNAL_INBOUND);
|
|
12
|
+
// @ts-expect-error inbound external endpoint options require calledBy
|
|
13
|
+
(0, decorators_1.Endpoint)(decorators_1.POST, '/missing-external-caller', decorators_1.WRITE, decorators_1.EXTERNAL_INBOUND, { formPost: true });
|
|
10
14
|
// @ts-expect-error callerKind is a closed enum-backed union rather than free text
|
|
11
|
-
(0, decorators_1.Endpoint)(decorators_1.POST, '/invalid-caller-kind', decorators_1.WRITE, decorators_1.
|
|
15
|
+
(0, decorators_1.Endpoint)(decorators_1.POST, '/invalid-caller-kind', decorators_1.WRITE, decorators_1.EXTERNAL_INBOUND, {
|
|
12
16
|
calledBy: 'twilio',
|
|
13
17
|
callerKind: 'vendor',
|
|
14
18
|
});
|
|
@@ -18,4 +22,6 @@ const decorators_1 = require("./decorators");
|
|
|
18
22
|
(0, decorators_1.Endpoint)('POST', '/raw-method', decorators_1.WRITE, decorators_1.RPC);
|
|
19
23
|
// @ts-expect-error callers must import the enum-backed trigger-kind constant
|
|
20
24
|
(0, decorators_1.Endpoint)(decorators_1.POST, '/raw-trigger', decorators_1.WRITE, 'rpc');
|
|
25
|
+
// @ts-expect-error callers must import the enum-backed HTTP method constant, for the new verbs too
|
|
26
|
+
(0, decorators_1.Endpoint)('PUT', '/raw-put', decorators_1.WRITE, decorators_1.RPC);
|
|
21
27
|
//# sourceMappingURL=EndpointCompileAssertions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EndpointCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/EndpointCompileAssertions.ts"],"names":[],"mappings":";;AAAA,
|
|
1
|
+
{"version":3,"file":"EndpointCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/EndpointCompileAssertions.ts"],"names":[],"mappings":";;AAAA,6CAAgJ;AAEhJ,IAAA,qBAAQ,EAAC,iBAAI,EAAE,YAAY,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC;AACzC,IAAA,qBAAQ,EAAC,gBAAG,EAAE,YAAY,EAAE,6BAAgB,EAAE,gBAAG,CAAC,CAAC;AACnD,IAAA,qBAAQ,EAAC,kBAAK,EAAE,cAAc,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC;AAC5C,IAAA,qBAAQ,EAAC,mBAAM,EAAE,eAAe,EAAE,6BAAgB,EAAE,gBAAG,CAAC,CAAC;AACzD,IAAA,qBAAQ,EAAC,iBAAI,EAAE,iBAAiB,EAAE,kBAAK,EAAE,6BAAgB,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;AACnF,IAAA,qBAAQ,EAAC,gBAAG,EAAE,iBAAiB,EAAE,iBAAI,EAAE,8BAAiB,CAAC,CAAC;AAE1D,kFAAkF;AAClF,IAAA,qBAAQ,EAAC,iBAAI,EAAE,2BAA2B,EAAE,kBAAK,EAAE,6BAAgB,CAAC,CAAC;AAErE,sEAAsE;AACtE,IAAA,qBAAQ,EAAC,iBAAI,EAAE,0BAA0B,EAAE,kBAAK,EAAE,6BAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AAExF,kFAAkF;AAClF,IAAA,qBAAQ,EAAC,iBAAI,EAAE,sBAAsB,EAAE,kBAAK,EAAE,6BAAgB,EAAE;IAC5D,QAAQ,EAAE,QAAQ;IAClB,UAAU,EAAE,QAAQ;CACvB,CAAC,CAAC;AAEH,0EAA0E;AAC1E,IAAA,qBAAQ,EAAC,iBAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAG,CAAC,CAAC;AAE/C,4EAA4E;AAC5E,IAAA,qBAAQ,EAAC,MAAM,EAAE,aAAa,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC;AAE5C,6EAA6E;AAC7E,IAAA,qBAAQ,EAAC,iBAAI,EAAE,cAAc,EAAE,kBAAK,EAAE,KAAK,CAAC,CAAC;AAE7C,mGAAmG;AACnG,IAAA,qBAAQ,EAAC,KAAK,EAAE,UAAU,EAAE,kBAAK,EAAE,gBAAG,CAAC,CAAC","sourcesContent":["import { DELETE, GET, READ, Endpoint, EXTERNAL_INBOUND, EXTERNAL_OUTBOUND, PATCH, POST, PUT, RPC, WRITE, WRITE_IDEMPOTENT } from './decorators';\n\nEndpoint(POST, '/valid-rpc', WRITE, RPC);\nEndpoint(PUT, '/valid-put', WRITE_IDEMPOTENT, RPC);\nEndpoint(PATCH, '/valid-patch', WRITE, RPC);\nEndpoint(DELETE, '/valid-delete', WRITE_IDEMPOTENT, RPC);\nEndpoint(POST, '/valid-external', WRITE, EXTERNAL_INBOUND, { calledBy: 'twilio' });\nEndpoint(GET, '/valid-outbound', READ, EXTERNAL_OUTBOUND);\n\n// @ts-expect-error inbound external endpoints require options containing calledBy\nEndpoint(POST, '/missing-external-options', WRITE, EXTERNAL_INBOUND);\n\n// @ts-expect-error inbound external endpoint options require calledBy\nEndpoint(POST, '/missing-external-caller', WRITE, EXTERNAL_INBOUND, { formPost: true });\n\n// @ts-expect-error callerKind is a closed enum-backed union rather than free text\nEndpoint(POST, '/invalid-caller-kind', WRITE, EXTERNAL_INBOUND, {\n calledBy: 'twilio',\n callerKind: 'vendor',\n});\n\n// @ts-expect-error callers must import the enum-backed operation constant\nEndpoint(POST, '/raw-operation', 'write', RPC);\n\n// @ts-expect-error callers must import the enum-backed HTTP method constant\nEndpoint('POST', '/raw-method', WRITE, RPC);\n\n// @ts-expect-error callers must import the enum-backed trigger-kind constant\nEndpoint(POST, '/raw-trigger', WRITE, 'rpc');\n\n// @ts-expect-error callers must import the enum-backed HTTP method constant, for the new verbs too\nEndpoint('PUT', '/raw-put', WRITE, RPC);\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
|
-
|
|
10
|
-
export
|
|
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
|
-
/**
|
|
14
|
-
|
|
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
|
-
|
|
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 {};
|
package/src/http/HttpContract.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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
|
-
* `
|
|
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
|
|
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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WRITE = exports.WRITE_IDEMPOTENT = exports.READ = void 0;
|
|
3
|
+
exports.NONE = exports.WRITE = exports.WRITE_IDEMPOTENT = exports.READ = void 0;
|
|
4
4
|
/** Nominal backing keeps raw string literals out of endpoint declarations. */
|
|
5
5
|
var EndpointOperationValue;
|
|
6
6
|
(function (EndpointOperationValue) {
|
|
@@ -12,4 +12,11 @@ var EndpointOperationValue;
|
|
|
12
12
|
exports.READ = EndpointOperationValue.READ;
|
|
13
13
|
exports.WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;
|
|
14
14
|
exports.WRITE = EndpointOperationValue.WRITE;
|
|
15
|
+
/** Nominal backing for {@link EndpointOptions.responseBody}. */
|
|
16
|
+
var ResponseBodyValue;
|
|
17
|
+
(function (ResponseBodyValue) {
|
|
18
|
+
ResponseBodyValue["NONE"] = "none";
|
|
19
|
+
})(ResponseBodyValue || (ResponseBodyValue = {}));
|
|
20
|
+
/** `@Endpoint(..., { responseBody: NONE })` — the success response carries 0 bytes. */
|
|
21
|
+
exports.NONE = ResponseBodyValue.NONE;
|
|
15
22
|
//# sourceMappingURL=HttpEndpointOptions.js.map
|