@webpieces/http-client-node 0.4.863 → 0.4.865

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,7 +73,8 @@ It is not a different kind of client. It is ONE filter:
73
73
  @ApiPath('/ot-webhook')
74
74
  export class PartnerWebhookApi {
75
75
  @Endpoint(POST, '/deliver', WRITE, RPC)
76
- @WpAuthWebhook('partner-hmac')
76
+ @WpAuth([webhook('partner-hmac')])
77
+ @WpAuthorization({authType: AuthorizationType.USERS_OR_SERVICES})
77
78
  deliver(envelope: WebhookEnvelope): Promise<DeliveryAck>;
78
79
  }
79
80
 
@@ -112,16 +113,16 @@ for (const webhook of webhooks) {
112
113
  refused rather than obeyed. The one relaxation — testing the partner path against a local fake —
113
114
  has to be said out loud, with a reason:
114
115
  `new ContextFullUrlFilter(new SsrfTestingPolicy('local fake in the delivery e2e'))`.
115
- - **Every auth mode still works.** `@WpAuthOidc` mints for the FINAL base URL, `@WpAuthSharedSecret`
116
+ - **Every auth mode still works.** `oidc(...)` mints for the FINAL base URL, `sharedSecret(...)`
116
117
  sends the value this client holds (N services implementing one contract behind one agreed secret is
117
- a real topology), and `@WpAuthWebhook(name)` calls your bound `WebhookSignerCallback`. The minter runs
118
+ a real topology), and `@WpAuth([webhook(name)])` calls your bound `WebhookSignerCallback`. The minter runs
118
119
  BELOW the SSRF guard, so a destination that is going to be refused never causes a credential to be
119
120
  created.
120
121
  - **The hop is VISIBLE.** `@externalSystem runtime <identity>` on the contract draws the destination
121
122
  as its own node on the runtime architecture graph, and two services delivering over the same
122
123
  contract converge on one box.
123
124
 
124
- ## `@WpAuthOidc`: generate the credential the Cloud Run edge expects
125
+ ## `oidc(...)`: generate the credential the Cloud Run edge expects
125
126
 
126
127
  The shared contract tells `OutboundAuthFilter` to mint a Google ID token for the final
127
128
  base URL and send it in `Authorization: Bearer`. A private Cloud Run service verifies
@@ -130,9 +131,9 @@ supplementary server checks; its empty caller list relies on edge authorization.
130
131
  clients mint unsigned synthetic tokens for local tests. See [Cloud Run OIDC](../../../docs/architecture/cloud-run-oidc.md)
131
132
  for deployment assumptions and why a direct application-verifier test does not test the edge.
132
133
 
133
- ## Signing an OUTBOUND webhook — `@WpAuthWebhook`, the other way round
134
+ ## Signing an OUTBOUND webhook — `webhook(...)`, the other way round
134
135
 
135
- `@WpAuthWebhook(name)` names a signing SCHEME, not a direction. Inbound, a vendor signs and your bound
136
+ `@WpAuth([webhook(name)])` names a signing SCHEME, not a direction. Inbound, a vendor signs and your bound
136
137
  `WebhookAuthCallback` (in `@webpieces/http-routing`) verifies. Outbound, WE are the vendor, so your
137
138
  bound `WebhookSignerCallback` produces the signature over the final URL and the exact wire bytes:
138
139
 
@@ -151,7 +152,7 @@ options.bind(WEBHOOK_SIGNER_CALLBACK).to(PartnerHmacSigner);
151
152
 
152
153
  The framework ships no vendor crypto, deliberately: Twilio signs the full URL with sorted params,
153
154
  Slack signs `v0:{ts}:{body}`, Meta signs the raw body. The scheme lives in your hook and the vendor on
154
- the contract. With **no** `WebhookSignerCallback` bound, every outbound `@WpAuthWebhook` call THROWS
155
+ the contract. With **no** `WebhookSignerCallback` bound, every outbound `webhook(...)` call THROWS
155
156
  rather than delivering unsigned — the mirror of an unbound `WebhookAuthCallback` 401ing every inbound
156
157
  one.
157
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-client-node",
3
- "version": "0.4.863",
3
+ "version": "0.4.865",
4
4
  "description": "Server-side HTTP client for webpieces: inversify-wired, reads RequestContext directly, mints OIDC/shared-secret delivery auth, resolves Cloud Run URLs from a service name",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -22,10 +22,10 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@webpieces/core-context": "0.4.863",
26
- "@webpieces/core-util": "0.4.863",
27
- "@webpieces/gcp-identity": "0.4.863",
28
- "@webpieces/http-client-core": "0.4.863",
25
+ "@webpieces/core-context": "0.4.865",
26
+ "@webpieces/core-util": "0.4.865",
27
+ "@webpieces/gcp-identity": "0.4.865",
28
+ "@webpieces/http-client-core": "0.4.865",
29
29
  "inversify": "7.10.4",
30
30
  "reflect-metadata": "0.2.2"
31
31
  }
@@ -32,7 +32,7 @@ import { NodeProxyClient } from './NodeProxyClient';
32
32
  * ]);
33
33
  * ```
34
34
  * The SSRF guard arms itself the moment that filter re-points a request, and the contract's
35
- * `@WpAuthWebhook(name)` selects the app's bound `WebhookSignerCallback` to sign the exact bytes —
35
+ * `webhook(name)` selects the app's bound `WebhookSignerCallback` to sign the exact bytes —
36
36
  * neither is something the app registers, orders, or can displace.
37
37
  *
38
38
  * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the
@@ -40,7 +40,7 @@ const NodeProxyClient_1 = require("./NodeProxyClient");
40
40
  * ]);
41
41
  * ```
42
42
  * The SSRF guard arms itself the moment that filter re-points a request, and the contract's
43
- * `@WpAuthWebhook(name)` selects the app's bound `WebhookSignerCallback` to sign the exact bytes —
43
+ * `webhook(name)` selects the app's bound `WebhookSignerCallback` to sign the exact bytes —
44
44
  * neither is something the app registers, orders, or can displace.
45
45
  *
46
46
  * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the
@@ -1 +1 @@
1
- {"version":3,"file":"ClientHttpFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientHttpFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,oDAA4E;AAC5E,0DAIiC;AAEjC,kEAA+D;AAE/D,uDAAgF;AAEhF,iGAAiG;AACjG,6EAA6E;AAC7E,IAAA,oCAAqB,EAAC,4CAA0B,EAAE,iCAAe,CAAC,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAGL;IAFrB,YAEqB,mBAA8C;QAA9C,wBAAmB,GAAnB,mBAAmB,CAA2B;IAChE,CAAC;IAEJ;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,eAAe,CACX,YAA6B,EAC7B,MAAoB,EACpB,OAA2C;QAE3C,IAAA,gCAAoB,EAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC;QACxD,sFAAsF;QACtF,wEAAwE;QACxE,MAAM,WAAW,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;QACnD,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;QAClF,OAAO,IAAA,mCAAgB,EAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACvD,CAAC;CACJ,CAAA;AA1CY,8CAAiB;4BAAjB,iBAAiB;IAF7B,IAAA,0BAAc,GAAE;IAChB,IAAA,wCAAyB,GAAE;IAGnB,mBAAA,IAAA,kBAAM,EAAC,4CAA0B,CAAC,CAAA;6CACG,uBAAQ;GAHzC,iBAAiB,CA0C7B","sourcesContent":["import { inject } from 'inversify';\nimport { assertNotInternalApi, DocumentDesign } from '@webpieces/core-util';\nimport {\n Provider,\n bindFrameworkProvider,\n provideFrameworkSingleton,\n} from '@webpieces/core-context';\nimport type { ApiPrototype, ClientFilterDefinition } from '@webpieces/http-client-core';\nimport { buildClientProxy } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\nimport { NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient } from './NodeProxyClient';\n\n// Teach the container how to hand out fresh NodeProxyClients. NodeProxyClient is bound TRANSIENT\n// (@provideFrameworkTransient), so each provider.get() constructs a new one.\nbindFrameworkProvider(NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient);\n\n/**\n * ClientHttpFactory - builds type-safe HTTP clients from API prototypes carrying\n * @ApiPath/@Endpoint decorators. The SERVER-side factory.\n *\n * This is the client-side equivalent of ApiRoutingFactory:\n * - Server routing: ApiRoutingFactory reads decorators -> routes HTTP requests to controllers\n * - Server client: ClientHttpFactory reads decorators -> generates HTTP requests from method calls\n *\n * Inject it and ask for a typed client per contract:\n * ```typescript\n * // same project + region as this container; the URL is derived, you maintain nothing\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n *\n * // to reach somewhere derivation cannot describe (other region/project, non-Cloud-Run, localhost),\n * // register it once at startup — the client still carries only the svcName:\n * // ClientRegistry.addUrlMapping('legacy', 'https://legacy.corp');\n * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy'));\n *\n * const response = await server2.fetchValue(req); // inside a RequestContext\n * ```\n *\n * A destination that is DATA rather than deployment — a URL a partner registered — is a FILTER, not\n * a different kind of client. Install `ContextBaseUrlFilter` on the one client that may be\n * re-pointed, and set the URL per call:\n * ```typescript\n * const partner = factory.createRpcClient(PartnerWebhookApi, new ClientConfig('partner-webhooks'), [\n * new ClientFilterDefinition(1000, new ContextBaseUrlFilter()),\n * ]);\n * ```\n * The SSRF guard arms itself the moment that filter re-points a request, and the contract's\n * `@WpAuthWebhook(name)` selects the app's bound `WebhookSignerCallback` to sign the exact bytes —\n * neither is something the app registers, orders, or can displace.\n *\n * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the\n * injected `Provider<NodeProxyClient>` hands out a fresh one per contract, which `createRpcClient`\n * then `init`s. Their collaborators (RequestContextHeaders, Secrets) come from the container, so\n * the whole dependency graph is visible in this package's design.html.\n *\n * Unlike @webpieces/http-client-browser this package is Node-only, so the factory IS the inversify\n * entry point and the magic context is read straight from the RequestContext. A call made outside\n * `RequestContext.run(...)` throws rather than silently dropping the trace.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class ClientHttpFactory {\n constructor(\n @inject(NODE_PROXY_CLIENT_PROVIDER)\n private readonly proxyClientProvider: Provider<NodeProxyClient>,\n ) {}\n\n /**\n * Create a type-safe RPC (HTTP) client for one API contract.\n *\n * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators\n * @param config - This client's state: its svcName, which is what `ClientRegistry` resolves\n * @param filters - This client's own OUTBOUND filters, each with the priority it runs at\n * (highest OUTERMOST). They wrap the send, so a filter may rewrite the URL, add or remove\n * headers, log, or replace `ClientRequest.body` — the exact bytes transmitted. What goes\n * here is APP behaviour: url rewriting, headers, logging, and `ContextBaseUrlFilter` when\n * this client's destination arrives per call.\n *\n * OPTIONAL, and omitting it is not a statement about security: the framework's own SSRF\n * guard and credential minter are installed on every client regardless, BENEATH anything\n * passed here, so there is nothing an app can decline by writing nothing.\n *\n * A PLAIN OPTIONAL ARRAY, deliberately. Omitting it, passing `[]`, and passing a list you\n * built up conditionally all mean the same thing and all compile — because the normal way\n * an app arrives at a filter list is to declare a `ClientFilterDefinition[]` and push to\n * it under `if`s, and the declared type of such a local can never be non-empty. A\n * non-empty tuple here would reject that ordinary code and force a cast at the call site.\n * `filters === undefined ? [] : [...filters]` below normalizes the two empty spellings to\n * one value before anything downstream sees them. Pinned in\n * {@link CreateRpcClientCompileAssertions}.\n */\n createRpcClient<T extends object>(\n apiPrototype: ApiPrototype<T>,\n config: ClientConfig,\n filters?: readonly ClientFilterDefinition[],\n ): T {\n assertNotInternalApi(apiPrototype, 'ClientHttpFactory');\n // Fresh instance per contract — NodeProxyClient is transient. init() binds it to this\n // contract + target; the collaborators already came from the container.\n const proxyClient = this.proxyClientProvider.get();\n proxyClient.init(apiPrototype, config, filters === undefined ? [] : [...filters]);\n return buildClientProxy(apiPrototype, proxyClient);\n }\n}\n"]}
1
+ {"version":3,"file":"ClientHttpFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientHttpFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,oDAA4E;AAC5E,0DAIiC;AAEjC,kEAA+D;AAE/D,uDAAgF;AAEhF,iGAAiG;AACjG,6EAA6E;AAC7E,IAAA,oCAAqB,EAAC,4CAA0B,EAAE,iCAAe,CAAC,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAGL;IAFrB,YAEqB,mBAA8C;QAA9C,wBAAmB,GAAnB,mBAAmB,CAA2B;IAChE,CAAC;IAEJ;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,eAAe,CACX,YAA6B,EAC7B,MAAoB,EACpB,OAA2C;QAE3C,IAAA,gCAAoB,EAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC;QACxD,sFAAsF;QACtF,wEAAwE;QACxE,MAAM,WAAW,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;QACnD,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;QAClF,OAAO,IAAA,mCAAgB,EAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACvD,CAAC;CACJ,CAAA;AA1CY,8CAAiB;4BAAjB,iBAAiB;IAF7B,IAAA,0BAAc,GAAE;IAChB,IAAA,wCAAyB,GAAE;IAGnB,mBAAA,IAAA,kBAAM,EAAC,4CAA0B,CAAC,CAAA;6CACG,uBAAQ;GAHzC,iBAAiB,CA0C7B","sourcesContent":["import { inject } from 'inversify';\nimport { assertNotInternalApi, DocumentDesign } from '@webpieces/core-util';\nimport {\n Provider,\n bindFrameworkProvider,\n provideFrameworkSingleton,\n} from '@webpieces/core-context';\nimport type { ApiPrototype, ClientFilterDefinition } from '@webpieces/http-client-core';\nimport { buildClientProxy } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\nimport { NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient } from './NodeProxyClient';\n\n// Teach the container how to hand out fresh NodeProxyClients. NodeProxyClient is bound TRANSIENT\n// (@provideFrameworkTransient), so each provider.get() constructs a new one.\nbindFrameworkProvider(NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient);\n\n/**\n * ClientHttpFactory - builds type-safe HTTP clients from API prototypes carrying\n * @ApiPath/@Endpoint decorators. The SERVER-side factory.\n *\n * This is the client-side equivalent of ApiRoutingFactory:\n * - Server routing: ApiRoutingFactory reads decorators -> routes HTTP requests to controllers\n * - Server client: ClientHttpFactory reads decorators -> generates HTTP requests from method calls\n *\n * Inject it and ask for a typed client per contract:\n * ```typescript\n * // same project + region as this container; the URL is derived, you maintain nothing\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n *\n * // to reach somewhere derivation cannot describe (other region/project, non-Cloud-Run, localhost),\n * // register it once at startup — the client still carries only the svcName:\n * // ClientRegistry.addUrlMapping('legacy', 'https://legacy.corp');\n * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy'));\n *\n * const response = await server2.fetchValue(req); // inside a RequestContext\n * ```\n *\n * A destination that is DATA rather than deployment — a URL a partner registered — is a FILTER, not\n * a different kind of client. Install `ContextBaseUrlFilter` on the one client that may be\n * re-pointed, and set the URL per call:\n * ```typescript\n * const partner = factory.createRpcClient(PartnerWebhookApi, new ClientConfig('partner-webhooks'), [\n * new ClientFilterDefinition(1000, new ContextBaseUrlFilter()),\n * ]);\n * ```\n * The SSRF guard arms itself the moment that filter re-points a request, and the contract's\n * `webhook(name)` selects the app's bound `WebhookSignerCallback` to sign the exact bytes —\n * neither is something the app registers, orders, or can displace.\n *\n * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the\n * injected `Provider<NodeProxyClient>` hands out a fresh one per contract, which `createRpcClient`\n * then `init`s. Their collaborators (RequestContextHeaders, Secrets) come from the container, so\n * the whole dependency graph is visible in this package's design.html.\n *\n * Unlike @webpieces/http-client-browser this package is Node-only, so the factory IS the inversify\n * entry point and the magic context is read straight from the RequestContext. A call made outside\n * `RequestContext.run(...)` throws rather than silently dropping the trace.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class ClientHttpFactory {\n constructor(\n @inject(NODE_PROXY_CLIENT_PROVIDER)\n private readonly proxyClientProvider: Provider<NodeProxyClient>,\n ) {}\n\n /**\n * Create a type-safe RPC (HTTP) client for one API contract.\n *\n * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators\n * @param config - This client's state: its svcName, which is what `ClientRegistry` resolves\n * @param filters - This client's own OUTBOUND filters, each with the priority it runs at\n * (highest OUTERMOST). They wrap the send, so a filter may rewrite the URL, add or remove\n * headers, log, or replace `ClientRequest.body` — the exact bytes transmitted. What goes\n * here is APP behaviour: url rewriting, headers, logging, and `ContextBaseUrlFilter` when\n * this client's destination arrives per call.\n *\n * OPTIONAL, and omitting it is not a statement about security: the framework's own SSRF\n * guard and credential minter are installed on every client regardless, BENEATH anything\n * passed here, so there is nothing an app can decline by writing nothing.\n *\n * A PLAIN OPTIONAL ARRAY, deliberately. Omitting it, passing `[]`, and passing a list you\n * built up conditionally all mean the same thing and all compile — because the normal way\n * an app arrives at a filter list is to declare a `ClientFilterDefinition[]` and push to\n * it under `if`s, and the declared type of such a local can never be non-empty. A\n * non-empty tuple here would reject that ordinary code and force a cast at the call site.\n * `filters === undefined ? [] : [...filters]` below normalizes the two empty spellings to\n * one value before anything downstream sees them. Pinned in\n * {@link CreateRpcClientCompileAssertions}.\n */\n createRpcClient<T extends object>(\n apiPrototype: ApiPrototype<T>,\n config: ClientConfig,\n filters?: readonly ClientFilterDefinition[],\n ): T {\n assertNotInternalApi(apiPrototype, 'ClientHttpFactory');\n // Fresh instance per contract — NodeProxyClient is transient. init() binds it to this\n // contract + target; the collaborators already came from the container.\n const proxyClient = this.proxyClientProvider.get();\n proxyClient.init(apiPrototype, config, filters === undefined ? [] : [...filters]);\n return buildClientProxy(apiPrototype, proxyClient);\n }\n}\n"]}
@@ -19,6 +19,7 @@ export declare class NodeProxyClient extends ProxyClient {
19
19
  private readonly addressResolver;
20
20
  private readonly secrets?;
21
21
  private readonly webhookSigner?;
22
+ protected selectAuthMethod(route: import('@webpieces/core-util').RouteMetadata): import('@webpieces/core-util').AuthMode;
22
23
  private config;
23
24
  constructor(headers: RequestContextHeaders, gcpOidc: GcpOidc, addressResolver: AddressResolver, secrets?: Secrets | undefined, webhookSigner?: WebhookSignerCallback | undefined);
24
25
  /**
@@ -69,8 +70,8 @@ export declare class NodeProxyClient extends ProxyClient {
69
70
  * Straight from the RequestContext. Throws when there is no active request scope.
70
71
  *
71
72
  * `destination` rides through unchanged: this is the ONE client that can legitimately propagate a
72
- * verified identity, and it does so exactly when the callee will authenticate us (@WpAuthOidc /
73
- * @WpAuthSharedSecret). Calling a peer's @WpAuthPublic or @WpAuthJwt endpoint now omits `x-user-id` and
73
+ * verified identity, and it does so exactly when the callee will authenticate us (oidc(...) /
74
+ * sharedSecret(...)). Calling a peer's @WpAuthPublic or jwt() endpoint now omits `x-user-id` and
74
75
  * friends instead of shipping headers that endpoint's AuthFilter is obliged to reject.
75
76
  */
76
77
  protected outboundContextHeaders(destination: DestinationTrust): Map<string, string>;
@@ -41,6 +41,19 @@ let NodeProxyClient = class NodeProxyClient extends http_client_core_1.ProxyClie
41
41
  addressResolver;
42
42
  secrets;
43
43
  webhookSigner;
44
+ selectAuthMethod(route) {
45
+ const methods = route.authMeta?.methods;
46
+ if (!methods)
47
+ throw new Error('Endpoint requires @WpAuth or @WpAuthPublic.');
48
+ const selected = methods.find((method) => method.kind === 'oidc' || method.kind === 'public' ||
49
+ (method.kind === 'shared-secret' && !!this.secrets?.get(method.secretKey)) ||
50
+ (method.kind === 'webhook' && !!this.webhookSigner));
51
+ if (selected)
52
+ return selected;
53
+ if (methods.length === 1)
54
+ return methods[0];
55
+ throw new Error('No configured Node client credential can satisfy this endpoint; bind a declared service credential.');
56
+ }
44
57
  config;
45
58
  constructor(headers, gcpOidc, addressResolver, secrets, webhookSigner) {
46
59
  // This package is node-only and already depends on core-context, so it builds the
@@ -170,8 +183,8 @@ let NodeProxyClient = class NodeProxyClient extends http_client_core_1.ProxyClie
170
183
  * Straight from the RequestContext. Throws when there is no active request scope.
171
184
  *
172
185
  * `destination` rides through unchanged: this is the ONE client that can legitimately propagate a
173
- * verified identity, and it does so exactly when the callee will authenticate us (@WpAuthOidc /
174
- * @WpAuthSharedSecret). Calling a peer's @WpAuthPublic or @WpAuthJwt endpoint now omits `x-user-id` and
186
+ * verified identity, and it does so exactly when the callee will authenticate us (oidc(...) /
187
+ * sharedSecret(...)). Calling a peer's @WpAuthPublic or jwt() endpoint now omits `x-user-id` and
175
188
  * friends instead of shipping headers that endpoint's AuthFilter is obliged to reject.
176
189
  */
177
190
  outboundContextHeaders(destination) {
@@ -1 +1 @@
1
- {"version":3,"file":"NodeProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/NodeProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { once } from 'node:events';\nimport { request as httpRequest, IncomingMessage } from 'node:http';\nimport { request as httpsRequest } from 'node:https';\nimport { Readable } from 'node:stream';\nimport {\n ClientRegistry,\n DestinationTrust,\n ApiImplementationError,\n RecordedEndpoint,\n RecordedError,\n LogApiCallImpl,\n RouteMetadata,\n Secrets,\n SECRETS,\n TestCaseRecorder,\n toError,\n} from '@webpieces/core-util';\nimport {\n RequestContext,\n RequestContextApiCallContext,\n RequestContextHeaders,\n provideFrameworkTransient,\n} from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport {\n ApiPrototype,\n ByteReadableStream,\n ClientRequest,\n ClientFilterDefinition,\n ProxyClient,\n} from '@webpieces/http-client-core';\nimport { AddressResolver } from './AddressResolver';\nimport { ClientConfig } from './ClientConfig';\nimport { ContextBaseUrlFilter } from './ContextBaseUrlFilter';\nimport { ContextFullUrlFilter } from './ContextFullUrlFilter';\nimport { OutboundAuthFilter } from './OutboundAuthFilter';\nimport { SsrfGuardFilter } from './SsrfGuardFilter';\nimport { SsrfPolicy } from './SsrfPolicy';\nimport { WEBHOOK_SIGNER_CALLBACK, WebhookSignerCallback } from './WebhookSignerCallback';\n\n/**\n * The two framework built-ins' priorities, RELATIVE TO EACH OTHER and to nothing else — they are\n * ordered beneath every app filter structurally, not by number (see `ProxyClient.initRoutes`).\n *\n * The guard is OUTSIDE the minter deliberately: a destination that is going to be refused must be\n * refused BEFORE a credential is minted for it, so a hostile URL never causes a token to exist.\n */\nconst SSRF_GUARD_PRIORITY = 900;\nconst OUTBOUND_AUTH_PRIORITY = 800;\n\n/**\n * The server-side {@link ProxyClient}. Everything a browser cannot do lives here: reading the\n * ambient RequestContext, minting OIDC tokens, holding shared secrets, and recording test cases.\n *\n * TRANSIENT on purpose. Every `createRpcClient(api, config)` needs its own instance, because `init()`\n * binds one instance to exactly one API contract and one target. {@link ProxyClientProvider} hands\n * them out — see its doc.\n */\n@provideFrameworkTransient()\nexport class NodeProxyClient extends ProxyClient {\n private config!: ClientConfig;\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(AddressResolver) private readonly addressResolver: AddressResolver,\n // @optional: only @WpAuthSharedSecret endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(SECRETS) private readonly secrets?: Secrets,\n // @optional: only @WpAuthWebhook endpoints need it, and an unbound one makes them THROW\n // rather than deliver unsigned — see WebhookSignerCallback.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional()\n @inject(WEBHOOK_SIGNER_CALLBACK)\n private readonly webhookSigner?: WebhookSignerCallback,\n ) {\n // This package is node-only and already depends on core-context, so it builds the\n // RequestContext-backed ApiCallContext itself. No startup install, and therefore nothing a\n // non-webpieces host (plain NestJS/Express) can forget.\n super(new LogApiCallImpl(new RequestContextApiCallContext()));\n }\n\n /**\n * Bind this client to one API contract + target, with the app's outbound filters (url\n * rewriting, header editing, logging, per-call re-pointing).\n */\n init(\n apiPrototype: ApiPrototype<object>,\n config: ClientConfig,\n appFilters: ClientFilterDefinition[],\n ): void {\n this.config = config;\n this.initRoutes(apiPrototype, appFilters);\n }\n\n /**\n * The same chain every client runs — a ClientRegistry mapping, else the installed deriver — but\n * with NODE's fallback: THROW. A server has no \"own origin\" to fall back to the way a browser\n * does, so an unresolvable peer is a setup bug and must fail loudly (the error names the fixes).\n *\n * Resolved per call, never at construction, so building a client stays synchronous. Any metadata\n * read beneath a deriver is memoized process-wide, so only the first call pays.\n */\n protected override resolveBaseUrl(): Promise<string> {\n return ClientRegistry.resolve(this.config.svcName);\n }\n\n /** Node/undici supports a live ReadableStream body when fetch is given `duplex: 'half'`. */\n protected override supportsConcurrentDuplexFetch(): boolean {\n return true;\n }\n\n /**\n * Node's native HTTP stream is genuinely concurrent: unlike browser Fetch, an early response\n * does not cancel the still-open upload. The returned Web Response keeps parsing isomorphic.\n */\n protected override sendStreamingTransport(\n request: ClientRequest,\n signal: AbortSignal,\n body: ByteReadableStream,\n ): Promise<Response> {\n return new Promise<Response>(\n (resolve: (response: Response) => void, reject: (error: Error) => void) => {\n const url = new URL(request.url);\n const send = url.protocol === 'https:' ? httpsRequest : httpRequest;\n const outgoing = send(\n url,\n {\n method: 'POST',\n headers: request.headersAsRecord(),\n signal,\n },\n (incoming: IncomingMessage): void => {\n const headers = new Headers();\n for (const [name, value] of Object.entries(incoming.headers)) {\n if (Array.isArray(value)) {\n for (const item of value) headers.append(name, item);\n } else if (value !== undefined) {\n headers.set(name, value);\n }\n }\n const responseBody = Readable.toWeb(incoming) as ReadableStream<Uint8Array>;\n resolve(\n new Response(responseBody, {\n status: incoming.statusCode ?? 500,\n statusText: incoming.statusMessage,\n headers,\n }),\n );\n },\n );\n outgoing.once('error', reject);\n // webpieces-disable no-any-unknown -- a native socket/write rejection is normalized by toError\n void this.pumpStreamingRequest(body, outgoing).catch((err: unknown) => {\n const error = toError(err);\n outgoing.destroy(error);\n });\n },\n );\n }\n\n private async pumpStreamingRequest(\n body: ByteReadableStream,\n outgoing: import('node:http').ClientRequest,\n ): Promise<void> {\n const reader = body.getReader();\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- socket errors reject writer acknowledgements\n try {\n for (;;) {\n const part = await reader.read();\n if (part.done) break;\n if (!outgoing.write(part.value)) await once(outgoing, 'drain');\n }\n outgoing.end();\n } finally {\n reader.releaseLock();\n }\n }\n\n /**\n * The two framework built-ins, installed on EVERY client this package builds and ordered\n * beneath every app filter by {@link ProxyClient.initRoutes}.\n *\n * They are unconditional rather than opt-in because neither costs anything on the path that\n * does not need it: the SSRF guard steps aside when nothing re-pointed the request, and the\n * auth filter does nothing for a `@WpAuthPublic` endpoint. An app therefore cannot forget to install\n * the guard on the one client that takes runtime URLs — the ACT of re-pointing is what arms it.\n */\n protected override clientFilters(): ClientFilterDefinition[] {\n return [\n new ClientFilterDefinition(\n SSRF_GUARD_PRIORITY,\n new SsrfGuardFilter(this.ssrfPolicy(), this.addressResolver),\n ),\n new ClientFilterDefinition(\n OUTBOUND_AUTH_PRIORITY,\n new OutboundAuthFilter(this.gcpOidc, this.secrets, this.webhookSigner),\n ),\n ];\n }\n\n /**\n * WHICH policy the guard applies when something does re-point this client.\n *\n * Read off an installed {@link ContextBaseUrlFilter} or {@link ContextFullUrlFilter}, because\n * that filter is where an app says\n * \"this client may be re-pointed\", and the single legitimate relaxation\n * ({@link SsrfTestingPolicy}) belongs at the same construction site as that decision rather\n * than in a second place a reader has to correlate. No such filter — or one built with the\n * default — means {@link SsrfPolicy} (the strict one), so the safe answer is what an app gets by saying\n * nothing.\n */\n private ssrfPolicy(): SsrfPolicy {\n for (const definition of this.appFilters) {\n const filter = definition.filter;\n if (filter instanceof ContextBaseUrlFilter || filter instanceof ContextFullUrlFilter) {\n return filter.ssrfPolicy;\n }\n }\n return new SsrfPolicy();\n }\n\n /**\n * Straight from the RequestContext. Throws when there is no active request scope.\n *\n * `destination` rides through unchanged: this is the ONE client that can legitimately propagate a\n * verified identity, and it does so exactly when the callee will authenticate us (@WpAuthOidc /\n * @WpAuthSharedSecret). Calling a peer's @WpAuthPublic or @WpAuthJwt endpoint now omits `x-user-id` and\n * friends instead of shipping headers that endpoint's AuthFilter is obliged to reject.\n */\n protected override outboundContextHeaders(destination: DestinationTrust): Map<string, string> {\n return this.headers.buildOutboundHeaders(destination);\n }\n\n /**\n * Straight into the ambient RequestContext — the reason a value a callee set climbs the call\n * tree: after this returns, the CALLER's context holds it, and the caller's own response (and its\n * own outbound calls) carry it onward.\n *\n * `destination` is the trust gate, and it is the mirror of the inbound problem rather than a\n * restatement of it: a response is another process's ASSERTION. Untrusted response keys are\n * admitted; a TRUSTED one is admitted only from a destination this client authenticated to, and\n * is otherwise dropped — see `RequestContextHeaders.acceptResponseHeaders`.\n */\n protected override acceptResponseContext(\n headers: Headers,\n destination: DestinationTrust,\n ): void {\n this.headers.acceptResponseHeaders(headers, destination);\n }\n\n /**\n * Test-case recording hook (mirror of Java HttpsJsonClientInvokeHandler): if a recorder is\n * travelling in the magic context, capture this outbound call + its result so it becomes a mock\n * in the generated test. Absent a recorder this is exactly the base behavior.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected override async execute(\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n // Preserve the context precondition before logging, which also requires an active scope.\n if (!RequestContext.isActive()) {\n throw new Error(\n 'No active RequestContext. Run the client call inside RequestContext.run(...).',\n );\n }\n const recorder = this.headers.findRecorder();\n if (!recorder) {\n return super.execute(route, requestDto, method);\n }\n return this.recordCall(recorder, route, requestDto, method);\n }\n\n /**\n * Execute the call while recording it (args + masked ctx snapshot + result).\n *\n * The snapshot is a FIXTURE field, not a log line, so it is built here rather than handed down\n * from the call path — a logging backend stamps its own fields and never sees this.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n private async recordCall(\n recorder: TestCaseRecorder,\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n const ctxSnapshot: Record<string, string> = {};\n for (const entry of RequestContext.buildLogFields().entries()) {\n ctxSnapshot[entry[0]] = entry[1];\n }\n const recorded = new RecordedEndpoint(\n this.contractName(),\n route.methodName,\n [requestDto],\n ctxSnapshot,\n );\n recorder.addEndpointInfo(recorded);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- capture failure into the recording, then rethrow unchanged\n try {\n const response = await super.execute(route, requestDto, method);\n recorded.successResponse = response;\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n recorded.failureResponse = new RecordedError(error.name, error.message);\n throw err;\n }\n }\n\n}\n\n/**\n * DI token for the `Provider<NodeProxyClient>` that hands out RPC clients — one per API contract.\n * `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because NodeProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would instead hand back one lazily-created\n * instance — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const NODE_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<NodeProxyClient>');\n"]}
1
+ {"version":3,"file":"NodeProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/NodeProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { once } from 'node:events';\nimport { request as httpRequest, IncomingMessage } from 'node:http';\nimport { request as httpsRequest } from 'node:https';\nimport { Readable } from 'node:stream';\nimport {\n ClientRegistry,\n DestinationTrust,\n ApiImplementationError,\n RecordedEndpoint,\n RecordedError,\n LogApiCallImpl,\n RouteMetadata,\n Secrets,\n SECRETS,\n TestCaseRecorder,\n toError,\n} from '@webpieces/core-util';\nimport {\n RequestContext,\n RequestContextApiCallContext,\n RequestContextHeaders,\n provideFrameworkTransient,\n} from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport {\n ApiPrototype,\n ByteReadableStream,\n ClientRequest,\n ClientFilterDefinition,\n ProxyClient,\n} from '@webpieces/http-client-core';\nimport { AddressResolver } from './AddressResolver';\nimport { ClientConfig } from './ClientConfig';\nimport { ContextBaseUrlFilter } from './ContextBaseUrlFilter';\nimport { ContextFullUrlFilter } from './ContextFullUrlFilter';\nimport { OutboundAuthFilter } from './OutboundAuthFilter';\nimport { SsrfGuardFilter } from './SsrfGuardFilter';\nimport { SsrfPolicy } from './SsrfPolicy';\nimport { WEBHOOK_SIGNER_CALLBACK, WebhookSignerCallback } from './WebhookSignerCallback';\n\n/**\n * The two framework built-ins' priorities, RELATIVE TO EACH OTHER and to nothing else — they are\n * ordered beneath every app filter structurally, not by number (see `ProxyClient.initRoutes`).\n *\n * The guard is OUTSIDE the minter deliberately: a destination that is going to be refused must be\n * refused BEFORE a credential is minted for it, so a hostile URL never causes a token to exist.\n */\nconst SSRF_GUARD_PRIORITY = 900;\nconst OUTBOUND_AUTH_PRIORITY = 800;\n\n/**\n * The server-side {@link ProxyClient}. Everything a browser cannot do lives here: reading the\n * ambient RequestContext, minting OIDC tokens, holding shared secrets, and recording test cases.\n *\n * TRANSIENT on purpose. Every `createRpcClient(api, config)` needs its own instance, because `init()`\n * binds one instance to exactly one API contract and one target. {@link ProxyClientProvider} hands\n * them out — see its doc.\n */\n@provideFrameworkTransient()\nexport class NodeProxyClient extends ProxyClient {\n protected override selectAuthMethod(route: import('@webpieces/core-util').RouteMetadata): import('@webpieces/core-util').AuthMode {\n const methods = route.authMeta?.methods;\n if (!methods) throw new Error('Endpoint requires @WpAuth or @WpAuthPublic.');\n const selected = methods.find((method: import('@webpieces/core-util').AuthMode) =>\n method.kind === 'oidc' || method.kind === 'public' ||\n (method.kind === 'shared-secret' && !!this.secrets?.get(method.secretKey)) ||\n (method.kind === 'webhook' && !!this.webhookSigner));\n if (selected) return selected;\n if (methods.length === 1) return methods[0];\n throw new Error('No configured Node client credential can satisfy this endpoint; bind a declared service credential.');\n }\n private config!: ClientConfig;\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(AddressResolver) private readonly addressResolver: AddressResolver,\n // @optional: only sharedSecret(...) endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(SECRETS) private readonly secrets?: Secrets,\n // @optional: only webhook(...) endpoints need it, and an unbound one makes them THROW\n // rather than deliver unsigned — see WebhookSignerCallback.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional()\n @inject(WEBHOOK_SIGNER_CALLBACK)\n private readonly webhookSigner?: WebhookSignerCallback,\n ) {\n // This package is node-only and already depends on core-context, so it builds the\n // RequestContext-backed ApiCallContext itself. No startup install, and therefore nothing a\n // non-webpieces host (plain NestJS/Express) can forget.\n super(new LogApiCallImpl(new RequestContextApiCallContext()));\n }\n\n /**\n * Bind this client to one API contract + target, with the app's outbound filters (url\n * rewriting, header editing, logging, per-call re-pointing).\n */\n init(\n apiPrototype: ApiPrototype<object>,\n config: ClientConfig,\n appFilters: ClientFilterDefinition[],\n ): void {\n this.config = config;\n this.initRoutes(apiPrototype, appFilters);\n }\n\n /**\n * The same chain every client runs — a ClientRegistry mapping, else the installed deriver — but\n * with NODE's fallback: THROW. A server has no \"own origin\" to fall back to the way a browser\n * does, so an unresolvable peer is a setup bug and must fail loudly (the error names the fixes).\n *\n * Resolved per call, never at construction, so building a client stays synchronous. Any metadata\n * read beneath a deriver is memoized process-wide, so only the first call pays.\n */\n protected override resolveBaseUrl(): Promise<string> {\n return ClientRegistry.resolve(this.config.svcName);\n }\n\n /** Node/undici supports a live ReadableStream body when fetch is given `duplex: 'half'`. */\n protected override supportsConcurrentDuplexFetch(): boolean {\n return true;\n }\n\n /**\n * Node's native HTTP stream is genuinely concurrent: unlike browser Fetch, an early response\n * does not cancel the still-open upload. The returned Web Response keeps parsing isomorphic.\n */\n protected override sendStreamingTransport(\n request: ClientRequest,\n signal: AbortSignal,\n body: ByteReadableStream,\n ): Promise<Response> {\n return new Promise<Response>(\n (resolve: (response: Response) => void, reject: (error: Error) => void) => {\n const url = new URL(request.url);\n const send = url.protocol === 'https:' ? httpsRequest : httpRequest;\n const outgoing = send(\n url,\n {\n method: 'POST',\n headers: request.headersAsRecord(),\n signal,\n },\n (incoming: IncomingMessage): void => {\n const headers = new Headers();\n for (const [name, value] of Object.entries(incoming.headers)) {\n if (Array.isArray(value)) {\n for (const item of value) headers.append(name, item);\n } else if (value !== undefined) {\n headers.set(name, value);\n }\n }\n const responseBody = Readable.toWeb(incoming) as ReadableStream<Uint8Array>;\n resolve(\n new Response(responseBody, {\n status: incoming.statusCode ?? 500,\n statusText: incoming.statusMessage,\n headers,\n }),\n );\n },\n );\n outgoing.once('error', reject);\n // webpieces-disable no-any-unknown -- a native socket/write rejection is normalized by toError\n void this.pumpStreamingRequest(body, outgoing).catch((err: unknown) => {\n const error = toError(err);\n outgoing.destroy(error);\n });\n },\n );\n }\n\n private async pumpStreamingRequest(\n body: ByteReadableStream,\n outgoing: import('node:http').ClientRequest,\n ): Promise<void> {\n const reader = body.getReader();\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- socket errors reject writer acknowledgements\n try {\n for (;;) {\n const part = await reader.read();\n if (part.done) break;\n if (!outgoing.write(part.value)) await once(outgoing, 'drain');\n }\n outgoing.end();\n } finally {\n reader.releaseLock();\n }\n }\n\n /**\n * The two framework built-ins, installed on EVERY client this package builds and ordered\n * beneath every app filter by {@link ProxyClient.initRoutes}.\n *\n * They are unconditional rather than opt-in because neither costs anything on the path that\n * does not need it: the SSRF guard steps aside when nothing re-pointed the request, and the\n * auth filter does nothing for a `@WpAuthPublic` endpoint. An app therefore cannot forget to install\n * the guard on the one client that takes runtime URLs — the ACT of re-pointing is what arms it.\n */\n protected override clientFilters(): ClientFilterDefinition[] {\n return [\n new ClientFilterDefinition(\n SSRF_GUARD_PRIORITY,\n new SsrfGuardFilter(this.ssrfPolicy(), this.addressResolver),\n ),\n new ClientFilterDefinition(\n OUTBOUND_AUTH_PRIORITY,\n new OutboundAuthFilter(this.gcpOidc, this.secrets, this.webhookSigner),\n ),\n ];\n }\n\n /**\n * WHICH policy the guard applies when something does re-point this client.\n *\n * Read off an installed {@link ContextBaseUrlFilter} or {@link ContextFullUrlFilter}, because\n * that filter is where an app says\n * \"this client may be re-pointed\", and the single legitimate relaxation\n * ({@link SsrfTestingPolicy}) belongs at the same construction site as that decision rather\n * than in a second place a reader has to correlate. No such filter — or one built with the\n * default — means {@link SsrfPolicy} (the strict one), so the safe answer is what an app gets by saying\n * nothing.\n */\n private ssrfPolicy(): SsrfPolicy {\n for (const definition of this.appFilters) {\n const filter = definition.filter;\n if (filter instanceof ContextBaseUrlFilter || filter instanceof ContextFullUrlFilter) {\n return filter.ssrfPolicy;\n }\n }\n return new SsrfPolicy();\n }\n\n /**\n * Straight from the RequestContext. Throws when there is no active request scope.\n *\n * `destination` rides through unchanged: this is the ONE client that can legitimately propagate a\n * verified identity, and it does so exactly when the callee will authenticate us (oidc(...) /\n * sharedSecret(...)). Calling a peer's @WpAuthPublic or jwt() endpoint now omits `x-user-id` and\n * friends instead of shipping headers that endpoint's AuthFilter is obliged to reject.\n */\n protected override outboundContextHeaders(destination: DestinationTrust): Map<string, string> {\n return this.headers.buildOutboundHeaders(destination);\n }\n\n /**\n * Straight into the ambient RequestContext — the reason a value a callee set climbs the call\n * tree: after this returns, the CALLER's context holds it, and the caller's own response (and its\n * own outbound calls) carry it onward.\n *\n * `destination` is the trust gate, and it is the mirror of the inbound problem rather than a\n * restatement of it: a response is another process's ASSERTION. Untrusted response keys are\n * admitted; a TRUSTED one is admitted only from a destination this client authenticated to, and\n * is otherwise dropped — see `RequestContextHeaders.acceptResponseHeaders`.\n */\n protected override acceptResponseContext(\n headers: Headers,\n destination: DestinationTrust,\n ): void {\n this.headers.acceptResponseHeaders(headers, destination);\n }\n\n /**\n * Test-case recording hook (mirror of Java HttpsJsonClientInvokeHandler): if a recorder is\n * travelling in the magic context, capture this outbound call + its result so it becomes a mock\n * in the generated test. Absent a recorder this is exactly the base behavior.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected override async execute(\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n // Preserve the context precondition before logging, which also requires an active scope.\n if (!RequestContext.isActive()) {\n throw new Error(\n 'No active RequestContext. Run the client call inside RequestContext.run(...).',\n );\n }\n const recorder = this.headers.findRecorder();\n if (!recorder) {\n return super.execute(route, requestDto, method);\n }\n return this.recordCall(recorder, route, requestDto, method);\n }\n\n /**\n * Execute the call while recording it (args + masked ctx snapshot + result).\n *\n * The snapshot is a FIXTURE field, not a log line, so it is built here rather than handed down\n * from the call path — a logging backend stamps its own fields and never sees this.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n private async recordCall(\n recorder: TestCaseRecorder,\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n const ctxSnapshot: Record<string, string> = {};\n for (const entry of RequestContext.buildLogFields().entries()) {\n ctxSnapshot[entry[0]] = entry[1];\n }\n const recorded = new RecordedEndpoint(\n this.contractName(),\n route.methodName,\n [requestDto],\n ctxSnapshot,\n );\n recorder.addEndpointInfo(recorded);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- capture failure into the recording, then rethrow unchanged\n try {\n const response = await super.execute(route, requestDto, method);\n recorded.successResponse = response;\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n recorded.failureResponse = new RecordedError(error.name, error.message);\n throw err;\n }\n }\n\n}\n\n/**\n * DI token for the `Provider<NodeProxyClient>` that hands out RPC clients — one per API contract.\n * `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because NodeProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would instead hand back one lazily-created\n * instance — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const NODE_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<NodeProxyClient>');\n"]}
@@ -9,34 +9,34 @@
9
9
  * auth mode requires is simply absent.
10
10
  */
11
11
  /**
12
- * An `@WpAuthSharedSecret(key)` endpoint was called by a client whose bound `Secrets` holds no value
12
+ * An `sharedSecret(key)` endpoint was called by a client whose bound `Secrets` holds no value
13
13
  * for that key. Thrown at CALL time, from `OutboundAuthFilter`.
14
14
  */
15
15
  export declare class MissingSharedSecretError extends Error {
16
16
  /** `Contract.method`, so the log line names the call without a stack read. */
17
17
  readonly endpoint: string;
18
- /** The `@WpAuthSharedSecret` key that had no value, so the fix names itself. */
18
+ /** The `sharedSecret(...)` key that had no value, so the fix names itself. */
19
19
  readonly secretKey: string;
20
20
  constructor(message: string,
21
21
  /** `Contract.method`, so the log line names the call without a stack read. */
22
22
  endpoint: string,
23
- /** The `@WpAuthSharedSecret` key that had no value, so the fix names itself. */
23
+ /** The `sharedSecret(...)` key that had no value, so the fix names itself. */
24
24
  secretKey: string);
25
25
  }
26
26
  /**
27
- * An `@WpAuthWebhook(name)` endpoint was called outbound with no `WebhookSignerCallback` bound, so
27
+ * An `webhook(name)` endpoint was called outbound with no `WebhookSignerCallback` bound, so
28
28
  * nothing can produce the signature the partner verifies. Thrown at CALL time, from
29
29
  * `OutboundAuthFilter`, rather than delivering unsigned — the mirror of the inbound side, where an
30
- * unbound `WebhookAuthCallback` 401s every `@WpAuthWebhook` endpoint instead of admitting it.
30
+ * unbound `WebhookAuthCallback` 401s every `webhook(...)` endpoint instead of admitting it.
31
31
  */
32
32
  export declare class MissingWebhookSignerError extends Error {
33
33
  /** `Contract.method`, so the log line names the call without a stack read. */
34
34
  readonly endpoint: string;
35
- /** The vendor on the contract's `@WpAuthWebhook(name)`, which selects the scheme. */
35
+ /** The vendor on the contract's `webhook(name)`, which selects the scheme. */
36
36
  readonly webhookName: string;
37
37
  constructor(message: string,
38
38
  /** `Contract.method`, so the log line names the call without a stack read. */
39
39
  endpoint: string,
40
- /** The vendor on the contract's `@WpAuthWebhook(name)`, which selects the scheme. */
40
+ /** The vendor on the contract's `webhook(name)`, which selects the scheme. */
41
41
  webhookName: string);
42
42
  }
@@ -12,7 +12,7 @@
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.MissingWebhookSignerError = exports.MissingSharedSecretError = void 0;
14
14
  /**
15
- * An `@WpAuthSharedSecret(key)` endpoint was called by a client whose bound `Secrets` holds no value
15
+ * An `sharedSecret(key)` endpoint was called by a client whose bound `Secrets` holds no value
16
16
  * for that key. Thrown at CALL time, from `OutboundAuthFilter`.
17
17
  */
18
18
  class MissingSharedSecretError extends Error {
@@ -21,7 +21,7 @@ class MissingSharedSecretError extends Error {
21
21
  constructor(message,
22
22
  /** `Contract.method`, so the log line names the call without a stack read. */
23
23
  endpoint,
24
- /** The `@WpAuthSharedSecret` key that had no value, so the fix names itself. */
24
+ /** The `sharedSecret(...)` key that had no value, so the fix names itself. */
25
25
  secretKey) {
26
26
  super(message);
27
27
  this.endpoint = endpoint;
@@ -31,10 +31,10 @@ class MissingSharedSecretError extends Error {
31
31
  }
32
32
  exports.MissingSharedSecretError = MissingSharedSecretError;
33
33
  /**
34
- * An `@WpAuthWebhook(name)` endpoint was called outbound with no `WebhookSignerCallback` bound, so
34
+ * An `webhook(name)` endpoint was called outbound with no `WebhookSignerCallback` bound, so
35
35
  * nothing can produce the signature the partner verifies. Thrown at CALL time, from
36
36
  * `OutboundAuthFilter`, rather than delivering unsigned — the mirror of the inbound side, where an
37
- * unbound `WebhookAuthCallback` 401s every `@WpAuthWebhook` endpoint instead of admitting it.
37
+ * unbound `WebhookAuthCallback` 401s every `webhook(...)` endpoint instead of admitting it.
38
38
  */
39
39
  class MissingWebhookSignerError extends Error {
40
40
  endpoint;
@@ -42,7 +42,7 @@ class MissingWebhookSignerError extends Error {
42
42
  constructor(message,
43
43
  /** `Contract.method`, so the log line names the call without a stack read. */
44
44
  endpoint,
45
- /** The vendor on the contract's `@WpAuthWebhook(name)`, which selects the scheme. */
45
+ /** The vendor on the contract's `webhook(name)`, which selects the scheme. */
46
46
  webhookName) {
47
47
  super(message);
48
48
  this.endpoint = endpoint;
@@ -1 +1 @@
1
- {"version":3,"file":"OutboundAuthErrors.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/OutboundAuthErrors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAEH;;;GAGG;AACH,MAAa,wBAAyB,SAAQ,KAAK;IAI3B;IAEA;IALpB,YACI,OAAe;IACf,8EAA8E;IAC9D,QAAgB;IAChC,gFAAgF;IAChE,SAAiB;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,cAAS,GAAT,SAAS,CAAQ;QAGjC,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IAC3C,CAAC;CACJ;AAXD,4DAWC;AAED;;;;;GAKG;AACH,MAAa,yBAA0B,SAAQ,KAAK;IAI5B;IAEA;IALpB,YACI,OAAe;IACf,8EAA8E;IAC9D,QAAgB;IAChC,qFAAqF;IACrE,WAAmB;QAEnC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,gBAAW,GAAX,WAAW,CAAQ;QAGnC,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC5C,CAAC;CACJ;AAXD,8DAWC","sourcesContent":["/**\n * The two ways outbound auth refuses to send, each its own type for the reason\n * {@link MissingRuntimeBaseUrlError} is one: a delivery worker has to tell these apart from\n * {@link SsrfRefusedError}. Both mean THIS SERVICE is misconfigured — page somebody, retrying is\n * pointless — where an SSRF refusal means a partner registered something hostile and the delivery\n * should be dead-lettered. A bare `Error` forces that decision to be made by matching message text.\n *\n * Neither is thrown for anything a caller can influence: they fire when the binding a contract's\n * auth mode requires is simply absent.\n */\n\n/**\n * An `@WpAuthSharedSecret(key)` endpoint was called by a client whose bound `Secrets` holds no value\n * for that key. Thrown at CALL time, from `OutboundAuthFilter`.\n */\nexport class MissingSharedSecretError extends Error {\n constructor(\n message: string,\n /** `Contract.method`, so the log line names the call without a stack read. */\n public readonly endpoint: string,\n /** The `@WpAuthSharedSecret` key that had no value, so the fix names itself. */\n public readonly secretKey: string,\n ) {\n super(message);\n this.name = 'MissingSharedSecretError';\n }\n}\n\n/**\n * An `@WpAuthWebhook(name)` endpoint was called outbound with no `WebhookSignerCallback` bound, so\n * nothing can produce the signature the partner verifies. Thrown at CALL time, from\n * `OutboundAuthFilter`, rather than delivering unsigned — the mirror of the inbound side, where an\n * unbound `WebhookAuthCallback` 401s every `@WpAuthWebhook` endpoint instead of admitting it.\n */\nexport class MissingWebhookSignerError extends Error {\n constructor(\n message: string,\n /** `Contract.method`, so the log line names the call without a stack read. */\n public readonly endpoint: string,\n /** The vendor on the contract's `@WpAuthWebhook(name)`, which selects the scheme. */\n public readonly webhookName: string,\n ) {\n super(message);\n this.name = 'MissingWebhookSignerError';\n }\n}\n"]}
1
+ {"version":3,"file":"OutboundAuthErrors.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/OutboundAuthErrors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAEH;;;GAGG;AACH,MAAa,wBAAyB,SAAQ,KAAK;IAI3B;IAEA;IALpB,YACI,OAAe;IACf,8EAA8E;IAC9D,QAAgB;IAChC,8EAA8E;IAC9D,SAAiB;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,cAAS,GAAT,SAAS,CAAQ;QAGjC,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IAC3C,CAAC;CACJ;AAXD,4DAWC;AAED;;;;;GAKG;AACH,MAAa,yBAA0B,SAAQ,KAAK;IAI5B;IAEA;IALpB,YACI,OAAe;IACf,8EAA8E;IAC9D,QAAgB;IAChC,8EAA8E;IAC9D,WAAmB;QAEnC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,gBAAW,GAAX,WAAW,CAAQ;QAGnC,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC5C,CAAC;CACJ;AAXD,8DAWC","sourcesContent":["/**\n * The two ways outbound auth refuses to send, each its own type for the reason\n * {@link MissingRuntimeBaseUrlError} is one: a delivery worker has to tell these apart from\n * {@link SsrfRefusedError}. Both mean THIS SERVICE is misconfigured — page somebody, retrying is\n * pointless — where an SSRF refusal means a partner registered something hostile and the delivery\n * should be dead-lettered. A bare `Error` forces that decision to be made by matching message text.\n *\n * Neither is thrown for anything a caller can influence: they fire when the binding a contract's\n * auth mode requires is simply absent.\n */\n\n/**\n * An `sharedSecret(key)` endpoint was called by a client whose bound `Secrets` holds no value\n * for that key. Thrown at CALL time, from `OutboundAuthFilter`.\n */\nexport class MissingSharedSecretError extends Error {\n constructor(\n message: string,\n /** `Contract.method`, so the log line names the call without a stack read. */\n public readonly endpoint: string,\n /** The `sharedSecret(...)` key that had no value, so the fix names itself. */\n public readonly secretKey: string,\n ) {\n super(message);\n this.name = 'MissingSharedSecretError';\n }\n}\n\n/**\n * An `webhook(name)` endpoint was called outbound with no `WebhookSignerCallback` bound, so\n * nothing can produce the signature the partner verifies. Thrown at CALL time, from\n * `OutboundAuthFilter`, rather than delivering unsigned — the mirror of the inbound side, where an\n * unbound `WebhookAuthCallback` 401s every `webhook(...)` endpoint instead of admitting it.\n */\nexport class MissingWebhookSignerError extends Error {\n constructor(\n message: string,\n /** `Contract.method`, so the log line names the call without a stack read. */\n public readonly endpoint: string,\n /** The vendor on the contract's `webhook(name)`, which selects the scheme. */\n public readonly webhookName: string,\n ) {\n super(message);\n this.name = 'MissingWebhookSignerError';\n }\n}\n"]}
@@ -22,11 +22,11 @@ import { WebhookSignerCallback } from './WebhookSignerCallback';
22
22
  *
23
23
  * ## The three modes, and why none of them is restricted to a fixed host
24
24
  *
25
- * - `@WpAuthOidc` → a bearer token minted as this caller's runtime SA, audience = the final base URL.
26
- * - `@WpAuthSharedSecret` → the value this client holds for that key, as `Authorization: Webpieces …`.
25
+ * - `oidc(...)` → a bearer token minted as this caller's runtime SA, audience = the final base URL.
26
+ * - `sharedSecret(...)` → the value this client holds for that key, as `Authorization: Webpieces …`.
27
27
  * Legitimate against a re-pointed URL: N services implementing ONE contract behind ONE agreed
28
28
  * secret is a real and common topology, and often stands in for OIDC where OIDC is not available.
29
- * - `@WpAuthWebhook(name)` → the app's bound {@link WebhookSignerCallback} produces the headers. WE
29
+ * - `webhook(name)` → the app's bound {@link WebhookSignerCallback} produces the headers. WE
30
30
  * are the vendor on this side; see that class.
31
31
  *
32
32
  * Both credential-minting modes ride in the ONE `Authorization` header under their own scheme —
@@ -35,21 +35,21 @@ import { WebhookSignerCallback } from './WebhookSignerCallback';
35
35
  */
36
36
  export declare class OutboundAuthFilter extends Filter<ClientRequest, Response> {
37
37
  private readonly gcpOidc;
38
- /** Only @WpAuthSharedSecret endpoints need it; a server that has none binds nothing. */
38
+ /** Only sharedSecret(...) endpoints need it; a server that has none binds nothing. */
39
39
  private readonly secrets;
40
- /** Only @WpAuthWebhook endpoints need it, and an unbound one makes them THROW. */
40
+ /** Only webhook(...) endpoints need it, and an unbound one makes them THROW. */
41
41
  private readonly webhookSigner;
42
42
  constructor(gcpOidc: GcpOidc,
43
- /** Only @WpAuthSharedSecret endpoints need it; a server that has none binds nothing. */
43
+ /** Only sharedSecret(...) endpoints need it; a server that has none binds nothing. */
44
44
  secrets: Secrets | undefined,
45
- /** Only @WpAuthWebhook endpoints need it, and an unbound one makes them THROW. */
45
+ /** Only webhook(...) endpoints need it, and an unbound one makes them THROW. */
46
46
  webhookSigner: WebhookSignerCallback | undefined);
47
47
  filter(request: ClientRequest, nextFilter: Service<ClientRequest, Response>): Promise<Response>;
48
48
  private attach;
49
49
  /**
50
50
  * @throws MissingWebhookSignerError when no {@link WebhookSignerCallback} is bound. FAIL CLOSED,
51
51
  * matching the inbound side exactly: an unbound `WebhookAuthCallback` 401s every
52
- * `@WpAuthWebhook` endpoint rather than admitting it unverified, so an unbound signer must
52
+ * `webhook(...)` endpoint rather than admitting it unverified, so an unbound signer must
53
53
  * refuse to send rather than deliver something the partner is obliged to reject.
54
54
  */
55
55
  private signWebhook;
@@ -24,11 +24,11 @@ const WebhookSignerCallback_1 = require("./WebhookSignerCallback");
24
24
  *
25
25
  * ## The three modes, and why none of them is restricted to a fixed host
26
26
  *
27
- * - `@WpAuthOidc` → a bearer token minted as this caller's runtime SA, audience = the final base URL.
28
- * - `@WpAuthSharedSecret` → the value this client holds for that key, as `Authorization: Webpieces …`.
27
+ * - `oidc(...)` → a bearer token minted as this caller's runtime SA, audience = the final base URL.
28
+ * - `sharedSecret(...)` → the value this client holds for that key, as `Authorization: Webpieces …`.
29
29
  * Legitimate against a re-pointed URL: N services implementing ONE contract behind ONE agreed
30
30
  * secret is a real and common topology, and often stands in for OIDC where OIDC is not available.
31
- * - `@WpAuthWebhook(name)` → the app's bound {@link WebhookSignerCallback} produces the headers. WE
31
+ * - `webhook(name)` → the app's bound {@link WebhookSignerCallback} produces the headers. WE
32
32
  * are the vendor on this side; see that class.
33
33
  *
34
34
  * Both credential-minting modes ride in the ONE `Authorization` header under their own scheme —
@@ -40,9 +40,9 @@ class OutboundAuthFilter extends core_util_1.Filter {
40
40
  secrets;
41
41
  webhookSigner;
42
42
  constructor(gcpOidc,
43
- /** Only @WpAuthSharedSecret endpoints need it; a server that has none binds nothing. */
43
+ /** Only sharedSecret(...) endpoints need it; a server that has none binds nothing. */
44
44
  secrets,
45
- /** Only @WpAuthWebhook endpoints need it, and an unbound one makes them THROW. */
45
+ /** Only webhook(...) endpoints need it, and an unbound one makes them THROW. */
46
46
  webhookSigner) {
47
47
  super();
48
48
  this.gcpOidc = gcpOidc;
@@ -54,7 +54,7 @@ class OutboundAuthFilter extends core_util_1.Filter {
54
54
  return nextFilter.invoke(request);
55
55
  }
56
56
  async attach(request) {
57
- const mode = request.route.authMeta?.mode;
57
+ const mode = request.route.authMeta?.methods[0];
58
58
  if (mode?.kind === 'oidc') {
59
59
  request.headers.set('Authorization', `Bearer ${await this.gcpOidc.mintIdToken(request.baseUrl)}`);
60
60
  return;
@@ -63,7 +63,7 @@ class OutboundAuthFilter extends core_util_1.Filter {
63
63
  const secret = this.secrets?.get(mode.secretKey);
64
64
  if (!secret) {
65
65
  throw new OutboundAuthErrors_1.MissingSharedSecretError(`${request.contractName}.${request.route.methodName} is ` +
66
- `@WpAuthSharedSecret('${mode.secretKey}'), but this client's bound Secrets holds no ` +
66
+ `sharedSecret('${mode.secretKey}'), but this client's bound Secrets holds no ` +
67
67
  `value for that key, so there is no credential to send. Bind a Secrets carrying ` +
68
68
  `'${mode.secretKey}'. Refusing to send is deliberate: the callee is obliged to 401 an ` +
69
69
  `unauthenticated request, so sending it would report as the peer's failure.`, `${request.contractName}.${request.route.methodName}`, mode.secretKey);
@@ -80,12 +80,12 @@ class OutboundAuthFilter extends core_util_1.Filter {
80
80
  /**
81
81
  * @throws MissingWebhookSignerError when no {@link WebhookSignerCallback} is bound. FAIL CLOSED,
82
82
  * matching the inbound side exactly: an unbound `WebhookAuthCallback` 401s every
83
- * `@WpAuthWebhook` endpoint rather than admitting it unverified, so an unbound signer must
83
+ * `webhook(...)` endpoint rather than admitting it unverified, so an unbound signer must
84
84
  * refuse to send rather than deliver something the partner is obliged to reject.
85
85
  */
86
86
  async signWebhook(request, name) {
87
87
  if (this.webhookSigner === undefined) {
88
- throw new OutboundAuthErrors_1.MissingWebhookSignerError(`${request.contractName}.${request.route.methodName} is @WpAuthWebhook('${name}'), so this ` +
88
+ throw new OutboundAuthErrors_1.MissingWebhookSignerError(`${request.contractName}.${request.route.methodName} is webhook('${name}'), so this ` +
89
89
  `client must SIGN the request the way ${name} verifies it — but no WebhookSignerCallback ` +
90
90
  `is bound, so there is nothing to produce the signature. Bind one:\n` +
91
91
  ` options.bind(WEBHOOK_SIGNER_CALLBACK).to(MyWebhookSigner);\n` +
@@ -1 +1 @@
1
- {"version":3,"file":"OutboundAuthFilter.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/OutboundAuthFilter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Filter, Secrets, Service } from '@webpieces/core-util';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport { ClientRequest } from '@webpieces/http-client-core';\nimport { MissingSharedSecretError, MissingWebhookSignerError } from './OutboundAuthErrors';\nimport { SignableRequest, WebhookSignerCallback } from './WebhookSignerCallback';\n\n/**\n * Attaches the endpoint's outbound credential, against the destination the call is ACTUALLY going\n * to.\n *\n * ## Why this is a filter, and why it is the LAST one\n *\n * It used to be a method on the client, called before the request object even existed — so it\n * minted against the URL the client resolved at bind time, which is the URL BEFORE any filter had\n * run. That is wrong the moment a filter can re-point the request: an OIDC token's audience is the\n * callee's base URL, and a token minted for our own service name and then sent to a partner's\n * server is a credential handed to the wrong party.\n *\n * As the innermost filter (`ProxyClient.initRoutes` puts the framework's built-ins beneath every app\n * filter, and no app priority can get under them) it reads `request.baseUrl` / `request.url` after\n * everything has settled, which makes the audience correct by construction rather than by\n * convention. The SSRF guard sits immediately ABOVE it, so a destination that is going to be refused\n * is refused BEFORE any credential is minted for it.\n *\n * ## The three modes, and why none of them is restricted to a fixed host\n *\n * - `@WpAuthOidc` → a bearer token minted as this caller's runtime SA, audience = the final base URL.\n * - `@WpAuthSharedSecret` → the value this client holds for that key, as `Authorization: Webpieces …`.\n * Legitimate against a re-pointed URL: N services implementing ONE contract behind ONE agreed\n * secret is a real and common topology, and often stands in for OIDC where OIDC is not available.\n * - `@WpAuthWebhook(name)` → the app's bound {@link WebhookSignerCallback} produces the headers. WE\n * are the vendor on this side; see that class.\n *\n * Both credential-minting modes ride in the ONE `Authorization` header under their own scheme —\n * `Bearer <oidc>` / `Webpieces <secret>` — which is never a context key, so neither can leak onto\n * the next hop.\n */\nexport class OutboundAuthFilter extends Filter<ClientRequest, Response> {\n constructor(\n private readonly gcpOidc: GcpOidc,\n /** Only @WpAuthSharedSecret endpoints need it; a server that has none binds nothing. */\n private readonly secrets: Secrets | undefined,\n /** Only @WpAuthWebhook endpoints need it, and an unbound one makes them THROW. */\n private readonly webhookSigner: WebhookSignerCallback | undefined,\n ) {\n super();\n }\n\n override async filter(\n request: ClientRequest,\n nextFilter: Service<ClientRequest, Response>,\n ): Promise<Response> {\n await this.attach(request);\n return nextFilter.invoke(request);\n }\n\n private async attach(request: ClientRequest): Promise<void> {\n const mode = request.route.authMeta?.mode;\n if (mode?.kind === 'oidc') {\n request.headers.set(\n 'Authorization',\n `Bearer ${await this.gcpOidc.mintIdToken(request.baseUrl)}`,\n );\n return;\n }\n if (mode?.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (!secret) {\n throw new MissingSharedSecretError(\n `${request.contractName}.${request.route.methodName} is ` +\n `@WpAuthSharedSecret('${mode.secretKey}'), but this client's bound Secrets holds no ` +\n `value for that key, so there is no credential to send. Bind a Secrets carrying ` +\n `'${mode.secretKey}'. Refusing to send is deliberate: the callee is obliged to 401 an ` +\n `unauthenticated request, so sending it would report as the peer's failure.`,\n `${request.contractName}.${request.route.methodName}`,\n mode.secretKey,\n );\n }\n // Same header as a JWT/OIDC token, but its OWN scheme, so a secret can never be\n // mistaken for a token nor accepted where one was expected.\n request.headers.set('Authorization', `Webpieces ${secret}`);\n return;\n }\n if (mode?.kind === 'webhook') {\n await this.signWebhook(request, mode.name);\n }\n }\n\n /**\n * @throws MissingWebhookSignerError when no {@link WebhookSignerCallback} is bound. FAIL CLOSED,\n * matching the inbound side exactly: an unbound `WebhookAuthCallback` 401s every\n * `@WpAuthWebhook` endpoint rather than admitting it unverified, so an unbound signer must\n * refuse to send rather than deliver something the partner is obliged to reject.\n */\n private async signWebhook(request: ClientRequest, name: string): Promise<void> {\n if (this.webhookSigner === undefined) {\n throw new MissingWebhookSignerError(\n `${request.contractName}.${request.route.methodName} is @WpAuthWebhook('${name}'), so this ` +\n `client must SIGN the request the way ${name} verifies it — but no WebhookSignerCallback ` +\n `is bound, so there is nothing to produce the signature. Bind one:\\n` +\n ` options.bind(WEBHOOK_SIGNER_CALLBACK).to(MyWebhookSigner);\\n` +\n `Refusing to send is deliberate: an unsigned delivery is one the partner will reject, ` +\n `and sending it anyway would hide the missing binding until they complained.`,\n `${request.contractName}.${request.route.methodName}`,\n name,\n );\n }\n const signable = new SignableRequest(\n request.url,\n request.route.httpMethod,\n request.body,\n request.headers,\n request.contractName,\n request.route.methodName,\n );\n const signed = await this.webhookSigner.sign(name, signable);\n for (const entry of signed.entries()) {\n request.headers.set(entry[0], entry[1]);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"OutboundAuthFilter.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/OutboundAuthFilter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Filter, Secrets, Service } from '@webpieces/core-util';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport { ClientRequest } from '@webpieces/http-client-core';\nimport { MissingSharedSecretError, MissingWebhookSignerError } from './OutboundAuthErrors';\nimport { SignableRequest, WebhookSignerCallback } from './WebhookSignerCallback';\n\n/**\n * Attaches the endpoint's outbound credential, against the destination the call is ACTUALLY going\n * to.\n *\n * ## Why this is a filter, and why it is the LAST one\n *\n * It used to be a method on the client, called before the request object even existed — so it\n * minted against the URL the client resolved at bind time, which is the URL BEFORE any filter had\n * run. That is wrong the moment a filter can re-point the request: an OIDC token's audience is the\n * callee's base URL, and a token minted for our own service name and then sent to a partner's\n * server is a credential handed to the wrong party.\n *\n * As the innermost filter (`ProxyClient.initRoutes` puts the framework's built-ins beneath every app\n * filter, and no app priority can get under them) it reads `request.baseUrl` / `request.url` after\n * everything has settled, which makes the audience correct by construction rather than by\n * convention. The SSRF guard sits immediately ABOVE it, so a destination that is going to be refused\n * is refused BEFORE any credential is minted for it.\n *\n * ## The three modes, and why none of them is restricted to a fixed host\n *\n * - `oidc(...)` → a bearer token minted as this caller's runtime SA, audience = the final base URL.\n * - `sharedSecret(...)` → the value this client holds for that key, as `Authorization: Webpieces …`.\n * Legitimate against a re-pointed URL: N services implementing ONE contract behind ONE agreed\n * secret is a real and common topology, and often stands in for OIDC where OIDC is not available.\n * - `webhook(name)` → the app's bound {@link WebhookSignerCallback} produces the headers. WE\n * are the vendor on this side; see that class.\n *\n * Both credential-minting modes ride in the ONE `Authorization` header under their own scheme —\n * `Bearer <oidc>` / `Webpieces <secret>` — which is never a context key, so neither can leak onto\n * the next hop.\n */\nexport class OutboundAuthFilter extends Filter<ClientRequest, Response> {\n constructor(\n private readonly gcpOidc: GcpOidc,\n /** Only sharedSecret(...) endpoints need it; a server that has none binds nothing. */\n private readonly secrets: Secrets | undefined,\n /** Only webhook(...) endpoints need it, and an unbound one makes them THROW. */\n private readonly webhookSigner: WebhookSignerCallback | undefined,\n ) {\n super();\n }\n\n override async filter(\n request: ClientRequest,\n nextFilter: Service<ClientRequest, Response>,\n ): Promise<Response> {\n await this.attach(request);\n return nextFilter.invoke(request);\n }\n\n private async attach(request: ClientRequest): Promise<void> {\n const mode = request.route.authMeta?.methods[0];\n if (mode?.kind === 'oidc') {\n request.headers.set(\n 'Authorization',\n `Bearer ${await this.gcpOidc.mintIdToken(request.baseUrl)}`,\n );\n return;\n }\n if (mode?.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (!secret) {\n throw new MissingSharedSecretError(\n `${request.contractName}.${request.route.methodName} is ` +\n `sharedSecret('${mode.secretKey}'), but this client's bound Secrets holds no ` +\n `value for that key, so there is no credential to send. Bind a Secrets carrying ` +\n `'${mode.secretKey}'. Refusing to send is deliberate: the callee is obliged to 401 an ` +\n `unauthenticated request, so sending it would report as the peer's failure.`,\n `${request.contractName}.${request.route.methodName}`,\n mode.secretKey,\n );\n }\n // Same header as a JWT/OIDC token, but its OWN scheme, so a secret can never be\n // mistaken for a token nor accepted where one was expected.\n request.headers.set('Authorization', `Webpieces ${secret}`);\n return;\n }\n if (mode?.kind === 'webhook') {\n await this.signWebhook(request, mode.name);\n }\n }\n\n /**\n * @throws MissingWebhookSignerError when no {@link WebhookSignerCallback} is bound. FAIL CLOSED,\n * matching the inbound side exactly: an unbound `WebhookAuthCallback` 401s every\n * `webhook(...)` endpoint rather than admitting it unverified, so an unbound signer must\n * refuse to send rather than deliver something the partner is obliged to reject.\n */\n private async signWebhook(request: ClientRequest, name: string): Promise<void> {\n if (this.webhookSigner === undefined) {\n throw new MissingWebhookSignerError(\n `${request.contractName}.${request.route.methodName} is webhook('${name}'), so this ` +\n `client must SIGN the request the way ${name} verifies it — but no WebhookSignerCallback ` +\n `is bound, so there is nothing to produce the signature. Bind one:\\n` +\n ` options.bind(WEBHOOK_SIGNER_CALLBACK).to(MyWebhookSigner);\\n` +\n `Refusing to send is deliberate: an unsigned delivery is one the partner will reject, ` +\n `and sending it anyway would hide the missing binding until they complained.`,\n `${request.contractName}.${request.route.methodName}`,\n name,\n );\n }\n const signable = new SignableRequest(\n request.url,\n request.route.httpMethod,\n request.body,\n request.headers,\n request.contractName,\n request.route.methodName,\n );\n const signed = await this.webhookSigner.sign(name, signable);\n for (const entry of signed.entries()) {\n request.headers.set(entry[0], entry[1]);\n }\n }\n}\n"]}
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The OUTBOUND half of `@WpAuthWebhook(name)` — the exact mirror of `WebhookAuthCallback`, which is
2
+ * The OUTBOUND half of `webhook(name)` — the exact mirror of `WebhookAuthCallback`, which is
3
3
  * the INBOUND half.
4
4
  *
5
- * `@WpAuthWebhook('partner-hmac')` on a contract says: this hop is authenticated by a signature over
5
+ * `webhook('partner-hmac')` on a contract says: this hop is authenticated by a signature over
6
6
  * the request, in a scheme the FRAMEWORK does not know. Which side of it you are on decides which
7
7
  * hook runs, and nothing else changes:
8
8
  *
@@ -26,9 +26,9 @@
26
26
  *
27
27
  * ## It FAILS CLOSED
28
28
  *
29
- * With no `WebhookSignerCallback` bound, every outbound `@WpAuthWebhook` call THROWS rather than
29
+ * With no `WebhookSignerCallback` bound, every outbound `webhook(...)` call THROWS rather than
30
30
  * going out unsigned — exactly as an unbound `WebhookAuthCallback` 401s every inbound
31
- * `@WpAuthWebhook` endpoint. An unsigned delivery is not a degraded delivery; it is a request the
31
+ * `webhook(...)` endpoint. An unsigned delivery is not a degraded delivery; it is a request the
32
32
  * partner is obliged to reject, and quietly sending one hides the missing binding until a partner
33
33
  * complains.
34
34
  *
@@ -43,7 +43,7 @@ export declare abstract class WebhookSignerCallback {
43
43
  /**
44
44
  * Produce the headers that authenticate ONE outbound request, or throw to refuse to send it.
45
45
  *
46
- * @param name the string on the contract's `@WpAuthWebhook(name)` — which partner this is.
46
+ * @param name the string on the contract's `webhook(name)` — which partner this is.
47
47
  * @param request the FINAL request: the absolute url and the exact bytes that are about to go on
48
48
  * the wire. See {@link SignableRequest} for why both of those words matter.
49
49
  * @returns header name -> value, merged onto the request. An empty map is legal and means "this
@@ -2,10 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WEBHOOK_SIGNER_CALLBACK = exports.SignableRequest = exports.WebhookSignerCallback = void 0;
4
4
  /**
5
- * The OUTBOUND half of `@WpAuthWebhook(name)` — the exact mirror of `WebhookAuthCallback`, which is
5
+ * The OUTBOUND half of `webhook(name)` — the exact mirror of `WebhookAuthCallback`, which is
6
6
  * the INBOUND half.
7
7
  *
8
- * `@WpAuthWebhook('partner-hmac')` on a contract says: this hop is authenticated by a signature over
8
+ * `webhook('partner-hmac')` on a contract says: this hop is authenticated by a signature over
9
9
  * the request, in a scheme the FRAMEWORK does not know. Which side of it you are on decides which
10
10
  * hook runs, and nothing else changes:
11
11
  *
@@ -29,9 +29,9 @@ exports.WEBHOOK_SIGNER_CALLBACK = exports.SignableRequest = exports.WebhookSigne
29
29
  *
30
30
  * ## It FAILS CLOSED
31
31
  *
32
- * With no `WebhookSignerCallback` bound, every outbound `@WpAuthWebhook` call THROWS rather than
32
+ * With no `WebhookSignerCallback` bound, every outbound `webhook(...)` call THROWS rather than
33
33
  * going out unsigned — exactly as an unbound `WebhookAuthCallback` 401s every inbound
34
- * `@WpAuthWebhook` endpoint. An unsigned delivery is not a degraded delivery; it is a request the
34
+ * `webhook(...)` endpoint. An unsigned delivery is not a degraded delivery; it is a request the
35
35
  * partner is obliged to reject, and quietly sending one hides the missing binding until a partner
36
36
  * complains.
37
37
  *
@@ -1 +1 @@
1
- {"version":3,"file":"WebhookSignerCallback.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/WebhookSignerCallback.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAsB,qBAAqB;CAY1C;AAZD,sDAYC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAa,eAAe;IAGJ;IAEA;IAEA;IAOA;IAEA;IAEA;IAjBpB;IACI,kEAAkE;IAClD,GAAW;IAC3B,oCAAoC;IACpB,UAAkB;IAClC,2EAA2E;IAC3D,IAAwB;IACxC;;;;;OAKG;IACa,OAAoC;IACpD,+DAA+D;IAC/C,YAAoB;IACpC,wDAAwD;IACxC,UAAkB;QAflB,QAAG,GAAH,GAAG,CAAQ;QAEX,eAAU,GAAV,UAAU,CAAQ;QAElB,SAAI,GAAJ,IAAI,CAAoB;QAOxB,YAAO,GAAP,OAAO,CAA6B;QAEpC,iBAAY,GAAZ,YAAY,CAAQ;QAEpB,eAAU,GAAV,UAAU,CAAQ;IACnC,CAAC;CACP;AApBD,0CAoBC;AAED;;;;;GAKG;AACH,mNAAmN;AACtM,QAAA,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC","sourcesContent":["/**\n * The OUTBOUND half of `@WpAuthWebhook(name)` — the exact mirror of `WebhookAuthCallback`, which is\n * the INBOUND half.\n *\n * `@WpAuthWebhook('partner-hmac')` on a contract says: this hop is authenticated by a signature over\n * the request, in a scheme the FRAMEWORK does not know. Which side of it you are on decides which\n * hook runs, and nothing else changes:\n *\n * | | who signs | who verifies | the hook |\n * |---|---|---|---|\n * | a vendor posts to US | the vendor | us | `WebhookAuthCallback.verifyWebhook` (http-routing) |\n * | WE post to a partner | us | the partner | `WebhookSignerCallback.sign` (here) |\n *\n * The symmetry is the point: ONE decorator on ONE contract describes the credential, and the same\n * `name` selects the same vendor's scheme on both sides. A server that receives Twilio's callbacks\n * and a client that delivers to a partner read identically.\n *\n * ## Why a hook and not an `@AuthHmac` decorator\n *\n * Because there is no such thing as \"the\" HMAC scheme. Twilio signs the full absolute URL with its\n * sorted parameters; Slack signs `v0:{timestamp}:{body}`; Meta signs the raw body alone; GitHub and\n * Stripe each differ again in prefix, header name and encoding. A decorator that took a secret\n * would have to pick one of those and be wrong for everyone else, and a framework that shipped five\n * vendors' crypto would be shipping five things to keep in step with five vendors. So the scheme\n * lives in the app's hook, the VENDOR lives on the contract, and the framework carries neither.\n *\n * ## It FAILS CLOSED\n *\n * With no `WebhookSignerCallback` bound, every outbound `@WpAuthWebhook` call THROWS rather than\n * going out unsigned — exactly as an unbound `WebhookAuthCallback` 401s every inbound\n * `@WpAuthWebhook` endpoint. An unsigned delivery is not a degraded delivery; it is a request the\n * partner is obliged to reject, and quietly sending one hides the missing binding until a partner\n * complains.\n *\n * ```typescript\n * // AppModule.ts\n * options.bind(WEBHOOK_SIGNER_CALLBACK).to(PartnerHmacSigner);\n * ```\n *\n * ONE hook serves EVERY partner, the way the inbound one serves every vendor: `name` selects which.\n */\nexport abstract class WebhookSignerCallback {\n /**\n * Produce the headers that authenticate ONE outbound request, or throw to refuse to send it.\n *\n * @param name the string on the contract's `@WpAuthWebhook(name)` — which partner this is.\n * @param request the FINAL request: the absolute url and the exact bytes that are about to go on\n * the wire. See {@link SignableRequest} for why both of those words matter.\n * @returns header name -> value, merged onto the request. An empty map is legal and means \"this\n * partner needs no header\" — it is not a way to opt out of signing, because returning\n * it is a statement the hook made rather than a binding somebody forgot.\n */\n abstract sign(name: string, request: SignableRequest): Promise<Map<string, string>>;\n}\n\n/**\n * ONE outbound request, as the thing being signed. Data only.\n *\n * Every field a real vendor scheme needs is here, and the two that make it correct are {@link url}\n * and {@link body}:\n *\n * - {@link url} is the FINAL absolute URL, after every app filter has had its say. A signature\n * computed over the pre-filter URL authenticates a request nobody sent — and for a partner-webhook\n * client the pre-filter URL is the client's own service name, which is not a destination at all.\n * - {@link body} is the EXACT serialized bytes the transport will send, not the DTO. Serialization\n * happens before the filter chain runs and the transport sends this same string verbatim, so the\n * bytes signed and the bytes sent cannot differ. That was impossible while the client owned\n * serialization internally with no seam, which is why outbound webhook senders were forced back\n * to hand-rolling `JSON.stringify` plus a raw HTTP library — a library that re-serializes\n * internally signs one byte sequence and sends another, and the failure is silent.\n */\nexport class SignableRequest {\n constructor(\n /** The FINAL absolute URL this request is about to be sent to. */\n public readonly url: string,\n /** The HTTP method, e.g. 'POST'. */\n public readonly httpMethod: string,\n /** The EXACT serialized body, or undefined for a call with no argument. */\n public readonly body: string | undefined,\n /**\n * The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a\n * timestamp another filter set, a content-type) reads them, and the signature it returns is\n * merged by the caller — so there is exactly one place headers are added and it is the\n * return value.\n */\n public readonly headers: ReadonlyMap<string, string>,\n /** The API contract's class name, e.g. 'PartnerWebhookApi'. */\n public readonly contractName: string,\n /** The contract method being called, e.g. 'deliver'. */\n public readonly methodName: string,\n ) {}\n}\n\n/**\n * DI identifier for the optional {@link WebhookSignerCallback} binding. It is a Symbol (not the\n * class) so the app container's inversify autobind never auto-constructs this token, keeping\n * `@optional() @inject(WEBHOOK_SIGNER_CALLBACK)` correct — undefined when unbound, which is what\n * makes the fail-closed refusal reachable. Mirrors WEBHOOK_AUTH_CALLBACK exactly.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const WEBHOOK_SIGNER_CALLBACK = Symbol.for('WebhookSignerCallback');\n"]}
1
+ {"version":3,"file":"WebhookSignerCallback.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/WebhookSignerCallback.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAsB,qBAAqB;CAY1C;AAZD,sDAYC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAa,eAAe;IAGJ;IAEA;IAEA;IAOA;IAEA;IAEA;IAjBpB;IACI,kEAAkE;IAClD,GAAW;IAC3B,oCAAoC;IACpB,UAAkB;IAClC,2EAA2E;IAC3D,IAAwB;IACxC;;;;;OAKG;IACa,OAAoC;IACpD,+DAA+D;IAC/C,YAAoB;IACpC,wDAAwD;IACxC,UAAkB;QAflB,QAAG,GAAH,GAAG,CAAQ;QAEX,eAAU,GAAV,UAAU,CAAQ;QAElB,SAAI,GAAJ,IAAI,CAAoB;QAOxB,YAAO,GAAP,OAAO,CAA6B;QAEpC,iBAAY,GAAZ,YAAY,CAAQ;QAEpB,eAAU,GAAV,UAAU,CAAQ;IACnC,CAAC;CACP;AApBD,0CAoBC;AAED;;;;;GAKG;AACH,mNAAmN;AACtM,QAAA,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC","sourcesContent":["/**\n * The OUTBOUND half of `webhook(name)` — the exact mirror of `WebhookAuthCallback`, which is\n * the INBOUND half.\n *\n * `webhook('partner-hmac')` on a contract says: this hop is authenticated by a signature over\n * the request, in a scheme the FRAMEWORK does not know. Which side of it you are on decides which\n * hook runs, and nothing else changes:\n *\n * | | who signs | who verifies | the hook |\n * |---|---|---|---|\n * | a vendor posts to US | the vendor | us | `WebhookAuthCallback.verifyWebhook` (http-routing) |\n * | WE post to a partner | us | the partner | `WebhookSignerCallback.sign` (here) |\n *\n * The symmetry is the point: ONE decorator on ONE contract describes the credential, and the same\n * `name` selects the same vendor's scheme on both sides. A server that receives Twilio's callbacks\n * and a client that delivers to a partner read identically.\n *\n * ## Why a hook and not an `@AuthHmac` decorator\n *\n * Because there is no such thing as \"the\" HMAC scheme. Twilio signs the full absolute URL with its\n * sorted parameters; Slack signs `v0:{timestamp}:{body}`; Meta signs the raw body alone; GitHub and\n * Stripe each differ again in prefix, header name and encoding. A decorator that took a secret\n * would have to pick one of those and be wrong for everyone else, and a framework that shipped five\n * vendors' crypto would be shipping five things to keep in step with five vendors. So the scheme\n * lives in the app's hook, the VENDOR lives on the contract, and the framework carries neither.\n *\n * ## It FAILS CLOSED\n *\n * With no `WebhookSignerCallback` bound, every outbound `webhook(...)` call THROWS rather than\n * going out unsigned — exactly as an unbound `WebhookAuthCallback` 401s every inbound\n * `webhook(...)` endpoint. An unsigned delivery is not a degraded delivery; it is a request the\n * partner is obliged to reject, and quietly sending one hides the missing binding until a partner\n * complains.\n *\n * ```typescript\n * // AppModule.ts\n * options.bind(WEBHOOK_SIGNER_CALLBACK).to(PartnerHmacSigner);\n * ```\n *\n * ONE hook serves EVERY partner, the way the inbound one serves every vendor: `name` selects which.\n */\nexport abstract class WebhookSignerCallback {\n /**\n * Produce the headers that authenticate ONE outbound request, or throw to refuse to send it.\n *\n * @param name the string on the contract's `webhook(name)` — which partner this is.\n * @param request the FINAL request: the absolute url and the exact bytes that are about to go on\n * the wire. See {@link SignableRequest} for why both of those words matter.\n * @returns header name -> value, merged onto the request. An empty map is legal and means \"this\n * partner needs no header\" — it is not a way to opt out of signing, because returning\n * it is a statement the hook made rather than a binding somebody forgot.\n */\n abstract sign(name: string, request: SignableRequest): Promise<Map<string, string>>;\n}\n\n/**\n * ONE outbound request, as the thing being signed. Data only.\n *\n * Every field a real vendor scheme needs is here, and the two that make it correct are {@link url}\n * and {@link body}:\n *\n * - {@link url} is the FINAL absolute URL, after every app filter has had its say. A signature\n * computed over the pre-filter URL authenticates a request nobody sent — and for a partner-webhook\n * client the pre-filter URL is the client's own service name, which is not a destination at all.\n * - {@link body} is the EXACT serialized bytes the transport will send, not the DTO. Serialization\n * happens before the filter chain runs and the transport sends this same string verbatim, so the\n * bytes signed and the bytes sent cannot differ. That was impossible while the client owned\n * serialization internally with no seam, which is why outbound webhook senders were forced back\n * to hand-rolling `JSON.stringify` plus a raw HTTP library — a library that re-serializes\n * internally signs one byte sequence and sends another, and the failure is silent.\n */\nexport class SignableRequest {\n constructor(\n /** The FINAL absolute URL this request is about to be sent to. */\n public readonly url: string,\n /** The HTTP method, e.g. 'POST'. */\n public readonly httpMethod: string,\n /** The EXACT serialized body, or undefined for a call with no argument. */\n public readonly body: string | undefined,\n /**\n * The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a\n * timestamp another filter set, a content-type) reads them, and the signature it returns is\n * merged by the caller — so there is exactly one place headers are added and it is the\n * return value.\n */\n public readonly headers: ReadonlyMap<string, string>,\n /** The API contract's class name, e.g. 'PartnerWebhookApi'. */\n public readonly contractName: string,\n /** The contract method being called, e.g. 'deliver'. */\n public readonly methodName: string,\n ) {}\n}\n\n/**\n * DI identifier for the optional {@link WebhookSignerCallback} binding. It is a Symbol (not the\n * class) so the app container's inversify autobind never auto-constructs this token, keeping\n * `@optional() @inject(WEBHOOK_SIGNER_CALLBACK)` correct — undefined when unbound, which is what\n * makes the fail-closed refusal reachable. Mirrors WEBHOOK_AUTH_CALLBACK exactly.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const WEBHOOK_SIGNER_CALLBACK = Symbol.for('WebhookSignerCallback');\n"]}
package/src/index.js CHANGED
@@ -49,8 +49,8 @@ Object.defineProperty(exports, "MissingRuntimeBaseUrlError", { enumerable: true,
49
49
  var OutboundAuthErrors_1 = require("./OutboundAuthErrors");
50
50
  Object.defineProperty(exports, "MissingSharedSecretError", { enumerable: true, get: function () { return OutboundAuthErrors_1.MissingSharedSecretError; } });
51
51
  Object.defineProperty(exports, "MissingWebhookSignerError", { enumerable: true, get: function () { return OutboundAuthErrors_1.MissingWebhookSignerError; } });
52
- // The OUTBOUND half of @WpAuthWebhook(name) — the mirror of http-routing's WebhookAuthCallback. Bind
53
- // one, or every outbound @WpAuthWebhook call throws rather than delivering unsigned.
52
+ // The OUTBOUND half of webhook(name) — the mirror of http-routing's WebhookAuthCallback. Bind
53
+ // one, or every outbound webhook(...) call throws rather than delivering unsigned.
54
54
  var WebhookSignerCallback_1 = require("./WebhookSignerCallback");
55
55
  Object.defineProperty(exports, "WebhookSignerCallback", { enumerable: true, get: function () { return WebhookSignerCallback_1.WebhookSignerCallback; } });
56
56
  Object.defineProperty(exports, "SignableRequest", { enumerable: true, get: function () { return WebhookSignerCallback_1.SignableRequest; } });
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAEH,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,qDAAgF;AAAvE,kHAAA,eAAe,OAAA;AAAE,6HAAA,0BAA0B,OAAA;AACpD,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,kGAAkG;AAClG,4DAA4D;AAC5D,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,kGAAkG;AAClG,4EAA4E;AAC5E,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,2EAA0E;AAAjE,wIAAA,0BAA0B,OAAA;AAEnC,mGAAmG;AACnG,kGAAkG;AAClG,6DAA6D;AAC7D,2DAA2F;AAAlF,8HAAA,wBAAwB,OAAA;AAAE,+HAAA,yBAAyB,OAAA;AAE5D,qGAAqG;AACrG,qFAAqF;AACrF,iEAIiC;AAH7B,8HAAA,qBAAqB,OAAA;AACrB,wHAAA,eAAe,OAAA;AACf,gIAAA,uBAAuB,OAAA;AAG3B,qGAAqG;AACrG,+FAA+F;AAC/F,2CAA6D;AAApD,wGAAA,UAAU,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AACtC,qDAAoD;AAA3C,kHAAA,eAAe,OAAA;AACxB,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,2DAA0D;AAAjD,wHAAA,kBAAkB,OAAA;AAC3B,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,qDAAwE;AAA/D,kHAAA,eAAe,OAAA;AAAE,qHAAA,kBAAkB,OAAA;AAE5C,uEAAuE;AACvE,gEAAiF;AAAxE,+GAAA,WAAW,OAAA;AAAE,yHAAA,qBAAqB,OAAA;AAC3C,gEAAoF;AAA3E,iHAAA,aAAa,OAAA;AAAE,0HAAA,sBAAsB,OAAA;AAG9C,gEAAuE;AAA9D,4HAAA,wBAAwB,OAAA;AAEjC,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AACvB,gEAAqE;AAA5D,+GAAA,WAAW,OAAA;AAAE,6GAAA,SAAS,OAAA","sourcesContent":["/**\n * @webpieces/http-client-node\n *\n * The SERVER-side HTTP client. Reads an API contract's decorators and generates type-safe HTTP\n * clients from it — the same contract the callee's controller implements.\n *\n * Node-only, so unlike @webpieces/http-client-browser it is fully inversify-wired and reads the\n * magic context straight out of the AsyncLocalStorage-backed RequestContext. There is no\n * ContextReader indirection, because a server has exactly one right answer, and a call made\n * OUTSIDE `RequestContext.run(...)` throws instead of silently dropping the trace.\n *\n * Usage:\n * ```typescript\n * import { ClientHttpFactory, ClientConfig } from '@webpieces/http-client-node';\n *\n * // inject the factory, then one client per contract\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n * const response = await server2.fetchValue(req);\n *\n * // a client whose destination arrives per call: ONE filter, and nothing else changes\n * const partner = factory.createRpcClient(PartnerWebhookApi, new ClientConfig('partner-webhooks'), [\n * new ClientFilterDefinition(1000, new ContextFullUrlFilter()),\n * ]);\n * ```\n */\n\nexport { ClientHttpFactory } from './ClientHttpFactory';\nexport { NodeProxyClient, NODE_PROXY_CLIENT_PROVIDER } from './NodeProxyClient';\nexport { ClientConfig } from './ClientConfig';\n\n// Install this on the ONE client whose destination is data. Installing it IS the opt-in; a client\n// without it ignores an ambient OVERRIDE_BASE_URL entirely.\nexport { ContextBaseUrlFilter } from './ContextBaseUrlFilter';\n// The same opt-in for a COMPLETE stored url (host + path + query), sent verbatim with no contract\n// path appended — the usual partner-webhook shape. Reads OVERRIDE_FULL_URL.\nexport { ContextFullUrlFilter } from './ContextFullUrlFilter';\nexport { MissingRuntimeBaseUrlError } from './MissingRuntimeBaseUrlError';\n\n// The two ways outbound auth refuses to send: a binding a contract's auth mode requires is absent.\n// Typed, so a delivery worker tells 'we are misconfigured' from 'the partner registered something\n// hostile' (SsrfRefusedError) without matching message text.\nexport { MissingSharedSecretError, MissingWebhookSignerError } from './OutboundAuthErrors';\n\n// The OUTBOUND half of @WpAuthWebhook(name) — the mirror of http-routing's WebhookAuthCallback. Bind\n// one, or every outbound @WpAuthWebhook call throws rather than delivering unsigned.\nexport {\n WebhookSignerCallback,\n SignableRequest,\n WEBHOOK_SIGNER_CALLBACK,\n} from './WebhookSignerCallback';\n\n// The SSRF policy a re-pointed URL is judged under, and the refusal it produces. Automatic, armed by\n// the ACT of re-pointing; the ONLY way to relax it is naming SsrfTestingPolicy at a call site.\nexport { SsrfPolicy, SsrfTestingPolicy } from './SsrfPolicy';\nexport { SsrfGuardFilter } from './SsrfGuardFilter';\nexport { SsrfRefusedError } from './SsrfRefusedError';\nexport { OutboundAuthFilter } from './OutboundAuthFilter';\nexport { InternalAddressRules } from './InternalAddressRules';\nexport { AddressResolver, DnsAddressResolver } from './AddressResolver';\n\n// The isomorphic engine, re-exported so a server app needs one import.\nexport { ProxyClient, ClientErrorTranslator } from '@webpieces/http-client-core';\nexport { ClientRequest, ClientFilterDefinition } from '@webpieces/http-client-core';\nexport type { ClientFilter } from '@webpieces/http-client-core';\nexport type { ApiPrototype } from '@webpieces/http-client-core';\nexport { StreamingCapabilityError } from '@webpieces/http-client-core';\n\nexport { RuntimeClients } from './RuntimeClients';\nexport { ClientToken, rpcTarget } from '@webpieces/http-client-core';\nexport type { RpcTarget } from '@webpieces/http-client-core';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAEH,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,qDAAgF;AAAvE,kHAAA,eAAe,OAAA;AAAE,6HAAA,0BAA0B,OAAA;AACpD,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,kGAAkG;AAClG,4DAA4D;AAC5D,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,kGAAkG;AAClG,4EAA4E;AAC5E,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,2EAA0E;AAAjE,wIAAA,0BAA0B,OAAA;AAEnC,mGAAmG;AACnG,kGAAkG;AAClG,6DAA6D;AAC7D,2DAA2F;AAAlF,8HAAA,wBAAwB,OAAA;AAAE,+HAAA,yBAAyB,OAAA;AAE5D,8FAA8F;AAC9F,mFAAmF;AACnF,iEAIiC;AAH7B,8HAAA,qBAAqB,OAAA;AACrB,wHAAA,eAAe,OAAA;AACf,gIAAA,uBAAuB,OAAA;AAG3B,qGAAqG;AACrG,+FAA+F;AAC/F,2CAA6D;AAApD,wGAAA,UAAU,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AACtC,qDAAoD;AAA3C,kHAAA,eAAe,OAAA;AACxB,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,2DAA0D;AAAjD,wHAAA,kBAAkB,OAAA;AAC3B,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,qDAAwE;AAA/D,kHAAA,eAAe,OAAA;AAAE,qHAAA,kBAAkB,OAAA;AAE5C,uEAAuE;AACvE,gEAAiF;AAAxE,+GAAA,WAAW,OAAA;AAAE,yHAAA,qBAAqB,OAAA;AAC3C,gEAAoF;AAA3E,iHAAA,aAAa,OAAA;AAAE,0HAAA,sBAAsB,OAAA;AAG9C,gEAAuE;AAA9D,4HAAA,wBAAwB,OAAA;AAEjC,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AACvB,gEAAqE;AAA5D,+GAAA,WAAW,OAAA;AAAE,6GAAA,SAAS,OAAA","sourcesContent":["/**\n * @webpieces/http-client-node\n *\n * The SERVER-side HTTP client. Reads an API contract's decorators and generates type-safe HTTP\n * clients from it — the same contract the callee's controller implements.\n *\n * Node-only, so unlike @webpieces/http-client-browser it is fully inversify-wired and reads the\n * magic context straight out of the AsyncLocalStorage-backed RequestContext. There is no\n * ContextReader indirection, because a server has exactly one right answer, and a call made\n * OUTSIDE `RequestContext.run(...)` throws instead of silently dropping the trace.\n *\n * Usage:\n * ```typescript\n * import { ClientHttpFactory, ClientConfig } from '@webpieces/http-client-node';\n *\n * // inject the factory, then one client per contract\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n * const response = await server2.fetchValue(req);\n *\n * // a client whose destination arrives per call: ONE filter, and nothing else changes\n * const partner = factory.createRpcClient(PartnerWebhookApi, new ClientConfig('partner-webhooks'), [\n * new ClientFilterDefinition(1000, new ContextFullUrlFilter()),\n * ]);\n * ```\n */\n\nexport { ClientHttpFactory } from './ClientHttpFactory';\nexport { NodeProxyClient, NODE_PROXY_CLIENT_PROVIDER } from './NodeProxyClient';\nexport { ClientConfig } from './ClientConfig';\n\n// Install this on the ONE client whose destination is data. Installing it IS the opt-in; a client\n// without it ignores an ambient OVERRIDE_BASE_URL entirely.\nexport { ContextBaseUrlFilter } from './ContextBaseUrlFilter';\n// The same opt-in for a COMPLETE stored url (host + path + query), sent verbatim with no contract\n// path appended — the usual partner-webhook shape. Reads OVERRIDE_FULL_URL.\nexport { ContextFullUrlFilter } from './ContextFullUrlFilter';\nexport { MissingRuntimeBaseUrlError } from './MissingRuntimeBaseUrlError';\n\n// The two ways outbound auth refuses to send: a binding a contract's auth mode requires is absent.\n// Typed, so a delivery worker tells 'we are misconfigured' from 'the partner registered something\n// hostile' (SsrfRefusedError) without matching message text.\nexport { MissingSharedSecretError, MissingWebhookSignerError } from './OutboundAuthErrors';\n\n// The OUTBOUND half of webhook(name) — the mirror of http-routing's WebhookAuthCallback. Bind\n// one, or every outbound webhook(...) call throws rather than delivering unsigned.\nexport {\n WebhookSignerCallback,\n SignableRequest,\n WEBHOOK_SIGNER_CALLBACK,\n} from './WebhookSignerCallback';\n\n// The SSRF policy a re-pointed URL is judged under, and the refusal it produces. Automatic, armed by\n// the ACT of re-pointing; the ONLY way to relax it is naming SsrfTestingPolicy at a call site.\nexport { SsrfPolicy, SsrfTestingPolicy } from './SsrfPolicy';\nexport { SsrfGuardFilter } from './SsrfGuardFilter';\nexport { SsrfRefusedError } from './SsrfRefusedError';\nexport { OutboundAuthFilter } from './OutboundAuthFilter';\nexport { InternalAddressRules } from './InternalAddressRules';\nexport { AddressResolver, DnsAddressResolver } from './AddressResolver';\n\n// The isomorphic engine, re-exported so a server app needs one import.\nexport { ProxyClient, ClientErrorTranslator } from '@webpieces/http-client-core';\nexport { ClientRequest, ClientFilterDefinition } from '@webpieces/http-client-core';\nexport type { ClientFilter } from '@webpieces/http-client-core';\nexport type { ApiPrototype } from '@webpieces/http-client-core';\nexport { StreamingCapabilityError } from '@webpieces/http-client-core';\n\nexport { RuntimeClients } from './RuntimeClients';\nexport { ClientToken, rpcTarget } from '@webpieces/http-client-core';\nexport type { RpcTarget } from '@webpieces/http-client-core';\n"]}