@webpieces/http-client-node 0.4.965 → 0.4.967

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
@@ -144,8 +144,8 @@ role (both defined in `@webpieces/core-context`):
144
144
 
145
145
  | role | token | methods | who binds it | re-exported by |
146
146
  |---|---|---|---|---|
147
- | `VendorAuthMinter` | `VENDOR_AUTH_MINTER` | `mint`, `refresh` | a process that SENDS `vendorAuth(...)` calls (a CLI, a worker, a server calling a vendor) | `@webpieces/http-client-node` |
148
- | `VendorAuthVerifier` | `VENDOR_AUTH_VERIFIER` | `verify` | a server that RECEIVES `vendorAuth(...)` calls | `@webpieces/http-routing` |
147
+ | `VendorAuthMinter` | `VENDOR_AUTH_MINTER` | `supportedVendorNames`, `mint`, `refresh` | a process that SENDS `vendorAuth(...)` calls (a CLI, a worker, a server calling a vendor) | `@webpieces/http-client-node` |
148
+ | `VendorAuthVerifier` | `VENDOR_AUTH_VERIFIER` | `supportedVendorNames`, `verify` | a server that RECEIVES `vendorAuth(...)` calls | `@webpieces/http-routing` |
149
149
 
150
150
  A process binds neither, one, or both. `mint` produces the credential WE present when we call the
151
151
  vendor (a per-merchant OAuth bearer, a partner's HMAC over the final URL and the exact wire bytes),
@@ -154,6 +154,8 @@ and `refresh` renews it when the vendor answers our call with a `401`:
154
154
  ```ts
155
155
  @provideSingleton()
156
156
  export class PartnerAuthMinter extends VendorAuthMinter {
157
+ // PURE: a fixed list of every name mint()/refresh() below handle. createRpcClient calls it.
158
+ supportedVendorNames(): readonly string[] { return ['partner-hmac']; }
157
159
  async mint(name: string, request: SignableRequest): Promise<Map<string, string>> {
158
160
  const mac = createHmac('sha256', secretFor(name)).update(request.body ?? '').digest('hex');
159
161
  return new Map([['x-partner-signature', `sha256=${mac}`]]);
@@ -175,8 +177,9 @@ verifier that looks a credential up through such a client closed a DI cycle
175
177
  | configuration | minter required? | verifier required? |
176
178
  |---|---|---|
177
179
  | OIDC-only or public-only outbound client | no | no |
178
- | outbound call selecting `vendorAuth(name)` | yes, to send that call | no |
179
- | server receiving a `vendorAuth(name)` request | no | yes |
180
+ | outbound endpoint whose ONLY sendable method is `vendorAuth(name)` | yes, listing `name` — or `createRpcClient` throws | no |
181
+ | outbound endpoint that also allows OIDC (or a held shared secret) | no | no |
182
+ | server SERVING a `vendorAuth(name)` route | no | yes, listing `name` — or the server refuses to boot |
180
183
  | server receiving vendor calls and calling an OIDC-only API downstream | no (not for the OIDC hop) | yes |
181
184
  | process both sending and receiving vendor calls | yes | yes |
182
185
 
@@ -190,7 +193,9 @@ The unified hook is DELETED (no alias): the old import stops compiling, which is
190
193
  - A SERVER process: `extends VendorAuthVerifier`, keep `verify`, delete the meaningless
191
194
  `mint`/`refresh`, and bind `VENDOR_AUTH_VERIFIER`. Its constructor may inject the typed
192
195
  `oidcSameProject()` RPC client it uses to check the credential.
193
- - `MissingVendorAuthHookError` is now `MissingVendorAuthMinterError`.
196
+ - `MissingVendorAuthHookError` and its successor `MissingVendorAuthMinterError` (and
197
+ `MissingSharedSecretError`) are DELETED: `createRpcClient` throws `MissingClientCredentialError`
198
+ instead, before any call (#1362).
194
199
  - Contracts are unchanged: `@WpAuth([vendorAuth(name)])` keeps its meaning.
195
200
 
196
201
  On a raw `401`, `OutboundAuthFilter` cancels the 401's body, calls `refresh(name)` ONCE, mints again
@@ -200,11 +205,30 @@ instances (single-use refresh tokens) is the minter's job.
200
205
 
201
206
  The framework ships no vendor crypto, deliberately: Twilio signs the full URL with sorted params,
202
207
  Slack signs `v0:{ts}:{body}`, Meta signs the raw body. The scheme lives in your minter / verifier and
203
- the vendor on the contract. With **no** `VendorAuthMinter` bound, every outbound `vendorAuth(...)`
204
- call THROWS (`MissingVendorAuthMinterError`) before anything is sent, rather than sending
205
- unauthenticated — the mirror of an unbound `VendorAuthVerifier` 401ing every inbound one. A bound
206
- verifier never counts as a minter, and an unbound minter never breaks an OIDC, shared-secret or
207
- public call.
208
+ the vendor on the contract.
209
+
210
+ ### It fails at STARTUP, not on the first call (#1362)
211
+
212
+ Both roles declare the vendors they handle with a required, PURE `supportedVendorNames()` beside
213
+ `verify` / `mint` — add a vendor to both in one edit. The framework calls it at boot / client
214
+ creation and never calls `verify`, `mint`, `refresh` or the network then.
215
+
216
+ - **Client — one per endpoint.** `createRpcClient` throws `MissingClientCredentialError` (naming the
217
+ contract, the method and the binding to add) when an endpoint declares no method this client can
218
+ send now: OIDC and public always; `sharedSecret(key)` when the bound `Secrets` holds `key`;
219
+ `vendorAuth(name)` when a bound minter's `supportedVendorNames()` lists `name`; and `jwt()` /
220
+ `apiKey(...)`, which the framework never mints but the app relays per call (a forwarded user JWT
221
+ in a context key, a partner SDK's headers set by a client filter) — chosen only when no
222
+ framework-sent method is available. A client needs only ONE — a CLI need not hold OIDC
223
+ credentials because the contract also allows OIDC.
224
+ - **Server — strict.** Route registration refuses to boot (`UnverifiableAuthModeError`, naming the
225
+ endpoint, the mode and the exact `binder.bind(...)` line) when a SERVED route declares any mode
226
+ nothing can verify — including a dead alternative such as `[oidcSameProject(), vendorAuth('x')]`
227
+ with no verifier. See `@webpieces/http-routing`.
228
+
229
+ A bound verifier never counts as a minter, and an unbound minter never breaks an OIDC, shared-secret
230
+ or public call. A name the minter's / verifier's own dispatch does not know still refuses at request
231
+ time, as a backstop.
208
232
 
209
233
  Calls have a **30-second default timeout**, including URL resolution, filters/auth, fetch and body
210
234
  reading. `ClientRegistry` from `@webpieces/core-util` shares ALL, API and API-method overrides with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-client-node",
3
- "version": "0.4.965",
3
+ "version": "0.4.967",
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.965",
26
- "@webpieces/core-util": "0.4.965",
27
- "@webpieces/gcp-identity": "0.4.965",
28
- "@webpieces/http-client-core": "0.4.965",
25
+ "@webpieces/core-context": "0.4.967",
26
+ "@webpieces/core-util": "0.4.967",
27
+ "@webpieces/gcp-identity": "0.4.967",
28
+ "@webpieces/http-client-core": "0.4.967",
29
29
  "inversify": "7.10.4",
30
30
  "reflect-metadata": "0.2.2"
31
31
  }
@@ -18,14 +18,18 @@ class DeletedAuthCompileAssertions {
18
18
  void ClientNode.WebhookSignerCallback;
19
19
  // @ts-expect-error WEBHOOK_SIGNER_CALLBACK is replaced by VENDOR_AUTH_MINTER
20
20
  void ClientNode.WEBHOOK_SIGNER_CALLBACK;
21
- // @ts-expect-error MissingWebhookSignerError is replaced by MissingVendorAuthMinterError
21
+ // @ts-expect-error MissingWebhookSignerError is replaced by MissingClientCredentialError (thrown by createRpcClient)
22
22
  void ClientNode.MissingWebhookSignerError;
23
23
  // @ts-expect-error VendorAuthHook is DELETED: the client binds the OUTBOUND role VendorAuthMinter
24
24
  void ClientNode.VendorAuthHook;
25
25
  // @ts-expect-error VENDOR_AUTH_HOOK is DELETED: bind VENDOR_AUTH_MINTER
26
26
  void ClientNode.VENDOR_AUTH_HOOK;
27
- // @ts-expect-error MissingVendorAuthHookError is RENAMED to MissingVendorAuthMinterError
27
+ // @ts-expect-error MissingVendorAuthHookError is replaced by MissingClientCredentialError (thrown by createRpcClient)
28
28
  void ClientNode.MissingVendorAuthHookError;
29
+ // @ts-expect-error MissingVendorAuthMinterError is DELETED (#1362): createRpcClient throws MissingClientCredentialError instead of the first call
30
+ void ClientNode.MissingVendorAuthMinterError;
31
+ // @ts-expect-error MissingSharedSecretError is DELETED (#1362): createRpcClient throws MissingClientCredentialError instead of the first call
32
+ void ClientNode.MissingSharedSecretError;
29
33
  // @ts-expect-error the INBOUND role is the server's (http-routing); a client package never exports it
30
34
  void ClientNode.VENDOR_AUTH_VERIFIER;
31
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DeletedAuthCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/DeletedAuthCompileAssertions.ts"],"names":[],"mappings":";;;;AAEA,4DAAsC;AACtC,6DAA0D;AAE1D;;;;;;;GAOG;AACH,MAAa,4BAA4B;IACrC,cAAc;QACV,wGAAwG;QACxG,KAAK,UAAU,CAAC,qBAAqB,CAAC;QACtC,6EAA6E;QAC7E,KAAK,UAAU,CAAC,uBAAuB,CAAC;QACxC,yFAAyF;QACzF,KAAK,UAAU,CAAC,yBAAyB,CAAC;QAC1C,kGAAkG;QAClG,KAAK,UAAU,CAAC,cAAc,CAAC;QAC/B,wEAAwE;QACxE,KAAK,UAAU,CAAC,gBAAgB,CAAC;QACjC,yFAAyF;QACzF,KAAK,UAAU,CAAC,0BAA0B,CAAC;QAC3C,sGAAsG;QACtG,KAAK,UAAU,CAAC,oBAAoB,CAAC;IACzC,CAAC;IAED;;;OAGG;IACH,gCAAgC,CAAC,IAAa,EAAE,QAA4B;QACxE,iGAAiG;QACjG,KAAK,IAAI,uCAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC3D,CAAC;CACJ;AA1BD,oEA0BC","sourcesContent":["import { VendorAuthVerifier } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport * as ClientNode from './index';\nimport { OutboundAuthFilter } from './OutboundAuthFilter';\n\n/**\n * COMPILE-TIME proof that the deleted outbound auth spellings stay deleted. The outbound half of\n * `webhook(name)` (#1339) and the unified `VendorAuthHook` that replaced it (#1358) are both gone:\n * this client mints with the OUTBOUND role `VendorAuthMinter` (core-context) and nothing else. Each\n * `@ts-expect-error` fails the build (TS2578) the moment its symbol is exported or accepted again. A\n * compiled file, never a `.spec.ts`, because vitest strips types and a spec's `@ts-expect-error` is\n * inert.\n */\nexport class DeletedAuthCompileAssertions {\n removedExports(): void {\n // @ts-expect-error WebhookSignerCallback is replaced by VendorAuthMinter.mint (bind VENDOR_AUTH_MINTER)\n void ClientNode.WebhookSignerCallback;\n // @ts-expect-error WEBHOOK_SIGNER_CALLBACK is replaced by VENDOR_AUTH_MINTER\n void ClientNode.WEBHOOK_SIGNER_CALLBACK;\n // @ts-expect-error MissingWebhookSignerError is replaced by MissingVendorAuthMinterError\n void ClientNode.MissingWebhookSignerError;\n // @ts-expect-error VendorAuthHook is DELETED: the client binds the OUTBOUND role VendorAuthMinter\n void ClientNode.VendorAuthHook;\n // @ts-expect-error VENDOR_AUTH_HOOK is DELETED: bind VENDOR_AUTH_MINTER\n void ClientNode.VENDOR_AUTH_HOOK;\n // @ts-expect-error MissingVendorAuthHookError is RENAMED to MissingVendorAuthMinterError\n void ClientNode.MissingVendorAuthHookError;\n // @ts-expect-error the INBOUND role is the server's (http-routing); a client package never exports it\n void ClientNode.VENDOR_AUTH_VERIFIER;\n }\n\n /**\n * The outbound filter takes the OUTBOUND role only. Handing it the server's inbound verifier —\n * the shape that put the verifier into every RPC proxy's construction (#1358) — must not compile.\n */\n outboundFilterRefusesTheVerifier(oidc: GcpOidc, verifier: VendorAuthVerifier): void {\n // @ts-expect-error a VendorAuthVerifier cannot mint: OutboundAuthFilter takes a VendorAuthMinter\n void new OutboundAuthFilter(oidc, undefined, verifier);\n }\n}\n"]}
1
+ {"version":3,"file":"DeletedAuthCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/DeletedAuthCompileAssertions.ts"],"names":[],"mappings":";;;;AAEA,4DAAsC;AACtC,6DAA0D;AAE1D;;;;;;;GAOG;AACH,MAAa,4BAA4B;IACrC,cAAc;QACV,wGAAwG;QACxG,KAAK,UAAU,CAAC,qBAAqB,CAAC;QACtC,6EAA6E;QAC7E,KAAK,UAAU,CAAC,uBAAuB,CAAC;QACxC,qHAAqH;QACrH,KAAK,UAAU,CAAC,yBAAyB,CAAC;QAC1C,kGAAkG;QAClG,KAAK,UAAU,CAAC,cAAc,CAAC;QAC/B,wEAAwE;QACxE,KAAK,UAAU,CAAC,gBAAgB,CAAC;QACjC,sHAAsH;QACtH,KAAK,UAAU,CAAC,0BAA0B,CAAC;QAC3C,kJAAkJ;QAClJ,KAAK,UAAU,CAAC,4BAA4B,CAAC;QAC7C,8IAA8I;QAC9I,KAAK,UAAU,CAAC,wBAAwB,CAAC;QACzC,sGAAsG;QACtG,KAAK,UAAU,CAAC,oBAAoB,CAAC;IACzC,CAAC;IAED;;;OAGG;IACH,gCAAgC,CAAC,IAAa,EAAE,QAA4B;QACxE,iGAAiG;QACjG,KAAK,IAAI,uCAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC3D,CAAC;CACJ;AA9BD,oEA8BC","sourcesContent":["import { VendorAuthVerifier } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport * as ClientNode from './index';\nimport { OutboundAuthFilter } from './OutboundAuthFilter';\n\n/**\n * COMPILE-TIME proof that the deleted outbound auth spellings stay deleted. The outbound half of\n * `webhook(name)` (#1339) and the unified `VendorAuthHook` that replaced it (#1358) are both gone:\n * this client mints with the OUTBOUND role `VendorAuthMinter` (core-context) and nothing else. Each\n * `@ts-expect-error` fails the build (TS2578) the moment its symbol is exported or accepted again. A\n * compiled file, never a `.spec.ts`, because vitest strips types and a spec's `@ts-expect-error` is\n * inert.\n */\nexport class DeletedAuthCompileAssertions {\n removedExports(): void {\n // @ts-expect-error WebhookSignerCallback is replaced by VendorAuthMinter.mint (bind VENDOR_AUTH_MINTER)\n void ClientNode.WebhookSignerCallback;\n // @ts-expect-error WEBHOOK_SIGNER_CALLBACK is replaced by VENDOR_AUTH_MINTER\n void ClientNode.WEBHOOK_SIGNER_CALLBACK;\n // @ts-expect-error MissingWebhookSignerError is replaced by MissingClientCredentialError (thrown by createRpcClient)\n void ClientNode.MissingWebhookSignerError;\n // @ts-expect-error VendorAuthHook is DELETED: the client binds the OUTBOUND role VendorAuthMinter\n void ClientNode.VendorAuthHook;\n // @ts-expect-error VENDOR_AUTH_HOOK is DELETED: bind VENDOR_AUTH_MINTER\n void ClientNode.VENDOR_AUTH_HOOK;\n // @ts-expect-error MissingVendorAuthHookError is replaced by MissingClientCredentialError (thrown by createRpcClient)\n void ClientNode.MissingVendorAuthHookError;\n // @ts-expect-error MissingVendorAuthMinterError is DELETED (#1362): createRpcClient throws MissingClientCredentialError instead of the first call\n void ClientNode.MissingVendorAuthMinterError;\n // @ts-expect-error MissingSharedSecretError is DELETED (#1362): createRpcClient throws MissingClientCredentialError instead of the first call\n void ClientNode.MissingSharedSecretError;\n // @ts-expect-error the INBOUND role is the server's (http-routing); a client package never exports it\n void ClientNode.VENDOR_AUTH_VERIFIER;\n }\n\n /**\n * The outbound filter takes the OUTBOUND role only. Handing it the server's inbound verifier —\n * the shape that put the verifier into every RPC proxy's construction (#1358) — must not compile.\n */\n outboundFilterRefusesTheVerifier(oidc: GcpOidc, verifier: VendorAuthVerifier): void {\n // @ts-expect-error a VendorAuthVerifier cannot mint: OutboundAuthFilter takes a VendorAuthMinter\n void new OutboundAuthFilter(oidc, undefined, verifier);\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { DestinationTrust, RouteMetadata, Secrets, ClientRole, HttpCallFactsHolder } from '@webpieces/core-util';
1
+ import { AuthMode, DestinationTrust, RouteMetadata, Secrets, ClientRole, HttpCallFactsHolder } from '@webpieces/core-util';
2
2
  import { RequestContextHeaders, VendorAuthMinter } from '@webpieces/core-context';
3
3
  import { GcpOidc } from '@webpieces/gcp-identity';
4
4
  import { ApiPrototype, ByteReadableStream, ClientRequest, ClientFilterDefinition, ProxyClient } from '@webpieces/http-client-core';
@@ -18,7 +18,39 @@ export declare class NodeProxyClient extends ProxyClient {
18
18
  private readonly addressResolver;
19
19
  private readonly secrets?;
20
20
  private readonly vendorAuthMinter?;
21
- protected selectAuthMethod(route: import('@webpieces/core-util').RouteMetadata): import('@webpieces/core-util').AuthMode;
21
+ /**
22
+ * The declared method this client sends. ONE implementation, run at client creation by
23
+ * {@link assertEndpointSupported} (which is what makes `createRpcClient` fail fast, #1362) and per
24
+ * call by the base class — so the per-call run always finds what creation found, and there is no
25
+ * "send an unsatisfiable `methods[0]` and let it fail later" fallback.
26
+ *
27
+ * A credential the FRAMEWORK can send now wins (the first in declaration order); otherwise a
28
+ * credential the APP relays per call (a user JWT or a partner's api key, carried by a context key
29
+ * or a client filter — see {@link appRelays}); otherwise nothing is sendable and creation fails.
30
+ */
31
+ protected selectAuthMethod(route: RouteMetadata): AuthMode;
32
+ /**
33
+ * ONE PER ENDPOINT (#1362): every route of the contract must declare at least one method this
34
+ * client can send, or `createRpcClient` throws {@link MissingClientCredentialError}. Reads the bound
35
+ * `Secrets` and the minter's pure `supportedVendorNames()` only — never mints, never refreshes, no network — and
36
+ * never touches the inbound `VendorAuthVerifier`, so the #1358 construction cycle stays closed.
37
+ */
38
+ protected assertEndpointSupported(route: RouteMetadata): void;
39
+ /**
40
+ * Can the FRAMEWORK send `method` from what this process holds NOW? An exhaustive switch with no
41
+ * `default`, so a new AuthMode kind is a compile error here rather than silently sendable or not.
42
+ */
43
+ private frameworkSends;
44
+ /**
45
+ * A credential the framework never mints but the APP supplies PER CALL — a user JWT a node BFF
46
+ * forwards from its own caller through a context key, a partner SDK's api-key headers set by a
47
+ * client filter. Whether a given call carries it is unknowable at creation, exactly as for a
48
+ * browser's JWT, so it is not refused here: a call without it is the callee's 401.
49
+ */
50
+ private static appRelays;
51
+ private noSendableCredential;
52
+ private cureFor;
53
+ private static spell;
22
54
  private config;
23
55
  constructor(headers: RequestContextHeaders, gcpOidc: GcpOidc, addressResolver: AddressResolver, secrets?: Secrets | undefined, vendorAuthMinter?: VendorAuthMinter | undefined);
24
56
  /**
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ var NodeProxyClient_1;
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.NODE_PROXY_CLIENT_PROVIDER = exports.NodeProxyClient = void 0;
4
5
  const tslib_1 = require("tslib");
@@ -14,6 +15,7 @@ const http_client_core_1 = require("@webpieces/http-client-core");
14
15
  const AddressResolver_1 = require("./AddressResolver");
15
16
  const ContextBaseUrlFilter_1 = require("./ContextBaseUrlFilter");
16
17
  const ContextFullUrlFilter_1 = require("./ContextFullUrlFilter");
18
+ const OutboundAuthErrors_1 = require("./OutboundAuthErrors");
17
19
  const OutboundAuthFilter_1 = require("./OutboundAuthFilter");
18
20
  const SsrfGuardFilter_1 = require("./SsrfGuardFilter");
19
21
  const SsrfPolicy_1 = require("./SsrfPolicy");
@@ -34,24 +36,118 @@ const OUTBOUND_AUTH_PRIORITY = 800;
34
36
  * binds one instance to exactly one API contract and one target. {@link ProxyClientProvider} hands
35
37
  * them out — see its doc.
36
38
  */
37
- let NodeProxyClient = class NodeProxyClient extends http_client_core_1.ProxyClient {
39
+ let NodeProxyClient = NodeProxyClient_1 = class NodeProxyClient extends http_client_core_1.ProxyClient {
38
40
  headers;
39
41
  gcpOidc;
40
42
  addressResolver;
41
43
  secrets;
42
44
  vendorAuthMinter;
45
+ /**
46
+ * The declared method this client sends. ONE implementation, run at client creation by
47
+ * {@link assertEndpointSupported} (which is what makes `createRpcClient` fail fast, #1362) and per
48
+ * call by the base class — so the per-call run always finds what creation found, and there is no
49
+ * "send an unsatisfiable `methods[0]` and let it fail later" fallback.
50
+ *
51
+ * A credential the FRAMEWORK can send now wins (the first in declaration order); otherwise a
52
+ * credential the APP relays per call (a user JWT or a partner's api key, carried by a context key
53
+ * or a client filter — see {@link appRelays}); otherwise nothing is sendable and creation fails.
54
+ */
43
55
  selectAuthMethod(route) {
44
56
  const methods = route.authMeta?.methods;
45
57
  if (!methods)
46
58
  throw new Error('Endpoint requires @WpAuth or @WpAuthPublic.');
47
- const selected = methods.find((method) => method.kind === 'oidc' || method.kind === 'public' ||
48
- (method.kind === 'shared-secret' && !!this.secrets?.get(method.secretKey)) ||
49
- (method.kind === 'vendorAuth' && !!this.vendorAuthMinter));
59
+ const selected = methods.find((method) => this.frameworkSends(method)) ??
60
+ methods.find((method) => NodeProxyClient_1.appRelays(method));
50
61
  if (selected)
51
62
  return selected;
52
- if (methods.length === 1)
53
- return methods[0];
54
- throw new Error('No configured Node client credential can satisfy this endpoint; bind a declared service credential.');
63
+ throw this.noSendableCredential(route, methods);
64
+ }
65
+ /**
66
+ * ONE PER ENDPOINT (#1362): every route of the contract must declare at least one method this
67
+ * client can send, or `createRpcClient` throws {@link MissingClientCredentialError}. Reads the bound
68
+ * `Secrets` and the minter's pure `supportedVendorNames()` only — never mints, never refreshes, no network — and
69
+ * never touches the inbound `VendorAuthVerifier`, so the #1358 construction cycle stays closed.
70
+ */
71
+ assertEndpointSupported(route) {
72
+ this.selectAuthMethod(route);
73
+ }
74
+ /**
75
+ * Can the FRAMEWORK send `method` from what this process holds NOW? An exhaustive switch with no
76
+ * `default`, so a new AuthMode kind is a compile error here rather than silently sendable or not.
77
+ */
78
+ frameworkSends(method) {
79
+ switch (method.kind) {
80
+ case 'oidc':
81
+ case 'public':
82
+ return true;
83
+ case 'shared-secret':
84
+ return !!this.secrets?.get(method.secretKey);
85
+ case 'vendorAuth':
86
+ return this.vendorAuthMinter?.supportedVendorNames().includes(method.name) ?? false;
87
+ // Never minted by the framework: see appRelays.
88
+ case 'jwt':
89
+ case 'apikey':
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * A credential the framework never mints but the APP supplies PER CALL — a user JWT a node BFF
95
+ * forwards from its own caller through a context key, a partner SDK's api-key headers set by a
96
+ * client filter. Whether a given call carries it is unknowable at creation, exactly as for a
97
+ * browser's JWT, so it is not refused here: a call without it is the callee's 401.
98
+ */
99
+ // webpieces-disable no-function-outside-class -- static pure predicate over the AuthMode union, kept beside its only caller
100
+ static appRelays(method) {
101
+ switch (method.kind) {
102
+ case 'jwt':
103
+ case 'apikey':
104
+ return true;
105
+ case 'oidc':
106
+ case 'public':
107
+ case 'shared-secret':
108
+ case 'vendorAuth':
109
+ return false;
110
+ }
111
+ }
112
+ noSendableCredential(route, methods) {
113
+ const endpoint = `${this.contractName()}.${route.methodName}`;
114
+ const declared = methods.map((method) => NodeProxyClient_1.spell(method));
115
+ const cures = methods.map((method) => ` - ${NodeProxyClient_1.spell(method)}: ${this.cureFor(method)}`);
116
+ return new OutboundAuthErrors_1.MissingClientCredentialError(`${endpoint} declares ${declared.join(', ')}, and this client can send none of them, so ` +
117
+ `createRpcClient refuses the contract rather than fail on the first call. Make ONE of ` +
118
+ `these sendable (a client needs only one):\n${cures.join('\n')}`, endpoint, declared);
119
+ }
120
+ cureFor(method) {
121
+ switch (method.kind) {
122
+ case 'shared-secret':
123
+ return this.secrets
124
+ ? `the bound Secrets (${this.secrets.constructor.name}) holds no value for '${method.secretKey}' — give it one`
125
+ : `bind a Secrets carrying '${method.secretKey}': binder.bind(SECRETS).to(YourSecrets).inSingletonScope();`;
126
+ case 'vendorAuth':
127
+ return this.vendorAuthMinter
128
+ ? `the bound VendorAuthMinter (${this.vendorAuthMinter.constructor.name}) supportedVendorNames() returns ` +
129
+ `[${this.vendorAuthMinter.supportedVendorNames().map((name) => `'${name}'`).join(', ')}] — add '${method.name}' to ` +
130
+ `supportedVendorNames() and handle it in mint()/refresh()`
131
+ : `bind the OUTBOUND role: binder.bind(VENDOR_AUTH_MINTER).to(YourVendorAuthMinter).inSingletonScope(); ` +
132
+ `(a VENDOR_AUTH_VERIFIER binding does not count: it verifies inbound calls and never sends)`;
133
+ case 'jwt':
134
+ case 'apikey':
135
+ return 'relayed by the app per call';
136
+ case 'oidc':
137
+ case 'public':
138
+ return 'always sendable';
139
+ }
140
+ }
141
+ // webpieces-disable no-function-outside-class -- static pure formatter of the AuthMode union, kept beside its only callers
142
+ static spell(method) {
143
+ switch (method.kind) {
144
+ case 'oidc': return method.callerScope === 'same-project' ? 'oidcSameProject()' : `oidcSvcs([${method.callers.join(', ')}])`;
145
+ case 'public': return '@WpAuthPublic';
146
+ case 'jwt': return 'jwt()';
147
+ case 'shared-secret': return `sharedSecret('${method.secretKey}')`;
148
+ case 'vendorAuth': return `vendorAuth('${method.name}')`;
149
+ case 'apikey': return `apiKey('${method.regime}', credentials)`;
150
+ }
55
151
  }
56
152
  config;
57
153
  constructor(headers, gcpOidc, addressResolver, secrets, vendorAuthMinter) {
@@ -254,7 +350,7 @@ let NodeProxyClient = class NodeProxyClient extends http_client_core_1.ProxyClie
254
350
  }
255
351
  };
256
352
  exports.NodeProxyClient = NodeProxyClient;
257
- exports.NodeProxyClient = NodeProxyClient = tslib_1.__decorate([
353
+ exports.NodeProxyClient = NodeProxyClient = NodeProxyClient_1 = tslib_1.__decorate([
258
354
  (0, core_context_1.provideFrameworkTransient)(),
259
355
  tslib_1.__param(0, (0, inversify_1.inject)(core_context_1.RequestContextHeaders)),
260
356
  tslib_1.__param(1, (0, inversify_1.inject)(gcp_identity_1.GcpOidc)),
@@ -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 ClientRole,\n HttpCallFactsHolder,\n} from '@webpieces/core-util';\nimport {\n RequestContext,\n RequestContextApiCallContext,\n RequestContextHeaders,\n provideFrameworkTransient,\n VendorAuthMinter,\n VENDOR_AUTH_MINTER,\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';\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 === 'vendorAuth' && !!this.vendorAuthMinter));\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 OUTBOUND vendorAuth(...) calls need it, and an unbound one makes them THROW\n // rather than send unauthenticated — see VendorAuthMinter. The OUTBOUND role ONLY: this\n // client must never depend on the server's inbound VendorAuthVerifier, because a verifier\n // that itself uses an RPC client would then close a construction cycle (#1358).\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(VENDOR_AUTH_MINTER)\n private readonly vendorAuthMinter?: VendorAuthMinter,\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 clientRole(): ClientRole {\n return this.config.role;\n }\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.vendorAuthMinter),\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 (oidcSameProject() /\n * oidcSvcs(...) / 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 http?: HttpCallFactsHolder,\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, http);\n }\n return this.recordCall(recorder, route, requestDto, method, http);\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 http: HttpCallFactsHolder | undefined,\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, http);\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 AuthMode,\n ClientRegistry,\n DestinationTrust,\n ApiImplementationError,\n RecordedEndpoint,\n RecordedError,\n LogApiCallImpl,\n RouteMetadata,\n Secrets,\n SECRETS,\n TestCaseRecorder,\n toError,\n ClientRole,\n HttpCallFactsHolder,\n} from '@webpieces/core-util';\nimport {\n RequestContext,\n RequestContextApiCallContext,\n RequestContextHeaders,\n provideFrameworkTransient,\n VendorAuthMinter,\n VENDOR_AUTH_MINTER,\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 { MissingClientCredentialError } from './OutboundAuthErrors';\nimport { OutboundAuthFilter } from './OutboundAuthFilter';\nimport { SsrfGuardFilter } from './SsrfGuardFilter';\nimport { SsrfPolicy } from './SsrfPolicy';\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 /**\n * The declared method this client sends. ONE implementation, run at client creation by\n * {@link assertEndpointSupported} (which is what makes `createRpcClient` fail fast, #1362) and per\n * call by the base class — so the per-call run always finds what creation found, and there is no\n * \"send an unsatisfiable `methods[0]` and let it fail later\" fallback.\n *\n * A credential the FRAMEWORK can send now wins (the first in declaration order); otherwise a\n * credential the APP relays per call (a user JWT or a partner's api key, carried by a context key\n * or a client filter — see {@link appRelays}); otherwise nothing is sendable and creation fails.\n */\n protected override selectAuthMethod(route: RouteMetadata): AuthMode {\n const methods = route.authMeta?.methods;\n if (!methods) throw new Error('Endpoint requires @WpAuth or @WpAuthPublic.');\n const selected =\n methods.find((method: AuthMode) => this.frameworkSends(method)) ??\n methods.find((method: AuthMode) => NodeProxyClient.appRelays(method));\n if (selected) return selected;\n throw this.noSendableCredential(route, methods);\n }\n\n /**\n * ONE PER ENDPOINT (#1362): every route of the contract must declare at least one method this\n * client can send, or `createRpcClient` throws {@link MissingClientCredentialError}. Reads the bound\n * `Secrets` and the minter's pure `supportedVendorNames()` only — never mints, never refreshes, no network — and\n * never touches the inbound `VendorAuthVerifier`, so the #1358 construction cycle stays closed.\n */\n protected override assertEndpointSupported(route: RouteMetadata): void {\n this.selectAuthMethod(route);\n }\n\n /**\n * Can the FRAMEWORK send `method` from what this process holds NOW? An exhaustive switch with no\n * `default`, so a new AuthMode kind is a compile error here rather than silently sendable or not.\n */\n private frameworkSends(method: AuthMode): boolean {\n switch (method.kind) {\n case 'oidc':\n case 'public':\n return true;\n case 'shared-secret':\n return !!this.secrets?.get(method.secretKey);\n case 'vendorAuth':\n return this.vendorAuthMinter?.supportedVendorNames().includes(method.name) ?? false;\n // Never minted by the framework: see appRelays.\n case 'jwt':\n case 'apikey':\n return false;\n }\n }\n\n /**\n * A credential the framework never mints but the APP supplies PER CALL — a user JWT a node BFF\n * forwards from its own caller through a context key, a partner SDK's api-key headers set by a\n * client filter. Whether a given call carries it is unknowable at creation, exactly as for a\n * browser's JWT, so it is not refused here: a call without it is the callee's 401.\n */\n // webpieces-disable no-function-outside-class -- static pure predicate over the AuthMode union, kept beside its only caller\n private static appRelays(method: AuthMode): boolean {\n switch (method.kind) {\n case 'jwt':\n case 'apikey':\n return true;\n case 'oidc':\n case 'public':\n case 'shared-secret':\n case 'vendorAuth':\n return false;\n }\n }\n\n private noSendableCredential(route: RouteMetadata, methods: readonly AuthMode[]): MissingClientCredentialError {\n const endpoint = `${this.contractName()}.${route.methodName}`;\n const declared = methods.map((method: AuthMode) => NodeProxyClient.spell(method));\n const cures = methods.map((method: AuthMode) => ` - ${NodeProxyClient.spell(method)}: ${this.cureFor(method)}`);\n return new MissingClientCredentialError(\n `${endpoint} declares ${declared.join(', ')}, and this client can send none of them, so ` +\n `createRpcClient refuses the contract rather than fail on the first call. Make ONE of ` +\n `these sendable (a client needs only one):\\n${cures.join('\\n')}`,\n endpoint,\n declared,\n );\n }\n\n private cureFor(method: AuthMode): string {\n switch (method.kind) {\n case 'shared-secret':\n return this.secrets\n ? `the bound Secrets (${this.secrets.constructor.name}) holds no value for '${method.secretKey}' — give it one`\n : `bind a Secrets carrying '${method.secretKey}': binder.bind(SECRETS).to(YourSecrets).inSingletonScope();`;\n case 'vendorAuth':\n return this.vendorAuthMinter\n ? `the bound VendorAuthMinter (${this.vendorAuthMinter.constructor.name}) supportedVendorNames() returns ` +\n `[${this.vendorAuthMinter.supportedVendorNames().map((name: string) => `'${name}'`).join(', ')}] — add '${method.name}' to ` +\n `supportedVendorNames() and handle it in mint()/refresh()`\n : `bind the OUTBOUND role: binder.bind(VENDOR_AUTH_MINTER).to(YourVendorAuthMinter).inSingletonScope(); ` +\n `(a VENDOR_AUTH_VERIFIER binding does not count: it verifies inbound calls and never sends)`;\n case 'jwt':\n case 'apikey':\n return 'relayed by the app per call';\n case 'oidc':\n case 'public':\n return 'always sendable';\n }\n }\n\n // webpieces-disable no-function-outside-class -- static pure formatter of the AuthMode union, kept beside its only callers\n private static spell(method: AuthMode): string {\n switch (method.kind) {\n case 'oidc': return method.callerScope === 'same-project' ? 'oidcSameProject()' : `oidcSvcs([${method.callers.join(', ')}])`;\n case 'public': return '@WpAuthPublic';\n case 'jwt': return 'jwt()';\n case 'shared-secret': return `sharedSecret('${method.secretKey}')`;\n case 'vendorAuth': return `vendorAuth('${method.name}')`;\n case 'apikey': return `apiKey('${method.regime}', credentials)`;\n }\n }\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 OUTBOUND vendorAuth(...) calls need it, and createRpcClient refuses a contract\n // whose endpoint can be sent ONLY with a minter that is absent or does not list the vendor\n // (assertEndpointSupported) — see VendorAuthMinter. The OUTBOUND role ONLY: this\n // client must never depend on the server's inbound VendorAuthVerifier, because a verifier\n // that itself uses an RPC client would then close a construction cycle (#1358).\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(VENDOR_AUTH_MINTER)\n private readonly vendorAuthMinter?: VendorAuthMinter,\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 clientRole(): ClientRole {\n return this.config.role;\n }\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.vendorAuthMinter),\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 (oidcSameProject() /\n * oidcSvcs(...) / 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 http?: HttpCallFactsHolder,\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, http);\n }\n return this.recordCall(recorder, route, requestDto, method, http);\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 http: HttpCallFactsHolder | undefined,\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, http);\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,42 +1,28 @@
1
1
  /**
2
- * The two ways outbound auth refuses to send, each its own type for the reason
3
- * {@link MissingRuntimeBaseUrlError} is one: a delivery worker has to tell these apart from
4
- * {@link SsrfRefusedError}. Both mean THIS SERVICE is misconfigured — page somebody, retrying is
5
- * pointless — where an SSRF refusal means a partner registered something hostile and the delivery
6
- * should be dead-lettered. A bare `Error` forces that decision to be made by matching message text.
2
+ * The ONE way outbound auth refuses a contract: an endpoint declares no credential this client can
3
+ * send. Thrown at CLIENT CREATION (`createRpcClient`, from `NodeProxyClient.assertEndpointSupported`),
4
+ * before any call is made (#1362) — it replaces the call-time `MissingSharedSecretError` and
5
+ * `MissingVendorAuthMinterError`, which fired only on the first call and only for one mode.
7
6
  *
8
- * Neither is thrown for anything a caller can influence: they fire when the binding a contract's
9
- * auth mode requires is simply absent.
10
- */
11
- /**
12
- * An `sharedSecret(key)` endpoint was called by a client whose bound `Secrets` holds no value
13
- * for that key. Thrown at CALL time, from `OutboundAuthFilter`.
14
- */
15
- export declare class MissingSharedSecretError extends Error {
16
- /** `Contract.method`, so the log line names the call without a stack read. */
17
- readonly endpoint: string;
18
- /** The `sharedSecret(...)` key that had no value, so the fix names itself. */
19
- readonly secretKey: string;
20
- constructor(message: string,
21
- /** `Contract.method`, so the log line names the call without a stack read. */
22
- endpoint: string,
23
- /** The `sharedSecret(...)` key that had no value, so the fix names itself. */
24
- secretKey: string);
25
- }
26
- /**
27
- * A `vendorAuth(name)` endpoint was called outbound with no `VendorAuthMinter` (VENDOR_AUTH_MINTER)
28
- * bound, so nothing can mint the credential the vendor checks. Thrown at CALL time, from
29
- * `OutboundAuthFilter`, BEFORE anything is sent — the mirror of the inbound side, where an unbound
30
- * `VendorAuthVerifier` 401s every served `vendorAuth(...)` endpoint instead of admitting it.
7
+ * Its own type for the reason {@link MissingRuntimeBaseUrlError} is one: it means THIS SERVICE is
8
+ * misconfigured — page somebody, retrying is pointless — and nothing a caller sends can cause it. A
9
+ * bare `Error` would force a host to tell it apart from {@link SsrfRefusedError} by matching message
10
+ * text.
11
+ *
12
+ * ONE PER ENDPOINT, not every alternative: a client needs only ONE declared method it can send now
13
+ * (OIDC and public always; `sharedSecret(key)` when its bound `Secrets` holds `key`; `vendorAuth(name)`
14
+ * when its bound `VendorAuthMinter`'s `supportedVendorNames()` lists `name`; `jwt()` / `apiKey(...)`,
15
+ * which the app relays per call). A CLI need not hold OIDC credentials just because
16
+ * the contract ALSO allows OIDC.
31
17
  */
32
- export declare class MissingVendorAuthMinterError extends Error {
33
- /** `Contract.method`, so the log line names the call without a stack read. */
18
+ export declare class MissingClientCredentialError extends Error {
19
+ /** `Contract.method`, so the log line names the endpoint without a stack read. */
34
20
  readonly endpoint: string;
35
- /** The vendor on the contract's `vendorAuth(name)`, which selects the scheme. */
36
- readonly vendorName: string;
21
+ /** Every declared method, as written on the contract, e.g. `vendorAuth('clover')`. */
22
+ readonly declared: readonly string[];
37
23
  constructor(message: string,
38
- /** `Contract.method`, so the log line names the call without a stack read. */
24
+ /** `Contract.method`, so the log line names the endpoint without a stack read. */
39
25
  endpoint: string,
40
- /** The vendor on the contract's `vendorAuth(name)`, which selects the scheme. */
41
- vendorName: string);
26
+ /** Every declared method, as written on the contract, e.g. `vendorAuth('clover')`. */
27
+ declared: readonly string[]);
42
28
  }
@@ -1,54 +1,36 @@
1
1
  "use strict";
2
- /**
3
- * The two ways outbound auth refuses to send, each its own type for the reason
4
- * {@link MissingRuntimeBaseUrlError} is one: a delivery worker has to tell these apart from
5
- * {@link SsrfRefusedError}. Both mean THIS SERVICE is misconfigured — page somebody, retrying is
6
- * pointless — where an SSRF refusal means a partner registered something hostile and the delivery
7
- * should be dead-lettered. A bare `Error` forces that decision to be made by matching message text.
8
- *
9
- * Neither is thrown for anything a caller can influence: they fire when the binding a contract's
10
- * auth mode requires is simply absent.
11
- */
12
2
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.MissingVendorAuthMinterError = exports.MissingSharedSecretError = void 0;
14
- /**
15
- * An `sharedSecret(key)` endpoint was called by a client whose bound `Secrets` holds no value
16
- * for that key. Thrown at CALL time, from `OutboundAuthFilter`.
17
- */
18
- class MissingSharedSecretError extends Error {
19
- endpoint;
20
- secretKey;
21
- constructor(message,
22
- /** `Contract.method`, so the log line names the call without a stack read. */
23
- endpoint,
24
- /** The `sharedSecret(...)` key that had no value, so the fix names itself. */
25
- secretKey) {
26
- super(message);
27
- this.endpoint = endpoint;
28
- this.secretKey = secretKey;
29
- this.name = 'MissingSharedSecretError';
30
- }
31
- }
32
- exports.MissingSharedSecretError = MissingSharedSecretError;
3
+ exports.MissingClientCredentialError = void 0;
33
4
  /**
34
- * A `vendorAuth(name)` endpoint was called outbound with no `VendorAuthMinter` (VENDOR_AUTH_MINTER)
35
- * bound, so nothing can mint the credential the vendor checks. Thrown at CALL time, from
36
- * `OutboundAuthFilter`, BEFORE anything is sent — the mirror of the inbound side, where an unbound
37
- * `VendorAuthVerifier` 401s every served `vendorAuth(...)` endpoint instead of admitting it.
5
+ * The ONE way outbound auth refuses a contract: an endpoint declares no credential this client can
6
+ * send. Thrown at CLIENT CREATION (`createRpcClient`, from `NodeProxyClient.assertEndpointSupported`),
7
+ * before any call is made (#1362) — it replaces the call-time `MissingSharedSecretError` and
8
+ * `MissingVendorAuthMinterError`, which fired only on the first call and only for one mode.
9
+ *
10
+ * Its own type for the reason {@link MissingRuntimeBaseUrlError} is one: it means THIS SERVICE is
11
+ * misconfigured — page somebody, retrying is pointless — and nothing a caller sends can cause it. A
12
+ * bare `Error` would force a host to tell it apart from {@link SsrfRefusedError} by matching message
13
+ * text.
14
+ *
15
+ * ONE PER ENDPOINT, not every alternative: a client needs only ONE declared method it can send now
16
+ * (OIDC and public always; `sharedSecret(key)` when its bound `Secrets` holds `key`; `vendorAuth(name)`
17
+ * when its bound `VendorAuthMinter`'s `supportedVendorNames()` lists `name`; `jwt()` / `apiKey(...)`,
18
+ * which the app relays per call). A CLI need not hold OIDC credentials just because
19
+ * the contract ALSO allows OIDC.
38
20
  */
39
- class MissingVendorAuthMinterError extends Error {
21
+ class MissingClientCredentialError extends Error {
40
22
  endpoint;
41
- vendorName;
23
+ declared;
42
24
  constructor(message,
43
- /** `Contract.method`, so the log line names the call without a stack read. */
25
+ /** `Contract.method`, so the log line names the endpoint without a stack read. */
44
26
  endpoint,
45
- /** The vendor on the contract's `vendorAuth(name)`, which selects the scheme. */
46
- vendorName) {
27
+ /** Every declared method, as written on the contract, e.g. `vendorAuth('clover')`. */
28
+ declared) {
47
29
  super(message);
48
30
  this.endpoint = endpoint;
49
- this.vendorName = vendorName;
50
- this.name = 'MissingVendorAuthMinterError';
31
+ this.declared = declared;
32
+ this.name = 'MissingClientCredentialError';
51
33
  }
52
34
  }
53
- exports.MissingVendorAuthMinterError = MissingVendorAuthMinterError;
35
+ exports.MissingClientCredentialError = MissingClientCredentialError;
54
36
  //# sourceMappingURL=OutboundAuthErrors.js.map
@@ -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,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,4BAA6B,SAAQ,KAAK;IAI/B;IAEA;IALpB,YACI,OAAe;IACf,8EAA8E;IAC9D,QAAgB;IAChC,iFAAiF;IACjE,UAAkB;QAElC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,eAAU,GAAV,UAAU,CAAQ;QAGlC,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;IAC/C,CAAC;CACJ;AAXD,oEAWC","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 * A `vendorAuth(name)` endpoint was called outbound with no `VendorAuthMinter` (VENDOR_AUTH_MINTER)\n * bound, so nothing can mint the credential the vendor checks. Thrown at CALL time, from\n * `OutboundAuthFilter`, BEFORE anything is sent — the mirror of the inbound side, where an unbound\n * `VendorAuthVerifier` 401s every served `vendorAuth(...)` endpoint instead of admitting it.\n */\nexport class MissingVendorAuthMinterError 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 `vendorAuth(name)`, which selects the scheme. */\n public readonly vendorName: string,\n ) {\n super(message);\n this.name = 'MissingVendorAuthMinterError';\n }\n}\n"]}
1
+ {"version":3,"file":"OutboundAuthErrors.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/OutboundAuthErrors.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAa,4BAA6B,SAAQ,KAAK;IAI/B;IAEA;IALpB,YACI,OAAe;IACf,kFAAkF;IAClE,QAAgB;IAChC,sFAAsF;IACtE,QAA2B;QAE3C,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,aAAQ,GAAR,QAAQ,CAAmB;QAG3C,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;IAC/C,CAAC;CACJ;AAXD,oEAWC","sourcesContent":["/**\n * The ONE way outbound auth refuses a contract: an endpoint declares no credential this client can\n * send. Thrown at CLIENT CREATION (`createRpcClient`, from `NodeProxyClient.assertEndpointSupported`),\n * before any call is made (#1362) — it replaces the call-time `MissingSharedSecretError` and\n * `MissingVendorAuthMinterError`, which fired only on the first call and only for one mode.\n *\n * Its own type for the reason {@link MissingRuntimeBaseUrlError} is one: it means THIS SERVICE is\n * misconfigured — page somebody, retrying is pointless — and nothing a caller sends can cause it. A\n * bare `Error` would force a host to tell it apart from {@link SsrfRefusedError} by matching message\n * text.\n *\n * ONE PER ENDPOINT, not every alternative: a client needs only ONE declared method it can send now\n * (OIDC and public always; `sharedSecret(key)` when its bound `Secrets` holds `key`; `vendorAuth(name)`\n * when its bound `VendorAuthMinter`'s `supportedVendorNames()` lists `name`; `jwt()` / `apiKey(...)`,\n * which the app relays per call). A CLI need not hold OIDC credentials just because\n * the contract ALSO allows OIDC.\n */\nexport class MissingClientCredentialError extends Error {\n constructor(\n message: string,\n /** `Contract.method`, so the log line names the endpoint without a stack read. */\n public readonly endpoint: string,\n /** Every declared method, as written on the contract, e.g. `vendorAuth('clover')`. */\n public readonly declared: readonly string[],\n ) {\n super(message);\n this.name = 'MissingClientCredentialError';\n }\n}\n"]}
@@ -37,22 +37,31 @@ import { ClientRequest } from '@webpieces/http-client-core';
37
37
  * Both credential-minting modes ride in the ONE `Authorization` header under their own scheme —
38
38
  * `Bearer <oidc>` / `Webpieces <secret>` — which is never a context key, so neither can leak onto
39
39
  * the next hop.
40
+ *
41
+ * ## A missing credential never reaches this filter
42
+ *
43
+ * The route's mode was selected by `NodeProxyClient.selectAuthMethod`, the same code that made
44
+ * `createRpcClient` refuse the contract when no declared method was sendable (#1362). So a
45
+ * `sharedSecret(key)` arriving here has a held value and a `vendorAuth(name)` a minter listing `name`;
46
+ * the two checks below only narrow the types, and name the broken invariant if they ever fire.
40
47
  */
41
48
  export declare class OutboundAuthFilter extends Filter<ClientRequest, Response> {
42
49
  private readonly gcpOidc;
43
- /** Only sharedSecret(...) endpoints need it; a server that has none binds nothing. */
50
+ /** Only sharedSecret(...) endpoints need it; a process that calls none binds nothing. */
44
51
  private readonly secrets;
45
52
  /**
46
- * The OUTBOUND role only. Only vendorAuth(...) calls need it, and an unbound one makes them
47
- * THROW. Never the inbound VendorAuthVerifier: sending a request verifies nothing (#1358).
53
+ * The OUTBOUND role only. Only vendorAuth(...) calls need it, and createRpcClient refuses a
54
+ * contract it cannot call without one. Never the inbound VendorAuthVerifier: sending a request
55
+ * verifies nothing (#1358).
48
56
  */
49
57
  private readonly vendorAuthMinter;
50
58
  constructor(gcpOidc: GcpOidc,
51
- /** Only sharedSecret(...) endpoints need it; a server that has none binds nothing. */
59
+ /** Only sharedSecret(...) endpoints need it; a process that calls none binds nothing. */
52
60
  secrets: Secrets | undefined,
53
61
  /**
54
- * The OUTBOUND role only. Only vendorAuth(...) calls need it, and an unbound one makes them
55
- * THROW. Never the inbound VendorAuthVerifier: sending a request verifies nothing (#1358).
62
+ * The OUTBOUND role only. Only vendorAuth(...) calls need it, and createRpcClient refuses a
63
+ * contract it cannot call without one. Never the inbound VendorAuthVerifier: sending a request
64
+ * verifies nothing (#1358).
56
65
  */
57
66
  vendorAuthMinter: VendorAuthMinter | undefined);
58
67
  /** Requests that already spent their one refresh — keyed by the request, so redirect hops share it. */
@@ -61,11 +70,5 @@ export declare class OutboundAuthFilter extends Filter<ClientRequest, Response>
61
70
  /** A request-streaming call's body is a one-shot stream, so it can never be re-sent. */
62
71
  private uploadsAStream;
63
72
  private attach;
64
- /**
65
- * @throws MissingVendorAuthMinterError when no {@link VendorAuthMinter} is bound. FAIL CLOSED,
66
- * matching the inbound side exactly: an unbound verifier 401s every served
67
- * `vendorAuth(...)` endpoint rather than admitting it unverified, so an unbound minter
68
- * must also refuse to SEND rather than deliver something the vendor is obliged to reject.
69
- */
70
73
  private mintVendorAuth;
71
74
  }
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OutboundAuthFilter = void 0;
4
4
  const core_util_1 = require("@webpieces/core-util");
5
5
  const core_context_1 = require("@webpieces/core-context");
6
- const OutboundAuthErrors_1 = require("./OutboundAuthErrors");
7
6
  /**
8
7
  * Attaches the endpoint's outbound credential, against the destination the call is ACTUALLY going
9
8
  * to.
@@ -39,17 +38,25 @@ const OutboundAuthErrors_1 = require("./OutboundAuthErrors");
39
38
  * Both credential-minting modes ride in the ONE `Authorization` header under their own scheme —
40
39
  * `Bearer <oidc>` / `Webpieces <secret>` — which is never a context key, so neither can leak onto
41
40
  * the next hop.
41
+ *
42
+ * ## A missing credential never reaches this filter
43
+ *
44
+ * The route's mode was selected by `NodeProxyClient.selectAuthMethod`, the same code that made
45
+ * `createRpcClient` refuse the contract when no declared method was sendable (#1362). So a
46
+ * `sharedSecret(key)` arriving here has a held value and a `vendorAuth(name)` a minter listing `name`;
47
+ * the two checks below only narrow the types, and name the broken invariant if they ever fire.
42
48
  */
43
49
  class OutboundAuthFilter extends core_util_1.Filter {
44
50
  gcpOidc;
45
51
  secrets;
46
52
  vendorAuthMinter;
47
53
  constructor(gcpOidc,
48
- /** Only sharedSecret(...) endpoints need it; a server that has none binds nothing. */
54
+ /** Only sharedSecret(...) endpoints need it; a process that calls none binds nothing. */
49
55
  secrets,
50
56
  /**
51
- * The OUTBOUND role only. Only vendorAuth(...) calls need it, and an unbound one makes them
52
- * THROW. Never the inbound VendorAuthVerifier: sending a request verifies nothing (#1358).
57
+ * The OUTBOUND role only. Only vendorAuth(...) calls need it, and createRpcClient refuses a
58
+ * contract it cannot call without one. Never the inbound VendorAuthVerifier: sending a request
59
+ * verifies nothing (#1358).
53
60
  */
54
61
  vendorAuthMinter) {
55
62
  super();
@@ -90,11 +97,8 @@ class OutboundAuthFilter extends core_util_1.Filter {
90
97
  if (mode?.kind === 'shared-secret') {
91
98
  const secret = this.secrets?.get(mode.secretKey);
92
99
  if (!secret) {
93
- throw new OutboundAuthErrors_1.MissingSharedSecretError(`${request.contractName}.${request.route.methodName} is ` +
94
- `sharedSecret('${mode.secretKey}'), but this client's bound Secrets holds no ` +
95
- `value for that key, so there is no credential to send. Bind a Secrets carrying ` +
96
- `'${mode.secretKey}'. Refusing to send is deliberate: the callee is obliged to 401 an ` +
97
- `unauthenticated request, so sending it would report as the peer's failure.`, `${request.contractName}.${request.route.methodName}`, mode.secretKey);
100
+ throw new Error(`Invariant broken: ${request.contractName}.${request.route.methodName} selected ` +
101
+ `sharedSecret('${mode.secretKey}') with no held value; NodeProxyClient.selectAuthMethod never selects it.`);
98
102
  }
99
103
  // Same header as a JWT/OIDC token, but its OWN scheme, so a secret can never be
100
104
  // mistaken for a token nor accepted where one was expected.
@@ -105,21 +109,10 @@ class OutboundAuthFilter extends core_util_1.Filter {
105
109
  await this.mintVendorAuth(request, mode.name);
106
110
  }
107
111
  }
108
- /**
109
- * @throws MissingVendorAuthMinterError when no {@link VendorAuthMinter} is bound. FAIL CLOSED,
110
- * matching the inbound side exactly: an unbound verifier 401s every served
111
- * `vendorAuth(...)` endpoint rather than admitting it unverified, so an unbound minter
112
- * must also refuse to SEND rather than deliver something the vendor is obliged to reject.
113
- */
114
112
  async mintVendorAuth(request, name) {
115
113
  if (this.vendorAuthMinter === undefined) {
116
- throw new OutboundAuthErrors_1.MissingVendorAuthMinterError(`${request.contractName}.${request.route.methodName} is vendorAuth('${name}'), so this ` +
117
- `client must present ${name}'s credential — but no VendorAuthMinter is bound, so there ` +
118
- `is nothing to mint it. Bind the OUTBOUND role in this process:\n` +
119
- ` binder.bind(VENDOR_AUTH_MINTER).to(MyVendorAuthMinter).inSingletonScope();\n` +
120
- `(A VENDOR_AUTH_VERIFIER binding does not count: it verifies inbound calls and never sends.)\n` +
121
- `Refusing to send is deliberate: an unauthenticated call is one the vendor will reject, ` +
122
- `and sending it anyway would hide the missing binding until they complained.`, `${request.contractName}.${request.route.methodName}`, name);
114
+ throw new Error(`Invariant broken: ${request.contractName}.${request.route.methodName} selected ` +
115
+ `vendorAuth('${name}') with no VendorAuthMinter bound; NodeProxyClient.selectAuthMethod never selects it.`);
123
116
  }
124
117
  const signable = new core_context_1.SignableRequest(request.url, request.route.httpMethod, request.body, request.headers, request.contractName, request.route.methodName);
125
118
  const minted = await this.vendorAuthMinter.mint(name, signable);
@@ -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, StreamDirection } from '@webpieces/core-util';\nimport { SignableRequest, VendorAuthMinter } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport { ClientRequest } from '@webpieces/http-client-core';\nimport { MissingSharedSecretError, MissingVendorAuthMinterError } from './OutboundAuthErrors';\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 * - `oidcSameProject()` / `oidcSvcs(...)` → 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 * - `vendorAuth(name)` → the app's bound {@link VendorAuthMinter} `mint`s the headers (a vendor's\n * OAuth bearer, a partner's HMAC signature — the scheme is the app's). On a raw `401` it is the ONE\n * mode that retries: cancel the 401's body, `refresh(name)` once, `mint` again and re-send ONCE.\n * Never for a streaming upload (its body cannot be replayed), and once per logical call — the SSRF\n * guard's redirect loop re-enters this filter for each hop, and a second 401 goes up the chain to\n * `ClientErrorTranslator` exactly as before, because a service that cannot log in to its vendor\n * twice running is a bug in THAT service.\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 /**\n * The OUTBOUND role only. Only vendorAuth(...) calls need it, and an unbound one makes them\n * THROW. Never the inbound VendorAuthVerifier: sending a request verifies nothing (#1358).\n */\n private readonly vendorAuthMinter: VendorAuthMinter | undefined,\n ) {\n super();\n }\n\n /** Requests that already spent their one refresh — keyed by the request, so redirect hops share it. */\n private readonly refreshed = new WeakSet<ClientRequest>();\n\n override async filter(\n request: ClientRequest,\n nextFilter: Service<ClientRequest, Response>,\n ): Promise<Response> {\n await this.attach(request);\n const response = await nextFilter.invoke(request);\n const mode = request.route.authMeta?.methods[0];\n if (\n response.status !== 401 ||\n mode?.kind !== 'vendorAuth' ||\n this.refreshed.has(request) ||\n this.uploadsAStream(request)\n ) {\n return response;\n }\n this.refreshed.add(request);\n // The 401's body is never read: release the connection before the second attempt.\n await response.body?.cancel();\n await this.vendorAuthMinter!.refresh(mode.name);\n await this.mintVendorAuth(request, mode.name);\n return nextFilter.invoke(request);\n }\n\n /** A request-streaming call's body is a one-shot stream, so it can never be re-sent. */\n private uploadsAStream(request: ClientRequest): boolean {\n const streaming = request.route.streaming;\n return streaming !== undefined && streaming.direction !== StreamDirection.RESPONSE;\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 === 'vendorAuth') {\n await this.mintVendorAuth(request, mode.name);\n }\n }\n\n /**\n * @throws MissingVendorAuthMinterError when no {@link VendorAuthMinter} is bound. FAIL CLOSED,\n * matching the inbound side exactly: an unbound verifier 401s every served\n * `vendorAuth(...)` endpoint rather than admitting it unverified, so an unbound minter\n * must also refuse to SEND rather than deliver something the vendor is obliged to reject.\n */\n private async mintVendorAuth(request: ClientRequest, name: string): Promise<void> {\n if (this.vendorAuthMinter === undefined) {\n throw new MissingVendorAuthMinterError(\n `${request.contractName}.${request.route.methodName} is vendorAuth('${name}'), so this ` +\n `client must present ${name}'s credential — but no VendorAuthMinter is bound, so there ` +\n `is nothing to mint it. Bind the OUTBOUND role in this process:\\n` +\n ` binder.bind(VENDOR_AUTH_MINTER).to(MyVendorAuthMinter).inSingletonScope();\\n` +\n `(A VENDOR_AUTH_VERIFIER binding does not count: it verifies inbound calls and never sends.)\\n` +\n `Refusing to send is deliberate: an unauthenticated call is one the vendor 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 minted = await this.vendorAuthMinter.mint(name, signable);\n for (const entry of minted.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, StreamDirection } from '@webpieces/core-util';\nimport { SignableRequest, VendorAuthMinter } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport { ClientRequest } from '@webpieces/http-client-core';\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 * - `oidcSameProject()` / `oidcSvcs(...)` → 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 * - `vendorAuth(name)` → the app's bound {@link VendorAuthMinter} `mint`s the headers (a vendor's\n * OAuth bearer, a partner's HMAC signature — the scheme is the app's). On a raw `401` it is the ONE\n * mode that retries: cancel the 401's body, `refresh(name)` once, `mint` again and re-send ONCE.\n * Never for a streaming upload (its body cannot be replayed), and once per logical call — the SSRF\n * guard's redirect loop re-enters this filter for each hop, and a second 401 goes up the chain to\n * `ClientErrorTranslator` exactly as before, because a service that cannot log in to its vendor\n * twice running is a bug in THAT service.\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 *\n * ## A missing credential never reaches this filter\n *\n * The route's mode was selected by `NodeProxyClient.selectAuthMethod`, the same code that made\n * `createRpcClient` refuse the contract when no declared method was sendable (#1362). So a\n * `sharedSecret(key)` arriving here has a held value and a `vendorAuth(name)` a minter listing `name`;\n * the two checks below only narrow the types, and name the broken invariant if they ever fire.\n */\nexport class OutboundAuthFilter extends Filter<ClientRequest, Response> {\n constructor(\n private readonly gcpOidc: GcpOidc,\n /** Only sharedSecret(...) endpoints need it; a process that calls none binds nothing. */\n private readonly secrets: Secrets | undefined,\n /**\n * The OUTBOUND role only. Only vendorAuth(...) calls need it, and createRpcClient refuses a\n * contract it cannot call without one. Never the inbound VendorAuthVerifier: sending a request\n * verifies nothing (#1358).\n */\n private readonly vendorAuthMinter: VendorAuthMinter | undefined,\n ) {\n super();\n }\n\n /** Requests that already spent their one refresh — keyed by the request, so redirect hops share it. */\n private readonly refreshed = new WeakSet<ClientRequest>();\n\n override async filter(\n request: ClientRequest,\n nextFilter: Service<ClientRequest, Response>,\n ): Promise<Response> {\n await this.attach(request);\n const response = await nextFilter.invoke(request);\n const mode = request.route.authMeta?.methods[0];\n if (\n response.status !== 401 ||\n mode?.kind !== 'vendorAuth' ||\n this.refreshed.has(request) ||\n this.uploadsAStream(request)\n ) {\n return response;\n }\n this.refreshed.add(request);\n // The 401's body is never read: release the connection before the second attempt.\n await response.body?.cancel();\n await this.vendorAuthMinter!.refresh(mode.name);\n await this.mintVendorAuth(request, mode.name);\n return nextFilter.invoke(request);\n }\n\n /** A request-streaming call's body is a one-shot stream, so it can never be re-sent. */\n private uploadsAStream(request: ClientRequest): boolean {\n const streaming = request.route.streaming;\n return streaming !== undefined && streaming.direction !== StreamDirection.RESPONSE;\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 Error(\n `Invariant broken: ${request.contractName}.${request.route.methodName} selected ` +\n `sharedSecret('${mode.secretKey}') with no held value; NodeProxyClient.selectAuthMethod never selects it.`,\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 === 'vendorAuth') {\n await this.mintVendorAuth(request, mode.name);\n }\n }\n\n private async mintVendorAuth(request: ClientRequest, name: string): Promise<void> {\n if (this.vendorAuthMinter === undefined) {\n throw new Error(\n `Invariant broken: ${request.contractName}.${request.route.methodName} selected ` +\n `vendorAuth('${name}') with no VendorAuthMinter bound; NodeProxyClient.selectAuthMethod never selects it.`,\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 minted = await this.vendorAuthMinter.mint(name, signable);\n for (const entry of minted.entries()) {\n request.headers.set(entry[0], entry[1]);\n }\n }\n}\n"]}
package/src/index.d.ts CHANGED
@@ -30,7 +30,7 @@ export { ClientRole } from '@webpieces/core-util';
30
30
  export { ContextBaseUrlFilter } from './ContextBaseUrlFilter';
31
31
  export { ContextFullUrlFilter } from './ContextFullUrlFilter';
32
32
  export { MissingRuntimeBaseUrlError } from './MissingRuntimeBaseUrlError';
33
- export { MissingSharedSecretError, MissingVendorAuthMinterError } from './OutboundAuthErrors';
33
+ export { MissingClientCredentialError } from './OutboundAuthErrors';
34
34
  export { VendorAuthMinter, VENDOR_AUTH_MINTER, SignableRequest } from '@webpieces/core-context';
35
35
  export { SsrfPolicy, SsrfTestingPolicy } from './SsrfPolicy';
36
36
  export { SsrfGuardFilter } from './SsrfGuardFilter';
package/src/index.js CHANGED
@@ -25,7 +25,7 @@
25
25
  * ```
26
26
  */
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
- exports.WiringPolicy = exports.StreamingCapabilityError = exports.ClientFilterDefinition = exports.ClientRequest = exports.ClientErrorTranslator = exports.ProxyClient = exports.DnsAddressResolver = exports.AddressResolver = exports.InternalAddressRules = exports.OutboundAuthFilter = exports.SsrfRefusedError = exports.SsrfGuardFilter = exports.SsrfTestingPolicy = exports.SsrfPolicy = exports.SignableRequest = exports.VENDOR_AUTH_MINTER = exports.VendorAuthMinter = exports.MissingVendorAuthMinterError = exports.MissingSharedSecretError = exports.MissingRuntimeBaseUrlError = exports.ContextFullUrlFilter = exports.ContextBaseUrlFilter = exports.ClientRole = exports.ClientConfig = exports.NODE_PROXY_CLIENT_PROVIDER = exports.NodeProxyClient = exports.ClientHttpFactory = void 0;
28
+ exports.WiringPolicy = exports.StreamingCapabilityError = exports.ClientFilterDefinition = exports.ClientRequest = exports.ClientErrorTranslator = exports.ProxyClient = exports.DnsAddressResolver = exports.AddressResolver = exports.InternalAddressRules = exports.OutboundAuthFilter = exports.SsrfRefusedError = exports.SsrfGuardFilter = exports.SsrfTestingPolicy = exports.SsrfPolicy = exports.SignableRequest = exports.VENDOR_AUTH_MINTER = exports.VendorAuthMinter = exports.MissingClientCredentialError = exports.MissingRuntimeBaseUrlError = exports.ContextFullUrlFilter = exports.ContextBaseUrlFilter = exports.ClientRole = exports.ClientConfig = exports.NODE_PROXY_CLIENT_PROVIDER = exports.NodeProxyClient = exports.ClientHttpFactory = void 0;
29
29
  var ClientHttpFactory_1 = require("./ClientHttpFactory");
30
30
  Object.defineProperty(exports, "ClientHttpFactory", { enumerable: true, get: function () { return ClientHttpFactory_1.ClientHttpFactory; } });
31
31
  var NodeProxyClient_1 = require("./NodeProxyClient");
@@ -46,15 +46,16 @@ var ContextFullUrlFilter_1 = require("./ContextFullUrlFilter");
46
46
  Object.defineProperty(exports, "ContextFullUrlFilter", { enumerable: true, get: function () { return ContextFullUrlFilter_1.ContextFullUrlFilter; } });
47
47
  var MissingRuntimeBaseUrlError_1 = require("./MissingRuntimeBaseUrlError");
48
48
  Object.defineProperty(exports, "MissingRuntimeBaseUrlError", { enumerable: true, get: function () { return MissingRuntimeBaseUrlError_1.MissingRuntimeBaseUrlError; } });
49
- // The two ways outbound auth refuses to send: a binding a contract's auth mode requires is absent.
50
- // Typed, so a delivery worker tells 'we are misconfigured' from 'the partner registered something
51
- // hostile' (SsrfRefusedError) without matching message text.
49
+ // The ONE way outbound auth refuses a contract: an endpoint declares no credential this client can
50
+ // send. Thrown by createRpcClient, before any call (#1362). Typed, so a host tells 'we are
51
+ // misconfigured' from 'the partner registered something hostile' (SsrfRefusedError) without
52
+ // matching message text.
52
53
  var OutboundAuthErrors_1 = require("./OutboundAuthErrors");
53
- Object.defineProperty(exports, "MissingSharedSecretError", { enumerable: true, get: function () { return OutboundAuthErrors_1.MissingSharedSecretError; } });
54
- Object.defineProperty(exports, "MissingVendorAuthMinterError", { enumerable: true, get: function () { return OutboundAuthErrors_1.MissingVendorAuthMinterError; } });
54
+ Object.defineProperty(exports, "MissingClientCredentialError", { enumerable: true, get: function () { return OutboundAuthErrors_1.MissingClientCredentialError; } });
55
55
  // vendorAuth(name), OUTBOUND role: the minter mints (and refreshes once on a 401) what this client
56
- // sends. Defined in @webpieces/core-context; bind VENDOR_AUTH_MINTER, or every outbound
57
- // vendorAuth(...) call throws rather than sending unauthenticated. The INBOUND VendorAuthVerifier is
56
+ // sends. Defined in @webpieces/core-context; bind VENDOR_AUTH_MINTER (its supportedVendorNames() listing the
57
+ // vendor), or createRpcClient refuses a contract whose endpoint has no other sendable credential —
58
+ // never an unauthenticated send. The INBOUND VendorAuthVerifier is
58
59
  // http-routing's, and no client ever depends on it (#1358).
59
60
  var core_context_1 = require("@webpieces/core-context");
60
61
  Object.defineProperty(exports, "VendorAuthMinter", { enumerable: true, get: function () { return core_context_1.VendorAuthMinter; } });
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;AACrB,8FAA8F;AAC9F,kDAAkD;AAAzC,uGAAA,UAAU,OAAA;AAEnB,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,2DAA8F;AAArF,8HAAA,wBAAwB,OAAA;AAAE,kIAAA,4BAA4B,OAAA;AAE/D,mGAAmG;AACnG,wFAAwF;AACxF,qGAAqG;AACrG,4DAA4D;AAC5D,wDAAgG;AAAvF,gHAAA,gBAAgB,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAAE,+GAAA,eAAe,OAAA;AAE9D,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,gEAA2D;AAAlD,gHAAA,YAAY,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', ClientRole.SERVER));\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', ClientRole.SERVER), [\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// ClientConfig's required `role` argument, re-exported so a client is set up from one import.\nexport { ClientRole } from '@webpieces/core-util';\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, MissingVendorAuthMinterError } from './OutboundAuthErrors';\n\n// vendorAuth(name), OUTBOUND role: the minter mints (and refreshes once on a 401) what this client\n// sends. Defined in @webpieces/core-context; bind VENDOR_AUTH_MINTER, or every outbound\n// vendorAuth(...) call throws rather than sending unauthenticated. The INBOUND VendorAuthVerifier is\n// http-routing's, and no client ever depends on it (#1358).\nexport { VendorAuthMinter, VENDOR_AUTH_MINTER, SignableRequest } from '@webpieces/core-context';\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 { WiringPolicy } 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;AACrB,8FAA8F;AAC9F,kDAAkD;AAAzC,uGAAA,UAAU,OAAA;AAEnB,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,2FAA2F;AAC3F,4FAA4F;AAC5F,yBAAyB;AACzB,2DAAoE;AAA3D,kIAAA,4BAA4B,OAAA;AAErC,mGAAmG;AACnG,6GAA6G;AAC7G,mGAAmG;AACnG,mEAAmE;AACnE,4DAA4D;AAC5D,wDAAgG;AAAvF,gHAAA,gBAAgB,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAAE,+GAAA,eAAe,OAAA;AAE9D,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,gEAA2D;AAAlD,gHAAA,YAAY,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', ClientRole.SERVER));\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', ClientRole.SERVER), [\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// ClientConfig's required `role` argument, re-exported so a client is set up from one import.\nexport { ClientRole } from '@webpieces/core-util';\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 ONE way outbound auth refuses a contract: an endpoint declares no credential this client can\n// send. Thrown by createRpcClient, before any call (#1362). Typed, so a host tells 'we are\n// misconfigured' from 'the partner registered something hostile' (SsrfRefusedError) without\n// matching message text.\nexport { MissingClientCredentialError } from './OutboundAuthErrors';\n\n// vendorAuth(name), OUTBOUND role: the minter mints (and refreshes once on a 401) what this client\n// sends. Defined in @webpieces/core-context; bind VENDOR_AUTH_MINTER (its supportedVendorNames() listing the\n// vendor), or createRpcClient refuses a contract whose endpoint has no other sendable credential —\n// never an unauthenticated send. The INBOUND VendorAuthVerifier is\n// http-routing's, and no client ever depends on it (#1358).\nexport { VendorAuthMinter, VENDOR_AUTH_MINTER, SignableRequest } from '@webpieces/core-context';\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 { WiringPolicy } from '@webpieces/http-client-core';\n"]}