@webpieces/http-client-node 0.4.698 → 0.4.700

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +75 -1
  2. package/package.json +5 -5
  3. package/src/AddressResolver.d.ts +23 -0
  4. package/src/AddressResolver.js +29 -0
  5. package/src/AddressResolver.js.map +1 -0
  6. package/src/ClientConfig.d.ts +41 -11
  7. package/src/ClientConfig.js +24 -7
  8. package/src/ClientConfig.js.map +1 -1
  9. package/src/ClientHttpFactory.d.ts +21 -4
  10. package/src/ClientHttpFactory.js +21 -5
  11. package/src/ClientHttpFactory.js.map +1 -1
  12. package/src/ContextBaseUrlOverrideFilter.d.ts +38 -0
  13. package/src/ContextBaseUrlOverrideFilter.js +57 -0
  14. package/src/ContextBaseUrlOverrideFilter.js.map +1 -0
  15. package/src/HostPolicy.d.ts +118 -0
  16. package/src/HostPolicy.js +169 -0
  17. package/src/HostPolicy.js.map +1 -0
  18. package/src/InternalAddressRules.d.ts +37 -0
  19. package/src/InternalAddressRules.js +132 -0
  20. package/src/InternalAddressRules.js.map +1 -0
  21. package/src/NodeProxyClient.d.ts +24 -6
  22. package/src/NodeProxyClient.js +30 -8
  23. package/src/NodeProxyClient.js.map +1 -1
  24. package/src/RuntimeHostErrors.d.ts +42 -0
  25. package/src/RuntimeHostErrors.js +54 -0
  26. package/src/RuntimeHostErrors.js.map +1 -0
  27. package/src/SsrfGuardFilter.d.ts +50 -0
  28. package/src/SsrfGuardFilter.js +146 -0
  29. package/src/SsrfGuardFilter.js.map +1 -0
  30. package/src/SsrfPolicy.d.ts +64 -0
  31. package/src/SsrfPolicy.js +52 -0
  32. package/src/SsrfPolicy.js.map +1 -0
  33. package/src/SsrfRefusedError.d.ts +20 -0
  34. package/src/SsrfRefusedError.js +28 -0
  35. package/src/SsrfRefusedError.js.map +1 -0
  36. package/src/index.d.ts +15 -1
  37. package/src/index.js +38 -2
  38. package/src/index.js.map +1 -1
package/README.md CHANGED
@@ -5,10 +5,18 @@ the Cloud Tasks twin — calling a method makes the HTTP request that contract d
5
5
 
6
6
  ```ts
7
7
  // inject the factory (a framework singleton), then one client per contract
8
- const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));
8
+ const server2 = factory.createRpcClient(
9
+ Server2Api,
10
+ new ClientConfig('server2', new DeployedServiceHost()),
11
+ [], // this client's outbound filters
12
+ );
9
13
  const res = await server2.fetchValue(req); // inside a RequestContext
10
14
  ```
11
15
 
16
+ Every `ClientConfig` states WHERE its requests go, because there are two kinds of destination and
17
+ they carry very different risk. `DeployedServiceHost` is the one above and is what almost every
18
+ client wants. See "A destination supplied at RUNTIME" below for the other.
19
+
12
20
  - `svcName` becomes a URL through `ClientRegistry.resolve` — ONE chain, the same one the browser
13
21
  client and Cloud Tasks run:
14
22
  1. a registered mapping wins: `ClientRegistry.addMapping(svcName, port)` (localhost) or
@@ -32,3 +40,69 @@ request-id chain loses the trace, and finding that out in production is worse th
32
40
  top-level server filter normally establishes the scope for you.
33
41
 
34
42
  The browser twin is [@webpieces/http-client-browser](../http-client-browser).
43
+
44
+ ## A destination supplied at RUNTIME
45
+
46
+ Some destinations are DATA, not deployment: a URL a partner registered (`OrganizationWebhook.url`),
47
+ an OAuth callback, a per-tenant or self-hosted host. There is no `svcName` to resolve and nothing to
48
+ register, but there IS a contract — the payload is fully specified and published to customers. Name
49
+ a runtime host policy and the base URL arrives per call:
50
+
51
+ ```ts
52
+ const partner = factory.createRpcClient(
53
+ PartnerWebhookApi,
54
+ new ClientConfig('partner-webhooks', new RuntimeHostFromContext(new DnsAddressResolver())),
55
+ [new ClientFilterDefinition(500, new HmacSigningFilter(secret))],
56
+ );
57
+
58
+ for (const webhook of webhooks) {
59
+ await RequestContext.run(() => {
60
+ RequestContext.putUntrusted(WebpiecesCoreHeaders.OVERRIDE_BASE_URL, webhook.url);
61
+ return partner.deliver(envelope);
62
+ });
63
+ }
64
+ ```
65
+
66
+ - **It cannot leak.** The override lives on the per-call request, never on the client, so one client
67
+ fans out across N partner URLs and each call goes exactly where its own scope said.
68
+ - **A `DeployedServiceHost` client IGNORES the key.** That is the point of making the policy a named
69
+ class: an ambient URL set for a partner delivery cannot re-point every other client in the same
70
+ request. `grep -rn RuntimeHostFromContext` lists every client that can be re-pointed at all.
71
+ - **SSRF policy is ON.** https only; loopback / RFC1918 / link-local / cloud-metadata refused, by
72
+ name AND by every address the name resolves to; redirects are not followed by the transport but
73
+ re-judged hop by hop, so a partner URL that 302s at `169.254.169.254` is refused rather than
74
+ obeyed. The ONLY way to relax it is to say so out loud:
75
+ `new RuntimeHostFromContextAllowingInternalAddresses('local emulator on 127.0.0.1', resolver)`.
76
+ - **`@AuthOidc` / `@AuthSharedSecret` endpoints are refused at bind time.** Both mint a credential
77
+ for a peer we chose; a destination that arrives per call has no honest audience, and minting one
78
+ would hand our credential to whoever registered the URL.
79
+ - **The hop is VISIBLE.** `svcName` is still required under a runtime policy: it is the identity the
80
+ destination gets on the runtime architecture graph, drawn as an external node of kind `runtime`.
81
+
82
+ ## Outbound filters
83
+
84
+ `createRpcClient`'s third argument is this client's OUTBOUND filter chain — the same `Filter` /
85
+ `Service` abstraction (from `@webpieces/core-util`) the server's inbound chain uses, pointed the
86
+ other way. A filter receives a mutable `ClientRequest` and returns the `Response`:
87
+
88
+ ```ts
89
+ class HmacSigningFilter extends Filter<ClientRequest, Response> {
90
+ constructor(private readonly secret: string) { super(); }
91
+
92
+ async filter(request: ClientRequest, next: Service<ClientRequest, Response>): Promise<Response> {
93
+ // request.body is the EXACT serialized bytes the transport will send — sign those.
94
+ const mac = createHmac('sha256', this.secret).update(request.body ?? '').digest('hex');
95
+ request.headers.set('x-signature', `sha256=${mac}`);
96
+ return next.invoke(request);
97
+ }
98
+ }
99
+ ```
100
+
101
+ Highest priority runs OUTERMOST, matching the server's `FilterMatcher`. The framework's own
102
+ built-ins occupy 1000 (the runtime base-URL override) and 900 (the SSRF guard), so an app filter
103
+ below them sees the destination already settled.
104
+
105
+ Signing over `request.body` is the whole reason this seam exists: the bytes a filter signs are the
106
+ bytes transmitted, byte for byte. Without it a sender has to hand-serialize and post the payload
107
+ itself, because a raw HTTP library that re-serializes internally signs one sequence and sends
108
+ another.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-client-node",
3
- "version": "0.4.698",
3
+ "version": "0.4.700",
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.698",
26
- "@webpieces/core-util": "0.4.698",
27
- "@webpieces/gcp-identity": "0.4.698",
28
- "@webpieces/http-client-core": "0.4.698",
25
+ "@webpieces/core-context": "0.4.700",
26
+ "@webpieces/core-util": "0.4.700",
27
+ "@webpieces/gcp-identity": "0.4.700",
28
+ "@webpieces/http-client-core": "0.4.700",
29
29
  "inversify": "7.10.4",
30
30
  "reflect-metadata": "0.2.2"
31
31
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Turns a hostname into the IP addresses it actually resolves to — the half of the SSRF check that
3
+ * has to touch the network, kept behind its own class so the policy itself is unit-testable without
4
+ * DNS and a test can hand the guard a resolver that answers whatever the case needs.
5
+ *
6
+ * ABSTRACT rather than an interface because it is a collaborator with behavior, and because
7
+ * @webpieces/core-mock and app tests substitute it by TYPE.
8
+ */
9
+ export declare abstract class AddressResolver {
10
+ /**
11
+ * Every address `hostname` resolves to, as strings. ALL of them matter: a hostname that answers
12
+ * with one public address and one 127.0.0.1 is the classic DNS-rebinding shape, and a guard that
13
+ * checks only the first answer waves it through.
14
+ *
15
+ * @throws Error when the name does not resolve. The guard treats that as a refusal, not as a
16
+ * pass — a destination we cannot even name is not one we should POST a payload to.
17
+ */
18
+ abstract resolve(hostname: string): Promise<string[]>;
19
+ }
20
+ /** The real one: node's DNS resolver, asking for every address family. */
21
+ export declare class DnsAddressResolver extends AddressResolver {
22
+ resolve(hostname: string): Promise<string[]>;
23
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DnsAddressResolver = exports.AddressResolver = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const promises_1 = require("node:dns/promises");
6
+ const core_context_1 = require("@webpieces/core-context");
7
+ /**
8
+ * Turns a hostname into the IP addresses it actually resolves to — the half of the SSRF check that
9
+ * has to touch the network, kept behind its own class so the policy itself is unit-testable without
10
+ * DNS and a test can hand the guard a resolver that answers whatever the case needs.
11
+ *
12
+ * ABSTRACT rather than an interface because it is a collaborator with behavior, and because
13
+ * @webpieces/core-mock and app tests substitute it by TYPE.
14
+ */
15
+ class AddressResolver {
16
+ }
17
+ exports.AddressResolver = AddressResolver;
18
+ /** The real one: node's DNS resolver, asking for every address family. */
19
+ let DnsAddressResolver = class DnsAddressResolver extends AddressResolver {
20
+ async resolve(hostname) {
21
+ const answers = await (0, promises_1.lookup)(hostname, { all: true, verbatim: true });
22
+ return answers.map((answer) => answer.address);
23
+ }
24
+ };
25
+ exports.DnsAddressResolver = DnsAddressResolver;
26
+ exports.DnsAddressResolver = DnsAddressResolver = tslib_1.__decorate([
27
+ (0, core_context_1.provideFrameworkSingleton)()
28
+ ], DnsAddressResolver);
29
+ //# sourceMappingURL=AddressResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AddressResolver.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/AddressResolver.ts"],"names":[],"mappings":";;;;AACA,gDAA2C;AAC3C,0DAAoE;AAEpE;;;;;;;GAOG;AACH,MAAsB,eAAe;CAUpC;AAVD,0CAUC;AAED,0EAA0E;AAEnE,IAAM,kBAAkB,GAAxB,MAAM,kBAAmB,SAAQ,eAAe;IAC1C,KAAK,CAAC,OAAO,CAAC,QAAgB;QACnC,MAAM,OAAO,GAAoB,MAAM,IAAA,iBAAM,EAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACvF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAqB,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAClE,CAAC;CACJ,CAAA;AALY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,wCAAyB,GAAE;GACf,kBAAkB,CAK9B","sourcesContent":["import type { LookupAddress } from 'node:dns';\nimport { lookup } from 'node:dns/promises';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\n\n/**\n * Turns a hostname into the IP addresses it actually resolves to — the half of the SSRF check that\n * has to touch the network, kept behind its own class so the policy itself is unit-testable without\n * DNS and a test can hand the guard a resolver that answers whatever the case needs.\n *\n * ABSTRACT rather than an interface because it is a collaborator with behavior, and because\n * @webpieces/core-mock and app tests substitute it by TYPE.\n */\nexport abstract class AddressResolver {\n /**\n * Every address `hostname` resolves to, as strings. ALL of them matter: a hostname that answers\n * with one public address and one 127.0.0.1 is the classic DNS-rebinding shape, and a guard that\n * checks only the first answer waves it through.\n *\n * @throws Error when the name does not resolve. The guard treats that as a refusal, not as a\n * pass — a destination we cannot even name is not one we should POST a payload to.\n */\n abstract resolve(hostname: string): Promise<string[]>;\n}\n\n/** The real one: node's DNS resolver, asking for every address family. */\n@provideFrameworkSingleton()\nexport class DnsAddressResolver extends AddressResolver {\n override async resolve(hostname: string): Promise<string[]> {\n const answers: LookupAddress[] = await lookup(hostname, { all: true, verbatim: true });\n return answers.map((answer: LookupAddress) => answer.address);\n }\n}\n"]}
@@ -1,26 +1,56 @@
1
+ import { HostPolicy } from './HostPolicy';
1
2
  /**
2
3
  * Per-client STATE for a server-side HTTP client — nothing else. A plain class; it extends nothing
3
4
  * and is unrelated to the browser package's ClientConfig, because the two answer "what URL?"
4
5
  * differently and share nothing worth a base class.
5
6
  *
6
7
  * Collaborators (RequestContextHeaders, Secrets) are NOT config: they are dependencies of
7
- * {@link NodeProxyClient} and are shared by every client the factory builds. This is the RPC twin
8
- * of cloudtasks-client's TaskClientConfig, and takes the same single field.
8
+ * {@link NodeProxyClient} and are shared by every client the factory builds. Outbound FILTERS are
9
+ * not config either — they are per-client collaborators an app constructs, so they are the third
10
+ * argument to `createRpcClient` rather than a field here.
9
11
  */
10
12
  export declare class ClientConfig {
11
13
  /**
12
- * The service name. On GCP the URL is DERIVED from it (same project, same region — the Cloud
13
- * Run service name, so you maintain no URL table), which works across demo/qa/prod. Anything
14
- * the derivation cannot describe a localhost port, another region/project, a non-Cloud-Run
15
- * hostis a `ClientRegistry` mapping registered at startup, NOT a per-client URL.
14
+ * The service name.
15
+ *
16
+ * Under {@link DeployedServiceHost} the URL is DERIVED from it (on GCP: same project, same
17
+ * regionthe Cloud Run service name, so you maintain no URL table), which works across
18
+ * demo/qa/prod. Anything the derivation cannot describe — a localhost port, another
19
+ * region/project, a non-Cloud-Run host — is a `ClientRegistry` mapping registered at
20
+ * startup, NOT a per-client URL.
21
+ *
22
+ * Under a runtime host policy nothing is derived from it, but it is still required and still
23
+ * load-bearing: it is the IDENTITY this outbound hop gets on the runtime architecture graph
24
+ * ('partner-webhooks'), which is what stops a partner delivery being an invisible edge.
16
25
  */
17
26
  readonly svcName: string;
27
+ /**
28
+ * WHERE this client's requests go: a service we deploy, or a host supplied per call at
29
+ * runtime. REQUIRED — see {@link HostPolicy} for why there is no default. The old
30
+ * one-argument `new ClientConfig('svc')` no longer compiles; write
31
+ * `new ClientConfig('svc', new DeployedServiceHost())` for the behaviour it used to have.
32
+ */
33
+ readonly hostPolicy: HostPolicy;
18
34
  constructor(
19
35
  /**
20
- * The service name. On GCP the URL is DERIVED from it (same project, same region — the Cloud
21
- * Run service name, so you maintain no URL table), which works across demo/qa/prod. Anything
22
- * the derivation cannot describe a localhost port, another region/project, a non-Cloud-Run
23
- * hostis a `ClientRegistry` mapping registered at startup, NOT a per-client URL.
36
+ * The service name.
37
+ *
38
+ * Under {@link DeployedServiceHost} the URL is DERIVED from it (on GCP: same project, same
39
+ * regionthe Cloud Run service name, so you maintain no URL table), which works across
40
+ * demo/qa/prod. Anything the derivation cannot describe — a localhost port, another
41
+ * region/project, a non-Cloud-Run host — is a `ClientRegistry` mapping registered at
42
+ * startup, NOT a per-client URL.
43
+ *
44
+ * Under a runtime host policy nothing is derived from it, but it is still required and still
45
+ * load-bearing: it is the IDENTITY this outbound hop gets on the runtime architecture graph
46
+ * ('partner-webhooks'), which is what stops a partner delivery being an invisible edge.
47
+ */
48
+ svcName: string,
49
+ /**
50
+ * WHERE this client's requests go: a service we deploy, or a host supplied per call at
51
+ * runtime. REQUIRED — see {@link HostPolicy} for why there is no default. The old
52
+ * one-argument `new ClientConfig('svc')` no longer compiles; write
53
+ * `new ClientConfig('svc', new DeployedServiceHost())` for the behaviour it used to have.
24
54
  */
25
- svcName: string);
55
+ hostPolicy: HostPolicy);
26
56
  }
@@ -7,20 +7,37 @@ exports.ClientConfig = void 0;
7
7
  * differently and share nothing worth a base class.
8
8
  *
9
9
  * Collaborators (RequestContextHeaders, Secrets) are NOT config: they are dependencies of
10
- * {@link NodeProxyClient} and are shared by every client the factory builds. This is the RPC twin
11
- * of cloudtasks-client's TaskClientConfig, and takes the same single field.
10
+ * {@link NodeProxyClient} and are shared by every client the factory builds. Outbound FILTERS are
11
+ * not config either — they are per-client collaborators an app constructs, so they are the third
12
+ * argument to `createRpcClient` rather than a field here.
12
13
  */
13
14
  class ClientConfig {
14
15
  svcName;
16
+ hostPolicy;
15
17
  constructor(
16
18
  /**
17
- * The service name. On GCP the URL is DERIVED from it (same project, same region — the Cloud
18
- * Run service name, so you maintain no URL table), which works across demo/qa/prod. Anything
19
- * the derivation cannot describe a localhost port, another region/project, a non-Cloud-Run
20
- * hostis a `ClientRegistry` mapping registered at startup, NOT a per-client URL.
19
+ * The service name.
20
+ *
21
+ * Under {@link DeployedServiceHost} the URL is DERIVED from it (on GCP: same project, same
22
+ * regionthe Cloud Run service name, so you maintain no URL table), which works across
23
+ * demo/qa/prod. Anything the derivation cannot describe — a localhost port, another
24
+ * region/project, a non-Cloud-Run host — is a `ClientRegistry` mapping registered at
25
+ * startup, NOT a per-client URL.
26
+ *
27
+ * Under a runtime host policy nothing is derived from it, but it is still required and still
28
+ * load-bearing: it is the IDENTITY this outbound hop gets on the runtime architecture graph
29
+ * ('partner-webhooks'), which is what stops a partner delivery being an invisible edge.
21
30
  */
22
- svcName) {
31
+ svcName,
32
+ /**
33
+ * WHERE this client's requests go: a service we deploy, or a host supplied per call at
34
+ * runtime. REQUIRED — see {@link HostPolicy} for why there is no default. The old
35
+ * one-argument `new ClientConfig('svc')` no longer compiles; write
36
+ * `new ClientConfig('svc', new DeployedServiceHost())` for the behaviour it used to have.
37
+ */
38
+ hostPolicy) {
23
39
  this.svcName = svcName;
40
+ this.hostPolicy = hostPolicy;
24
41
  }
25
42
  }
26
43
  exports.ClientConfig = ClientConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"ClientConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientConfig.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,MAAa,YAAY;IAQD;IAPpB;IACI;;;;;OAKG;IACa,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAChC,CAAC;CACP;AAVD,oCAUC","sourcesContent":["/**\n * Per-client STATE for a server-side HTTP client — nothing else. A plain class; it extends nothing\n * and is unrelated to the browser package's ClientConfig, because the two answer \"what URL?\"\n * differently and share nothing worth a base class.\n *\n * Collaborators (RequestContextHeaders, Secrets) are NOT config: they are dependencies of\n * {@link NodeProxyClient} and are shared by every client the factory builds. This is the RPC twin\n * of cloudtasks-client's TaskClientConfig, and takes the same single field.\n */\nexport class ClientConfig {\n constructor(\n /**\n * The service name. On GCP the URL is DERIVED from it (same project, same region — the Cloud\n * Run service name, so you maintain no URL table), which works across demo/qa/prod. Anything\n * the derivation cannot describe — a localhost port, another region/project, a non-Cloud-Run\n * host — is a `ClientRegistry` mapping registered at startup, NOT a per-client URL.\n */\n public readonly svcName: string,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"ClientConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientConfig.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;GASG;AACH,MAAa,YAAY;IAeD;IAOA;IArBpB;IACI;;;;;;;;;;;;OAYG;IACa,OAAe;IAC/B;;;;;OAKG;IACa,UAAsB;QAPtB,YAAO,GAAP,OAAO,CAAQ;QAOf,eAAU,GAAV,UAAU,CAAY;IACvC,CAAC;CACP;AAxBD,oCAwBC","sourcesContent":["import { HostPolicy } from './HostPolicy';\n\n/**\n * Per-client STATE for a server-side HTTP client — nothing else. A plain class; it extends nothing\n * and is unrelated to the browser package's ClientConfig, because the two answer \"what URL?\"\n * differently and share nothing worth a base class.\n *\n * Collaborators (RequestContextHeaders, Secrets) are NOT config: they are dependencies of\n * {@link NodeProxyClient} and are shared by every client the factory builds. Outbound FILTERS are\n * not config either — they are per-client collaborators an app constructs, so they are the third\n * argument to `createRpcClient` rather than a field here.\n */\nexport class ClientConfig {\n constructor(\n /**\n * The service name.\n *\n * Under {@link DeployedServiceHost} the URL is DERIVED from it (on GCP: same project, same\n * region — the Cloud Run service name, so you maintain no URL table), which works across\n * demo/qa/prod. Anything the derivation cannot describe — a localhost port, another\n * region/project, a non-Cloud-Run host — is a `ClientRegistry` mapping registered at\n * startup, NOT a per-client URL.\n *\n * Under a runtime host policy nothing is derived from it, but it is still required and still\n * load-bearing: it is the IDENTITY this outbound hop gets on the runtime architecture graph\n * ('partner-webhooks'), which is what stops a partner delivery being an invisible edge.\n */\n public readonly svcName: string,\n /**\n * WHERE this client's requests go: a service we deploy, or a host supplied per call at\n * runtime. REQUIRED — see {@link HostPolicy} for why there is no default. The old\n * one-argument `new ClientConfig('svc')` no longer compiles; write\n * `new ClientConfig('svc', new DeployedServiceHost())` for the behaviour it used to have.\n */\n public readonly hostPolicy: HostPolicy,\n ) {}\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { Provider } from '@webpieces/core-context';
2
2
  import type { ApiPrototype } from '@webpieces/http-client-core';
3
+ import { ClientFilterDefinition } from '@webpieces/http-client-core';
3
4
  import { ClientConfig } from './ClientConfig';
4
5
  import { NodeProxyClient } from './NodeProxyClient';
5
6
  /**
@@ -13,16 +14,26 @@ import { NodeProxyClient } from './NodeProxyClient';
13
14
  * Inject it and ask for a typed client per contract:
14
15
  * ```typescript
15
16
  * // same project + region as this container; the URL is derived, you maintain nothing
16
- * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));
17
+ * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2', new DeployedServiceHost()), []);
17
18
  *
18
19
  * // to reach somewhere derivation cannot describe (other region/project, non-Cloud-Run, localhost),
19
20
  * // register it once at startup — the client still carries only the svcName:
20
21
  * // ClientRegistry.addUrlMapping('legacy', 'https://legacy.corp');
21
- * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy'));
22
+ * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy', new DeployedServiceHost()), []);
22
23
  *
23
24
  * const response = await server2.fetchValue(req); // inside a RequestContext
24
25
  * ```
25
26
  *
27
+ * A destination that is DATA rather than deployment — a URL a partner registered — names a runtime
28
+ * host policy instead, and typically installs a signing filter over the exact bytes:
29
+ * ```typescript
30
+ * const partner = factory.createRpcClient(
31
+ * PartnerWebhookApi,
32
+ * new ClientConfig('partner-webhooks', new RuntimeHostFromContext(new DnsAddressResolver())),
33
+ * [new ClientFilterDefinition(500, new HmacSigningFilter(secret))],
34
+ * );
35
+ * ```
36
+ *
26
37
  * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the
27
38
  * injected `Provider<NodeProxyClient>` hands out a fresh one per contract, which `createRpcClient`
28
39
  * then `init`s. Their collaborators (RequestContextHeaders, Secrets) come from the container, so
@@ -39,7 +50,13 @@ export declare class ClientHttpFactory {
39
50
  * Create a type-safe RPC (HTTP) client for one API contract.
40
51
  *
41
52
  * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators
42
- * @param config - This client's state (its svcName)
53
+ * @param config - This client's state (its svcName and its host policy)
54
+ * @param filters - This client's OUTBOUND filters, each with the priority it runs at (highest
55
+ * OUTERMOST). They wrap the send, so a filter sees the final URL, may add headers, and
56
+ * may replace `ClientRequest.body` — the exact bytes transmitted, which is what makes
57
+ * signing possible without hand-serializing. Pass `[]` for a client that needs none;
58
+ * the argument is REQUIRED so "this client signs nothing" is written down rather than
59
+ * inferred from an omitted argument.
43
60
  */
44
- createRpcClient<T extends object>(apiPrototype: ApiPrototype<T>, config: ClientConfig): T;
61
+ createRpcClient<T extends object>(apiPrototype: ApiPrototype<T>, config: ClientConfig, filters: ClientFilterDefinition[]): T;
45
62
  }
@@ -21,16 +21,26 @@ const NodeProxyClient_1 = require("./NodeProxyClient");
21
21
  * Inject it and ask for a typed client per contract:
22
22
  * ```typescript
23
23
  * // same project + region as this container; the URL is derived, you maintain nothing
24
- * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));
24
+ * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2', new DeployedServiceHost()), []);
25
25
  *
26
26
  * // to reach somewhere derivation cannot describe (other region/project, non-Cloud-Run, localhost),
27
27
  * // register it once at startup — the client still carries only the svcName:
28
28
  * // ClientRegistry.addUrlMapping('legacy', 'https://legacy.corp');
29
- * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy'));
29
+ * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy', new DeployedServiceHost()), []);
30
30
  *
31
31
  * const response = await server2.fetchValue(req); // inside a RequestContext
32
32
  * ```
33
33
  *
34
+ * A destination that is DATA rather than deployment — a URL a partner registered — names a runtime
35
+ * host policy instead, and typically installs a signing filter over the exact bytes:
36
+ * ```typescript
37
+ * const partner = factory.createRpcClient(
38
+ * PartnerWebhookApi,
39
+ * new ClientConfig('partner-webhooks', new RuntimeHostFromContext(new DnsAddressResolver())),
40
+ * [new ClientFilterDefinition(500, new HmacSigningFilter(secret))],
41
+ * );
42
+ * ```
43
+ *
34
44
  * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the
35
45
  * injected `Provider<NodeProxyClient>` hands out a fresh one per contract, which `createRpcClient`
36
46
  * then `init`s. Their collaborators (RequestContextHeaders, Secrets) come from the container, so
@@ -49,13 +59,19 @@ let ClientHttpFactory = class ClientHttpFactory {
49
59
  * Create a type-safe RPC (HTTP) client for one API contract.
50
60
  *
51
61
  * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators
52
- * @param config - This client's state (its svcName)
62
+ * @param config - This client's state (its svcName and its host policy)
63
+ * @param filters - This client's OUTBOUND filters, each with the priority it runs at (highest
64
+ * OUTERMOST). They wrap the send, so a filter sees the final URL, may add headers, and
65
+ * may replace `ClientRequest.body` — the exact bytes transmitted, which is what makes
66
+ * signing possible without hand-serializing. Pass `[]` for a client that needs none;
67
+ * the argument is REQUIRED so "this client signs nothing" is written down rather than
68
+ * inferred from an omitted argument.
53
69
  */
54
- createRpcClient(apiPrototype, config) {
70
+ createRpcClient(apiPrototype, config, filters) {
55
71
  // Fresh instance per contract — NodeProxyClient is transient. init() binds it to this
56
72
  // contract + target; the collaborators already came from the container.
57
73
  const proxyClient = this.proxyClientProvider.get();
58
- proxyClient.init(apiPrototype, config);
74
+ proxyClient.init(apiPrototype, config, filters);
59
75
  return (0, http_client_core_1.buildClientProxy)(apiPrototype, proxyClient);
60
76
  }
61
77
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ClientHttpFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientHttpFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,oDAAsD;AACtD,0DAAqG;AAErG,kEAA+D;AAE/D,uDAAgF;AAEhF,iGAAiG;AACjG,6EAA6E;AAC7E,IAAA,oCAAqB,EAAC,4CAA0B,EAAE,iCAAe,CAAC,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAE+B;IADzD,YACyD,mBAA8C;QAA9C,wBAAmB,GAAnB,mBAAmB,CAA2B;IACpG,CAAC;IAEJ;;;;;OAKG;IACH,eAAe,CAAmB,YAA6B,EAAE,MAAoB;QACjF,sFAAsF;QACtF,wEAAwE;QACxE,MAAM,WAAW,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;QACnD,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QACvC,OAAO,IAAA,mCAAgB,EAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACvD,CAAC;CACJ,CAAA;AAlBY,8CAAiB;4BAAjB,iBAAiB;IAF7B,IAAA,0BAAc,GAAE;IAChB,IAAA,wCAAyB,GAAE;IAGnB,mBAAA,IAAA,kBAAM,EAAC,4CAA0B,CAAC,CAAA;6CAAuC,uBAAQ;GAF7E,iBAAiB,CAkB7B","sourcesContent":["import { inject } from 'inversify';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { Provider, bindFrameworkProvider, provideFrameworkSingleton } from '@webpieces/core-context';\nimport type { ApiPrototype } from '@webpieces/http-client-core';\nimport { buildClientProxy } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\nimport { NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient } from './NodeProxyClient';\n\n// Teach the container how to hand out fresh NodeProxyClients. NodeProxyClient is bound TRANSIENT\n// (@provideFrameworkTransient), so each provider.get() constructs a new one.\nbindFrameworkProvider(NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient);\n\n/**\n * ClientHttpFactory - builds type-safe HTTP clients from API prototypes carrying\n * @ApiPath/@Endpoint decorators. The SERVER-side factory.\n *\n * This is the client-side equivalent of ApiRoutingFactory:\n * - Server routing: ApiRoutingFactory reads decorators -> routes HTTP requests to controllers\n * - Server client: ClientHttpFactory reads decorators -> generates HTTP requests from method calls\n *\n * Inject it and ask for a typed client per contract:\n * ```typescript\n * // same project + region as this container; the URL is derived, you maintain nothing\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n *\n * // to reach somewhere derivation cannot describe (other region/project, non-Cloud-Run, localhost),\n * // register it once at startup — the client still carries only the svcName:\n * // ClientRegistry.addUrlMapping('legacy', 'https://legacy.corp');\n * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy'));\n *\n * const response = await server2.fetchValue(req); // inside a RequestContext\n * ```\n *\n * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the\n * injected `Provider<NodeProxyClient>` hands out a fresh one per contract, which `createRpcClient`\n * then `init`s. Their collaborators (RequestContextHeaders, Secrets) come from the container, so\n * the whole dependency graph is visible in this package's design.html.\n *\n * Unlike @webpieces/http-client-browser this package is Node-only, so the factory IS the inversify\n * entry point and the magic context is read straight from the RequestContext. A call made outside\n * `RequestContext.run(...)` throws rather than silently dropping the trace.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class ClientHttpFactory {\n constructor(\n @inject(NODE_PROXY_CLIENT_PROVIDER) private readonly proxyClientProvider: Provider<NodeProxyClient>,\n ) {}\n\n /**\n * Create a type-safe RPC (HTTP) client for one API contract.\n *\n * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators\n * @param config - This client's state (its svcName)\n */\n createRpcClient<T extends object>(apiPrototype: ApiPrototype<T>, config: ClientConfig): T {\n // Fresh instance per contract — NodeProxyClient is transient. init() binds it to this\n // contract + target; the collaborators already came from the container.\n const proxyClient = this.proxyClientProvider.get();\n proxyClient.init(apiPrototype, config);\n return buildClientProxy(apiPrototype, proxyClient);\n }\n}\n"]}
1
+ {"version":3,"file":"ClientHttpFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ClientHttpFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,oDAAsD;AACtD,0DAAqG;AAErG,kEAAuF;AAEvF,uDAAgF;AAEhF,iGAAiG;AACjG,6EAA6E;AAC7E,IAAA,oCAAqB,EAAC,4CAA0B,EAAE,iCAAe,CAAC,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAE+B;IADzD,YACyD,mBAA8C;QAA9C,wBAAmB,GAAnB,mBAAmB,CAA2B;IACpG,CAAC;IAEJ;;;;;;;;;;;OAWG;IACH,eAAe,CACX,YAA6B,EAC7B,MAAoB,EACpB,OAAiC;QAEjC,sFAAsF;QACtF,wEAAwE;QACxE,MAAM,WAAW,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;QACnD,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,IAAA,mCAAgB,EAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACvD,CAAC;CACJ,CAAA;AA5BY,8CAAiB;4BAAjB,iBAAiB;IAF7B,IAAA,0BAAc,GAAE;IAChB,IAAA,wCAAyB,GAAE;IAGnB,mBAAA,IAAA,kBAAM,EAAC,4CAA0B,CAAC,CAAA;6CAAuC,uBAAQ;GAF7E,iBAAiB,CA4B7B","sourcesContent":["import { inject } from 'inversify';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { Provider, bindFrameworkProvider, provideFrameworkSingleton } from '@webpieces/core-context';\nimport type { ApiPrototype } from '@webpieces/http-client-core';\nimport { buildClientProxy, ClientFilterDefinition } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\nimport { NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient } from './NodeProxyClient';\n\n// Teach the container how to hand out fresh NodeProxyClients. NodeProxyClient is bound TRANSIENT\n// (@provideFrameworkTransient), so each provider.get() constructs a new one.\nbindFrameworkProvider(NODE_PROXY_CLIENT_PROVIDER, NodeProxyClient);\n\n/**\n * ClientHttpFactory - builds type-safe HTTP clients from API prototypes carrying\n * @ApiPath/@Endpoint decorators. The SERVER-side factory.\n *\n * This is the client-side equivalent of ApiRoutingFactory:\n * - Server routing: ApiRoutingFactory reads decorators -> routes HTTP requests to controllers\n * - Server client: ClientHttpFactory reads decorators -> generates HTTP requests from method calls\n *\n * Inject it and ask for a typed client per contract:\n * ```typescript\n * // same project + region as this container; the URL is derived, you maintain nothing\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2', new DeployedServiceHost()), []);\n *\n * // to reach somewhere derivation cannot describe (other region/project, non-Cloud-Run, localhost),\n * // register it once at startup — the client still carries only the svcName:\n * // ClientRegistry.addUrlMapping('legacy', 'https://legacy.corp');\n * const legacy = factory.createRpcClient(LegacyApi, new ClientConfig('legacy', new DeployedServiceHost()), []);\n *\n * const response = await server2.fetchValue(req); // inside a RequestContext\n * ```\n *\n * A destination that is DATA rather than deployment — a URL a partner registered — names a runtime\n * host policy instead, and typically installs a signing filter over the exact bytes:\n * ```typescript\n * const partner = factory.createRpcClient(\n * PartnerWebhookApi,\n * new ClientConfig('partner-webhooks', new RuntimeHostFromContext(new DnsAddressResolver())),\n * [new ClientFilterDefinition(500, new HmacSigningFilter(secret))],\n * );\n * ```\n *\n * Every client it builds shares one {@link NodeProxyClient} *shape* but never one instance: the\n * injected `Provider<NodeProxyClient>` hands out a fresh one per contract, which `createRpcClient`\n * then `init`s. Their collaborators (RequestContextHeaders, Secrets) come from the container, so\n * the whole dependency graph is visible in this package's design.html.\n *\n * Unlike @webpieces/http-client-browser this package is Node-only, so the factory IS the inversify\n * entry point and the magic context is read straight from the RequestContext. A call made outside\n * `RequestContext.run(...)` throws rather than silently dropping the trace.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class ClientHttpFactory {\n constructor(\n @inject(NODE_PROXY_CLIENT_PROVIDER) private readonly proxyClientProvider: Provider<NodeProxyClient>,\n ) {}\n\n /**\n * Create a type-safe RPC (HTTP) client for one API contract.\n *\n * @param apiPrototype - The API prototype class with @ApiPath/@Endpoint decorators\n * @param config - This client's state (its svcName and its host policy)\n * @param filters - This client's OUTBOUND filters, each with the priority it runs at (highest\n * OUTERMOST). They wrap the send, so a filter sees the final URL, may add headers, and\n * may replace `ClientRequest.body` — the exact bytes transmitted, which is what makes\n * signing possible without hand-serializing. Pass `[]` for a client that needs none;\n * the argument is REQUIRED so \"this client signs nothing\" is written down rather than\n * inferred from an omitted argument.\n */\n createRpcClient<T extends object>(\n apiPrototype: ApiPrototype<T>,\n config: ClientConfig,\n filters: ClientFilterDefinition[],\n ): T {\n // Fresh instance per contract — NodeProxyClient is transient. init() binds it to this\n // contract + target; the collaborators already came from the container.\n const proxyClient = this.proxyClientProvider.get();\n proxyClient.init(apiPrototype, config, filters);\n return buildClientProxy(apiPrototype, proxyClient);\n }\n}\n"]}
@@ -0,0 +1,38 @@
1
+ import { Filter, Service } from '@webpieces/core-util';
2
+ import { ClientRequest } from '@webpieces/http-client-core';
3
+ /**
4
+ * Reads {@link WebpiecesCoreHeaders.OVERRIDE_BASE_URL} out of the ambient RequestContext and points
5
+ * THIS ONE CALL at it.
6
+ *
7
+ * This is the filter the whole runtime-base-URL feature is built out of, and expressing it as a
8
+ * filter rather than as a special case inside the transport is the point: information from OUTSIDE
9
+ * the call (a URL a partner registered, sitting in a database column) crosses into the send path
10
+ * through the same seam an app's own signing filter uses, and everything below it in the chain —
11
+ * the SSRF guard, the app's signature — sees the destination this filter chose rather than the one
12
+ * `ClientConfig` bound at construction.
13
+ *
14
+ * ## Scope, and why it cannot leak
15
+ *
16
+ * It mutates the per-call {@link ClientRequest} and nothing else. The client is untouched, so the
17
+ * next call through the same client starts from its configured host again; and the context entry is
18
+ * scoped to whatever `RequestContext.run(...)` the caller established, so a fan-out loop that sets a
19
+ * different URL per partner gets exactly the URL it set, per iteration.
20
+ *
21
+ * It is installed ONLY by the runtime host policies, so a client bound to a deployed service never
22
+ * reads the key at all. That is what stops an ambient value set for a partner delivery from silently
23
+ * re-pointing every other client in the same request at the partner's server.
24
+ *
25
+ * Priority {@link BASE_URL_OVERRIDE_PRIORITY} — the OUTERMOST framework filter, so the destination
26
+ * is settled before anything else looks at it.
27
+ */
28
+ export declare class ContextBaseUrlOverrideFilter extends Filter<ClientRequest, Response> {
29
+ filter(request: ClientRequest, nextFilter: Service<ClientRequest, Response>): Promise<Response>;
30
+ }
31
+ /**
32
+ * The priority the override filter runs at — OUTERMOST of everything, because every other filter's
33
+ * job depends on knowing where the call is going. Exported so an app can see what it is ordering
34
+ * against rather than guessing at a magic number.
35
+ */
36
+ export declare const BASE_URL_OVERRIDE_PRIORITY = 1000;
37
+ /** The SSRF guard's priority: immediately inside the override, so it judges the URL that won. */
38
+ export declare const SSRF_GUARD_PRIORITY = 900;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SSRF_GUARD_PRIORITY = exports.BASE_URL_OVERRIDE_PRIORITY = exports.ContextBaseUrlOverrideFilter = void 0;
4
+ const core_util_1 = require("@webpieces/core-util");
5
+ const core_context_1 = require("@webpieces/core-context");
6
+ const RuntimeHostErrors_1 = require("./RuntimeHostErrors");
7
+ /**
8
+ * Reads {@link WebpiecesCoreHeaders.OVERRIDE_BASE_URL} out of the ambient RequestContext and points
9
+ * THIS ONE CALL at it.
10
+ *
11
+ * This is the filter the whole runtime-base-URL feature is built out of, and expressing it as a
12
+ * filter rather than as a special case inside the transport is the point: information from OUTSIDE
13
+ * the call (a URL a partner registered, sitting in a database column) crosses into the send path
14
+ * through the same seam an app's own signing filter uses, and everything below it in the chain —
15
+ * the SSRF guard, the app's signature — sees the destination this filter chose rather than the one
16
+ * `ClientConfig` bound at construction.
17
+ *
18
+ * ## Scope, and why it cannot leak
19
+ *
20
+ * It mutates the per-call {@link ClientRequest} and nothing else. The client is untouched, so the
21
+ * next call through the same client starts from its configured host again; and the context entry is
22
+ * scoped to whatever `RequestContext.run(...)` the caller established, so a fan-out loop that sets a
23
+ * different URL per partner gets exactly the URL it set, per iteration.
24
+ *
25
+ * It is installed ONLY by the runtime host policies, so a client bound to a deployed service never
26
+ * reads the key at all. That is what stops an ambient value set for a partner delivery from silently
27
+ * re-pointing every other client in the same request at the partner's server.
28
+ *
29
+ * Priority {@link BASE_URL_OVERRIDE_PRIORITY} — the OUTERMOST framework filter, so the destination
30
+ * is settled before anything else looks at it.
31
+ */
32
+ class ContextBaseUrlOverrideFilter extends core_util_1.Filter {
33
+ async filter(request, nextFilter) {
34
+ const override = core_context_1.RequestContext.getUntrusted(core_util_1.WebpiecesCoreHeaders.OVERRIDE_BASE_URL);
35
+ if (override === undefined || override === '') {
36
+ throw new RuntimeHostErrors_1.MissingRuntimeBaseUrlError(`${request.contractName}.${request.route.methodName} is a RUNTIME-HOST client, so its ` +
37
+ `destination must be supplied per call, but no ` +
38
+ `WebpiecesCoreHeaders.OVERRIDE_BASE_URL was found in the RequestContext. Set it around ` +
39
+ `the call:\n` +
40
+ ` RequestContext.putUntrusted(WebpiecesCoreHeaders.OVERRIDE_BASE_URL, webhook.url);\n` +
41
+ `Refusing rather than falling back to a derived service URL is deliberate: a silent ` +
42
+ `fallback would send a partner's payload to one of our own services.`, `${request.contractName}.${request.route.methodName}`);
43
+ }
44
+ request.pointAtBaseUrl(override);
45
+ return nextFilter.invoke(request);
46
+ }
47
+ }
48
+ exports.ContextBaseUrlOverrideFilter = ContextBaseUrlOverrideFilter;
49
+ /**
50
+ * The priority the override filter runs at — OUTERMOST of everything, because every other filter's
51
+ * job depends on knowing where the call is going. Exported so an app can see what it is ordering
52
+ * against rather than guessing at a magic number.
53
+ */
54
+ exports.BASE_URL_OVERRIDE_PRIORITY = 1000;
55
+ /** The SSRF guard's priority: immediately inside the override, so it judges the URL that won. */
56
+ exports.SSRF_GUARD_PRIORITY = 900;
57
+ //# sourceMappingURL=ContextBaseUrlOverrideFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContextBaseUrlOverrideFilter.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/ContextBaseUrlOverrideFilter.ts"],"names":[],"mappings":";;;AAAA,oDAA6E;AAC7E,0DAAyD;AAEzD,2DAAiE;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAa,4BAA6B,SAAQ,kBAA+B;IACpE,KAAK,CAAC,MAAM,CAAC,OAAsB,EAAE,UAA4C;QACtF,MAAM,QAAQ,GAAG,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,iBAAiB,CAAC,CAAC;QACrF,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,8CAA0B,CAChC,GAAG,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,KAAK,CAAC,UAAU,oCAAoC;gBACnF,gDAAgD;gBAChD,wFAAwF;gBACxF,aAAa;gBACb,yFAAyF;gBACzF,qFAAqF;gBACrF,qEAAqE,EACzE,GAAG,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,KAAK,CAAC,UAAU,EAAE,CACxD,CAAC;QACN,CAAC;QACD,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACjC,OAAO,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;CACJ;AAlBD,oEAkBC;AAED;;;;GAIG;AACU,QAAA,0BAA0B,GAAG,IAAI,CAAC;AAE/C,iGAAiG;AACpF,QAAA,mBAAmB,GAAG,GAAG,CAAC","sourcesContent":["import { Filter, Service, WebpiecesCoreHeaders } from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\nimport { ClientRequest } from '@webpieces/http-client-core';\nimport { MissingRuntimeBaseUrlError } from './RuntimeHostErrors';\n\n/**\n * Reads {@link WebpiecesCoreHeaders.OVERRIDE_BASE_URL} out of the ambient RequestContext and points\n * THIS ONE CALL at it.\n *\n * This is the filter the whole runtime-base-URL feature is built out of, and expressing it as a\n * filter rather than as a special case inside the transport is the point: information from OUTSIDE\n * the call (a URL a partner registered, sitting in a database column) crosses into the send path\n * through the same seam an app's own signing filter uses, and everything below it in the chain —\n * the SSRF guard, the app's signature — sees the destination this filter chose rather than the one\n * `ClientConfig` bound at construction.\n *\n * ## Scope, and why it cannot leak\n *\n * It mutates the per-call {@link ClientRequest} and nothing else. The client is untouched, so the\n * next call through the same client starts from its configured host again; and the context entry is\n * scoped to whatever `RequestContext.run(...)` the caller established, so a fan-out loop that sets a\n * different URL per partner gets exactly the URL it set, per iteration.\n *\n * It is installed ONLY by the runtime host policies, so a client bound to a deployed service never\n * reads the key at all. That is what stops an ambient value set for a partner delivery from silently\n * re-pointing every other client in the same request at the partner's server.\n *\n * Priority {@link BASE_URL_OVERRIDE_PRIORITY} — the OUTERMOST framework filter, so the destination\n * is settled before anything else looks at it.\n */\nexport class ContextBaseUrlOverrideFilter extends Filter<ClientRequest, Response> {\n override async filter(request: ClientRequest, nextFilter: Service<ClientRequest, Response>): Promise<Response> {\n const override = RequestContext.getUntrusted(WebpiecesCoreHeaders.OVERRIDE_BASE_URL);\n if (override === undefined || override === '') {\n throw new MissingRuntimeBaseUrlError(\n `${request.contractName}.${request.route.methodName} is a RUNTIME-HOST client, so its ` +\n `destination must be supplied per call, but no ` +\n `WebpiecesCoreHeaders.OVERRIDE_BASE_URL was found in the RequestContext. Set it around ` +\n `the call:\\n` +\n ` RequestContext.putUntrusted(WebpiecesCoreHeaders.OVERRIDE_BASE_URL, webhook.url);\\n` +\n `Refusing rather than falling back to a derived service URL is deliberate: a silent ` +\n `fallback would send a partner's payload to one of our own services.`,\n `${request.contractName}.${request.route.methodName}`,\n );\n }\n request.pointAtBaseUrl(override);\n return nextFilter.invoke(request);\n }\n}\n\n/**\n * The priority the override filter runs at — OUTERMOST of everything, because every other filter's\n * job depends on knowing where the call is going. Exported so an app can see what it is ordering\n * against rather than guessing at a magic number.\n */\nexport const BASE_URL_OVERRIDE_PRIORITY = 1000;\n\n/** The SSRF guard's priority: immediately inside the override, so it judges the URL that won. */\nexport const SSRF_GUARD_PRIORITY = 900;\n"]}