@webpieces/http-client-node 0.4.699 → 0.4.701

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 (47) hide show
  1. package/README.md +105 -0
  2. package/package.json +5 -5
  3. package/src/AddressResolver.d.ts +33 -0
  4. package/src/AddressResolver.js +39 -0
  5. package/src/AddressResolver.js.map +1 -0
  6. package/src/ClientConfig.d.ts +32 -8
  7. package/src/ClientConfig.js +22 -5
  8. package/src/ClientConfig.js.map +1 -1
  9. package/src/ClientHttpFactory.d.ts +31 -2
  10. package/src/ClientHttpFactory.js +31 -3
  11. package/src/ClientHttpFactory.js.map +1 -1
  12. package/src/ContextBaseUrlFilter.d.ts +80 -0
  13. package/src/ContextBaseUrlFilter.js +95 -0
  14. package/src/ContextBaseUrlFilter.js.map +1 -0
  15. package/src/CreateRpcClientCompileAssertions.d.ts +17 -0
  16. package/src/CreateRpcClientCompileAssertions.js +50 -0
  17. package/src/CreateRpcClientCompileAssertions.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/MissingRuntimeBaseUrlError.d.ts +20 -0
  22. package/src/MissingRuntimeBaseUrlError.js +28 -0
  23. package/src/MissingRuntimeBaseUrlError.js.map +1 -0
  24. package/src/NodeProxyClient.d.ts +33 -16
  25. package/src/NodeProxyClient.js +67 -32
  26. package/src/NodeProxyClient.js.map +1 -1
  27. package/src/OutboundAuthErrors.d.ts +42 -0
  28. package/src/OutboundAuthErrors.js +54 -0
  29. package/src/OutboundAuthErrors.js.map +1 -0
  30. package/src/OutboundAuthFilter.d.ts +56 -0
  31. package/src/OutboundAuthFilter.js +103 -0
  32. package/src/OutboundAuthFilter.js.map +1 -0
  33. package/src/SsrfGuardFilter.d.ts +61 -0
  34. package/src/SsrfGuardFilter.js +165 -0
  35. package/src/SsrfGuardFilter.js.map +1 -0
  36. package/src/SsrfPolicy.d.ts +58 -0
  37. package/src/SsrfPolicy.js +67 -0
  38. package/src/SsrfPolicy.js.map +1 -0
  39. package/src/SsrfRefusedError.d.ts +20 -0
  40. package/src/SsrfRefusedError.js +28 -0
  41. package/src/SsrfRefusedError.js.map +1 -0
  42. package/src/WebhookSignerCallback.d.ts +114 -0
  43. package/src/WebhookSignerCallback.js +106 -0
  44. package/src/WebhookSignerCallback.js.map +1 -0
  45. package/src/index.d.ts +17 -0
  46. package/src/index.js +43 -1
  47. package/src/index.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SsrfGuardFilter.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/SsrfGuardFilter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Filter, Service, toError } from '@webpieces/core-util';\nimport { ClientRequest } from '@webpieces/http-client-core';\nimport { AddressResolver } from './AddressResolver';\nimport { InternalAddressRules } from './InternalAddressRules';\nimport { SsrfPolicy } from './SsrfPolicy';\nimport { SsrfRefusedError } from './SsrfRefusedError';\n\n/**\n * Enforces {@link SsrfPolicy} on a call whose destination came from RUNTIME data.\n *\n * The moment the base URL is a database column a partner edited, \"do not let our process be used as\n * a proxy into our own network\" stops being the caller's job and becomes the framework's. Every\n * consumer was reinventing this, badly or not at all, and the one that did it best still only\n * managed `maxRedirects: 0` by hand.\n *\n * ## Installed on EVERY client; it costs nothing until a filter moves the request\n *\n * It sits beneath every app filter of every client this package builds, and the first thing it does\n * is ask `request.destinationCameFromData`. FALSE — the URL is what `ClientRegistry` resolved, an\n * address we chose — and it steps aside without parsing a URL or resolving a name, so an ordinary\n * service-to-service RPC runs exactly the code path it ran before this class existed. TRUE only\n * once something re-pointed the request, which is the one input a partner controls.\n *\n * That is why there is no per-client switch: an app cannot forget to turn the guard on for a client\n * that takes runtime URLs, and cannot turn it off for one — the ACT of re-pointing is the trigger.\n *\n * ## What it enforces, per hop\n *\n * 1. the URL parses and its scheme is allowed (https only, by default);\n * 2. the hostname is not internal BY NAME (`localhost`, `metadata.google.internal`, `*.internal`);\n * 3. EVERY address the hostname resolves to is public — one private answer among several condemns\n * the request, which is what makes DNS rebinding a refusal rather than a coin toss;\n * 4. redirects are not followed by the transport. The guard reads the `Location` itself, re-runs\n * (1)–(3) on it, and only then re-invokes the chain. A partner URL that 302s to\n * `http://169.254.169.254/…` is refused at the redirect, not obeyed — and because the whole\n * chain below re-runs, whatever signs the request signs it for the host it is actually going to.\n *\n * ## What it deliberately does NOT claim\n *\n * There is a TOCTOU window between resolving a name and the transport connecting: a hostile DNS\n * server can answer differently for the two lookups. Closing it properly means pinning the resolved\n * address into the socket, which node's transport gives no seam for. This guard raises the cost of\n * the attack a great deal and is honest about not eliminating it; egress firewalling remains the\n * control that actually cannot be tricked.\n */\nexport class SsrfGuardFilter extends Filter<ClientRequest, Response> {\n private readonly rules = new InternalAddressRules();\n\n constructor(\n private readonly policy: SsrfPolicy,\n private readonly addressResolver: AddressResolver,\n ) {\n super();\n }\n\n override async filter(request: ClientRequest, nextFilter: Service<ClientRequest, Response>): Promise<Response> {\n // The destination is still the one this client resolved for itself, so there is nothing\n // attacker-influenced to judge. Step aside entirely — no parse, no DNS, no redirect\n // interception — so the deployed-service path is byte-identical to having no guard at all.\n if (!request.destinationCameFromData) {\n return nextFilter.invoke(request);\n }\n await this.assertAllowed(request.url);\n\n // The transport must not follow a redirect on its own — that would be a hop this policy\n // never saw. We read the Location and judge it ourselves, below.\n request.followRedirects = false;\n\n let response = await nextFilter.invoke(request);\n for (let hop = 0; this.isRedirect(response); hop++) {\n if (hop >= this.policy.maxRedirects) {\n throw new SsrfRefusedError(\n `${request.contractName}.${request.route.methodName}: refused to follow more than ` +\n `${this.policy.maxRedirects} redirect(s) from a runtime-supplied destination. ` +\n `The last one was ${request.url}. A webhook endpoint that needs a longer redirect ` +\n `chain should be registered at its final URL instead.`,\n request.url,\n );\n }\n const target = this.redirectTargetOf(response, request);\n await this.assertAllowed(target);\n request.followRedirectTo(target);\n response = await nextFilter.invoke(request);\n }\n return response;\n }\n\n /**\n * @throws SsrfRefusedError naming what condemned the url and the ONE opt-out that would allow\n * it. Returns normally when the destination is acceptable.\n */\n private async assertAllowed(url: string): Promise<void> {\n const parsed = this.parse(url);\n if (!this.policy.allowedSchemes.has(parsed.protocol)) {\n const allowed = [...this.policy.allowedSchemes].join(', ');\n throw new SsrfRefusedError(\n `Refusing to send to ${url}: scheme '${parsed.protocol}' is not allowed (allowed: ${allowed}). ` +\n `A destination supplied at runtime must be reached over TLS — a plaintext hop leaks the ` +\n `payload and its signature to anything on the path.`,\n url,\n );\n }\n if (this.policy.allowInternalAddresses) {\n return;\n }\n const hostname = parsed.hostname.replace(/^\\[|\\]$/g, '');\n if (this.rules.isInternalHostname(hostname)) {\n throw this.refuseInternal(url, `'${hostname}' names infrastructure inside our own network`);\n }\n for (const address of await this.resolveOrRefuse(url, hostname)) {\n if (this.rules.isInternalAddress(address)) {\n throw this.refuseInternal(url, `it resolves to the internal address ${address}`);\n }\n }\n }\n\n private refuseInternal(url: string, because: string): SsrfRefusedError {\n return new SsrfRefusedError(\n `Refusing to send to ${url}: ${because}. A destination supplied at runtime is attacker-influenced ` +\n `data, so loopback, RFC1918, link-local and cloud-metadata addresses are refused — reaching ` +\n `169.254.169.254 would hand this process's own service-account tokens to whoever registered ` +\n `the URL. To reach one of OUR OWN services at a local port, register it — ` +\n `ClientRegistry.addUrlMapping('svc', 'http://localhost:8202') — and a registry-resolved ` +\n `URL is never judged here at all. If this client genuinely must dial an internal address ` +\n `from RUNTIME data (exercising the partner path against a local fake), say so at the ` +\n `construction site with ` +\n `new ContextBaseUrlFilter(new SsrfTestingPolicy('<why>')).`,\n url,\n );\n }\n\n /** Every address for `hostname`, or a refusal — a name that will not resolve is not a destination. */\n private async resolveOrRefuse(url: string, hostname: string): Promise<string[]> {\n // An IP LITERAL never went to DNS, so judge it directly; hostnames go to the resolver.\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(hostname) || hostname.includes(':')) {\n return [hostname];\n }\n // webpieces-disable no-unmanaged-exceptions -- turn an unresolvable name into the SAME refusal a bad address gets\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await this.addressResolver.resolve(hostname);\n } catch (err: unknown) {\n const error = toError(err);\n throw new SsrfRefusedError(\n `Refusing to send to ${url}: '${hostname}' did not resolve (${error.message}). ` +\n `An unresolvable destination is refused rather than attempted, so a partner row that has ` +\n `gone stale fails as a delivery error instead of as a network timeout.`,\n url,\n );\n }\n }\n\n private parse(url: string): URL {\n // webpieces-disable no-unmanaged-exceptions -- an unparseable url is a refusal, in this filter's own vocabulary\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return new URL(url);\n } catch (err: unknown) {\n const error = toError(err);\n throw new SsrfRefusedError(\n `Refusing to send to '${url}': it is not a parseable absolute URL (${error.message}). ` +\n `A runtime-supplied base URL must be absolute, e.g. 'https://api.partner.example'.`,\n url,\n );\n }\n }\n\n private isRedirect(response: Response): boolean {\n return response.status >= 300 && response.status < 400 && response.headers.get('location') !== null;\n }\n\n /** The redirect's absolute target, resolving a relative Location against the URL we just called. */\n private redirectTargetOf(response: Response, request: ClientRequest): string {\n const location = response.headers.get('location') ?? '';\n // webpieces-disable no-unmanaged-exceptions -- a malformed Location is a refusal, in this filter's own vocabulary\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return new URL(location, request.url).toString();\n } catch (err: unknown) {\n const error = toError(err);\n throw new SsrfRefusedError(\n `Refusing to follow the redirect from ${request.url}: its Location header ` +\n `'${location}' is not a usable URL (${error.message}).`,\n request.url,\n );\n }\n }\n}\n"]}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * What a destination supplied at RUNTIME has to satisfy before this client will send to it: HTTPS
3
+ * only, no internal addresses, at most one redirect — each hop re-judged.
4
+ *
5
+ * DATA ONLY — the enforcement is {@link SsrfGuardFilter}, and the trigger is
6
+ * `ClientRequest.destinationCameFromData`: a URL that came out of `ClientRegistry` is an address WE
7
+ * chose and is never judged, so an ordinary service-to-service client pays nothing for this class
8
+ * existing.
9
+ *
10
+ * THIS class is what a client gets by saying nothing, and it is the whole policy — there is no
11
+ * argument to soften, no scheme list to widen, no flag to flip. Relaxing it means naming a
12
+ * DIFFERENT class, {@link SsrfTestingPolicy}, which is why that one carries a required reason.
13
+ */
14
+ export declare class SsrfPolicy {
15
+ /**
16
+ * URL schemes that may be sent to, with their colons. HTTPS-only: a partner-registered
17
+ * destination reached over plaintext http leaks the payload and its signature to anything on the
18
+ * path, and "the partner has not got TLS yet" is their bug to fix, not ours to accommodate
19
+ * silently.
20
+ */
21
+ readonly allowedSchemes: ReadonlySet<string>;
22
+ /** When true, the loopback / RFC1918 / link-local / metadata refusals are skipped. */
23
+ readonly allowInternalAddresses: boolean;
24
+ /**
25
+ * How many redirects may be followed, each one re-judged under this same policy. Small on
26
+ * purpose: a legitimate webhook endpoint does not need a redirect chain, and every hop is
27
+ * another chance for the destination to move somewhere we did not agree to.
28
+ */
29
+ readonly maxRedirects: number;
30
+ /** WHY internal addresses are allowed, in prose, when they are. See {@link SsrfTestingPolicy}. */
31
+ readonly allowInternalReason: string | undefined;
32
+ }
33
+ /**
34
+ * {@link SsrfPolicy} with plaintext http and internal addresses ALLOWED, for the one case that
35
+ * genuinely needs them: exercising the partner delivery path against a local fake, where the
36
+ * per-call URL is `http://127.0.0.1:9123`.
37
+ *
38
+ * This is NOT the way to reach a local emulator by service name. `ClientRegistry.addMapping` already
39
+ * covers that, and a registry-resolved URL is never SSRF-checked in the first place — so a localhost
40
+ * peer needs no opt-out at all, and anyone reaching for this class to get one is in the wrong place.
41
+ *
42
+ * THE LONG NAME IS THE FEATURE. This is the permissive branch, so it is a NOUN a reviewer can grep —
43
+ * `grep -rn SsrfTestingPolicy` lists every client in a codebase that can reach inside the network
44
+ * with a runtime-supplied URL — rather than a boolean, an omitted argument, or an empty allow-list. A
45
+ * widening that reads as an ABSENCE is invisible exactly where it matters most.
46
+ */
47
+ export declare class SsrfTestingPolicy extends SsrfPolicy {
48
+ readonly allowedSchemes: ReadonlySet<string>;
49
+ readonly allowInternalAddresses: boolean;
50
+ readonly allowInternalReason: string;
51
+ constructor(
52
+ /**
53
+ * WHY this client may reach internal addresses, in prose. REQUIRED, and quoted back in this
54
+ * client's refusals, so the justification travels with the decision instead of living in a
55
+ * commit message nobody will find.
56
+ */
57
+ reason: string);
58
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SsrfTestingPolicy = exports.SsrfPolicy = void 0;
4
+ /**
5
+ * What a destination supplied at RUNTIME has to satisfy before this client will send to it: HTTPS
6
+ * only, no internal addresses, at most one redirect — each hop re-judged.
7
+ *
8
+ * DATA ONLY — the enforcement is {@link SsrfGuardFilter}, and the trigger is
9
+ * `ClientRequest.destinationCameFromData`: a URL that came out of `ClientRegistry` is an address WE
10
+ * chose and is never judged, so an ordinary service-to-service client pays nothing for this class
11
+ * existing.
12
+ *
13
+ * THIS class is what a client gets by saying nothing, and it is the whole policy — there is no
14
+ * argument to soften, no scheme list to widen, no flag to flip. Relaxing it means naming a
15
+ * DIFFERENT class, {@link SsrfTestingPolicy}, which is why that one carries a required reason.
16
+ */
17
+ class SsrfPolicy {
18
+ /**
19
+ * URL schemes that may be sent to, with their colons. HTTPS-only: a partner-registered
20
+ * destination reached over plaintext http leaks the payload and its signature to anything on the
21
+ * path, and "the partner has not got TLS yet" is their bug to fix, not ours to accommodate
22
+ * silently.
23
+ */
24
+ allowedSchemes = new Set(['https:']);
25
+ /** When true, the loopback / RFC1918 / link-local / metadata refusals are skipped. */
26
+ allowInternalAddresses = false;
27
+ /**
28
+ * How many redirects may be followed, each one re-judged under this same policy. Small on
29
+ * purpose: a legitimate webhook endpoint does not need a redirect chain, and every hop is
30
+ * another chance for the destination to move somewhere we did not agree to.
31
+ */
32
+ maxRedirects = 1;
33
+ /** WHY internal addresses are allowed, in prose, when they are. See {@link SsrfTestingPolicy}. */
34
+ allowInternalReason = undefined;
35
+ }
36
+ exports.SsrfPolicy = SsrfPolicy;
37
+ /**
38
+ * {@link SsrfPolicy} with plaintext http and internal addresses ALLOWED, for the one case that
39
+ * genuinely needs them: exercising the partner delivery path against a local fake, where the
40
+ * per-call URL is `http://127.0.0.1:9123`.
41
+ *
42
+ * This is NOT the way to reach a local emulator by service name. `ClientRegistry.addMapping` already
43
+ * covers that, and a registry-resolved URL is never SSRF-checked in the first place — so a localhost
44
+ * peer needs no opt-out at all, and anyone reaching for this class to get one is in the wrong place.
45
+ *
46
+ * THE LONG NAME IS THE FEATURE. This is the permissive branch, so it is a NOUN a reviewer can grep —
47
+ * `grep -rn SsrfTestingPolicy` lists every client in a codebase that can reach inside the network
48
+ * with a runtime-supplied URL — rather than a boolean, an omitted argument, or an empty allow-list. A
49
+ * widening that reads as an ABSENCE is invisible exactly where it matters most.
50
+ */
51
+ class SsrfTestingPolicy extends SsrfPolicy {
52
+ allowedSchemes = new Set(['https:', 'http:']);
53
+ allowInternalAddresses = true;
54
+ allowInternalReason;
55
+ constructor(
56
+ /**
57
+ * WHY this client may reach internal addresses, in prose. REQUIRED, and quoted back in this
58
+ * client's refusals, so the justification travels with the decision instead of living in a
59
+ * commit message nobody will find.
60
+ */
61
+ reason) {
62
+ super();
63
+ this.allowInternalReason = reason;
64
+ }
65
+ }
66
+ exports.SsrfTestingPolicy = SsrfTestingPolicy;
67
+ //# sourceMappingURL=SsrfPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SsrfPolicy.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/SsrfPolicy.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;GAYG;AACH,MAAa,UAAU;IACnB;;;;;OAKG;IACM,cAAc,GAAwB,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEnE,sFAAsF;IAC7E,sBAAsB,GAAY,KAAK,CAAC;IAEjD;;;;OAIG;IACM,YAAY,GAAW,CAAC,CAAC;IAElC,kGAAkG;IACzF,mBAAmB,GAAuB,SAAS,CAAC;CAChE;AArBD,gCAqBC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAa,iBAAkB,SAAQ,UAAU;IAC3B,cAAc,GAAwB,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IACnE,sBAAsB,GAAY,IAAI,CAAC;IACvC,mBAAmB,CAAS;IAE9C;IACI;;;;OAIG;IACH,MAAc;QAEd,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC;IACtC,CAAC;CACJ;AAhBD,8CAgBC","sourcesContent":["/**\n * What a destination supplied at RUNTIME has to satisfy before this client will send to it: HTTPS\n * only, no internal addresses, at most one redirect — each hop re-judged.\n *\n * DATA ONLY — the enforcement is {@link SsrfGuardFilter}, and the trigger is\n * `ClientRequest.destinationCameFromData`: a URL that came out of `ClientRegistry` is an address WE\n * chose and is never judged, so an ordinary service-to-service client pays nothing for this class\n * existing.\n *\n * THIS class is what a client gets by saying nothing, and it is the whole policy — there is no\n * argument to soften, no scheme list to widen, no flag to flip. Relaxing it means naming a\n * DIFFERENT class, {@link SsrfTestingPolicy}, which is why that one carries a required reason.\n */\nexport class SsrfPolicy {\n /**\n * URL schemes that may be sent to, with their colons. HTTPS-only: a partner-registered\n * destination reached over plaintext http leaks the payload and its signature to anything on the\n * path, and \"the partner has not got TLS yet\" is their bug to fix, not ours to accommodate\n * silently.\n */\n readonly allowedSchemes: ReadonlySet<string> = new Set(['https:']);\n\n /** When true, the loopback / RFC1918 / link-local / metadata refusals are skipped. */\n readonly allowInternalAddresses: boolean = false;\n\n /**\n * How many redirects may be followed, each one re-judged under this same policy. Small on\n * purpose: a legitimate webhook endpoint does not need a redirect chain, and every hop is\n * another chance for the destination to move somewhere we did not agree to.\n */\n readonly maxRedirects: number = 1;\n\n /** WHY internal addresses are allowed, in prose, when they are. See {@link SsrfTestingPolicy}. */\n readonly allowInternalReason: string | undefined = undefined;\n}\n\n/**\n * {@link SsrfPolicy} with plaintext http and internal addresses ALLOWED, for the one case that\n * genuinely needs them: exercising the partner delivery path against a local fake, where the\n * per-call URL is `http://127.0.0.1:9123`.\n *\n * This is NOT the way to reach a local emulator by service name. `ClientRegistry.addMapping` already\n * covers that, and a registry-resolved URL is never SSRF-checked in the first place — so a localhost\n * peer needs no opt-out at all, and anyone reaching for this class to get one is in the wrong place.\n *\n * THE LONG NAME IS THE FEATURE. This is the permissive branch, so it is a NOUN a reviewer can grep —\n * `grep -rn SsrfTestingPolicy` lists every client in a codebase that can reach inside the network\n * with a runtime-supplied URL — rather than a boolean, an omitted argument, or an empty allow-list. A\n * widening that reads as an ABSENCE is invisible exactly where it matters most.\n */\nexport class SsrfTestingPolicy extends SsrfPolicy {\n override readonly allowedSchemes: ReadonlySet<string> = new Set(['https:', 'http:']);\n override readonly allowInternalAddresses: boolean = true;\n override readonly allowInternalReason: string;\n\n constructor(\n /**\n * WHY this client may reach internal addresses, in prose. REQUIRED, and quoted back in this\n * client's refusals, so the justification travels with the decision instead of living in a\n * commit message nobody will find.\n */\n reason: string,\n ) {\n super();\n this.allowInternalReason = reason;\n }\n}\n"]}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * We refused to SEND a request, because the destination failed this client's SSRF policy.
3
+ *
4
+ * Its own type rather than a bare Error so an app can tell "the partner's endpoint rejected us"
5
+ * (an HttpError carrying their status) from "we never contacted the partner at all" — those are
6
+ * different incidents with different owners, and only the second one means the URL in our database
7
+ * is hostile or wrong. A delivery worker typically dead-letters this instead of retrying: no number
8
+ * of retries makes 127.0.0.1 an acceptable destination.
9
+ *
10
+ * The message names the URL, the address it resolved to when that is what condemned it, and the
11
+ * ONE named opt-out that would allow it — so the reader does not have to go looking for whether an
12
+ * escape exists.
13
+ */
14
+ export declare class SsrfRefusedError extends Error {
15
+ /** The URL that was refused, verbatim, so a log line identifies the offending row. */
16
+ readonly refusedUrl: string;
17
+ constructor(message: string,
18
+ /** The URL that was refused, verbatim, so a log line identifies the offending row. */
19
+ refusedUrl: string);
20
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SsrfRefusedError = void 0;
4
+ /**
5
+ * We refused to SEND a request, because the destination failed this client's SSRF policy.
6
+ *
7
+ * Its own type rather than a bare Error so an app can tell "the partner's endpoint rejected us"
8
+ * (an HttpError carrying their status) from "we never contacted the partner at all" — those are
9
+ * different incidents with different owners, and only the second one means the URL in our database
10
+ * is hostile or wrong. A delivery worker typically dead-letters this instead of retrying: no number
11
+ * of retries makes 127.0.0.1 an acceptable destination.
12
+ *
13
+ * The message names the URL, the address it resolved to when that is what condemned it, and the
14
+ * ONE named opt-out that would allow it — so the reader does not have to go looking for whether an
15
+ * escape exists.
16
+ */
17
+ class SsrfRefusedError extends Error {
18
+ refusedUrl;
19
+ constructor(message,
20
+ /** The URL that was refused, verbatim, so a log line identifies the offending row. */
21
+ refusedUrl) {
22
+ super(message);
23
+ this.refusedUrl = refusedUrl;
24
+ this.name = 'SsrfRefusedError';
25
+ }
26
+ }
27
+ exports.SsrfRefusedError = SsrfRefusedError;
28
+ //# sourceMappingURL=SsrfRefusedError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SsrfRefusedError.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/SsrfRefusedError.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;GAYG;AACH,MAAa,gBAAiB,SAAQ,KAAK;IAInB;IAHpB,YACI,OAAe;IACf,sFAAsF;IACtE,UAAkB;QAElC,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,eAAU,GAAV,UAAU,CAAQ;QAGlC,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACnC,CAAC;CACJ;AATD,4CASC","sourcesContent":["/**\n * We refused to SEND a request, because the destination failed this client's SSRF policy.\n *\n * Its own type rather than a bare Error so an app can tell \"the partner's endpoint rejected us\"\n * (an HttpError carrying their status) from \"we never contacted the partner at all\" — those are\n * different incidents with different owners, and only the second one means the URL in our database\n * is hostile or wrong. A delivery worker typically dead-letters this instead of retrying: no number\n * of retries makes 127.0.0.1 an acceptable destination.\n *\n * The message names the URL, the address it resolved to when that is what condemned it, and the\n * ONE named opt-out that would allow it — so the reader does not have to go looking for whether an\n * escape exists.\n */\nexport class SsrfRefusedError extends Error {\n constructor(\n message: string,\n /** The URL that was refused, verbatim, so a log line identifies the offending row. */\n public readonly refusedUrl: string,\n ) {\n super(message);\n this.name = 'SsrfRefusedError';\n }\n}\n"]}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The OUTBOUND half of `@AuthWebhook(name)` — the exact mirror of `WebhookAuthCallback`, which is
3
+ * the INBOUND half.
4
+ *
5
+ * `@AuthWebhook('partner-hmac')` on a contract says: this hop is authenticated by a signature over
6
+ * the request, in a scheme the FRAMEWORK does not know. Which side of it you are on decides which
7
+ * hook runs, and nothing else changes:
8
+ *
9
+ * | | who signs | who verifies | the hook |
10
+ * |---|---|---|---|
11
+ * | a vendor posts to US | the vendor | us | `WebhookAuthCallback.verifyWebhook` (http-routing) |
12
+ * | WE post to a partner | us | the partner | `WebhookSignerCallback.sign` (here) |
13
+ *
14
+ * The symmetry is the point: ONE decorator on ONE contract describes the credential, and the same
15
+ * `name` selects the same vendor's scheme on both sides. A server that receives Twilio's callbacks
16
+ * and a client that delivers to a partner read identically.
17
+ *
18
+ * ## Why a hook and not an `@AuthHmac` decorator
19
+ *
20
+ * Because there is no such thing as "the" HMAC scheme. Twilio signs the full absolute URL with its
21
+ * sorted parameters; Slack signs `v0:{timestamp}:{body}`; Meta signs the raw body alone; GitHub and
22
+ * Stripe each differ again in prefix, header name and encoding. A decorator that took a secret
23
+ * would have to pick one of those and be wrong for everyone else, and a framework that shipped five
24
+ * vendors' crypto would be shipping five things to keep in step with five vendors. So the scheme
25
+ * lives in the app's hook, the VENDOR lives on the contract, and the framework carries neither.
26
+ *
27
+ * ## It FAILS CLOSED
28
+ *
29
+ * With no `WebhookSignerCallback` bound, every outbound `@AuthWebhook` call THROWS rather than
30
+ * going out unsigned — exactly as an unbound `WebhookAuthCallback` 401s every inbound
31
+ * `@AuthWebhook` endpoint. An unsigned delivery is not a degraded delivery; it is a request the
32
+ * partner is obliged to reject, and quietly sending one hides the missing binding until a partner
33
+ * complains.
34
+ *
35
+ * ```typescript
36
+ * // AppModule.ts
37
+ * options.bind(WEBHOOK_SIGNER_CALLBACK).to(PartnerHmacSigner);
38
+ * ```
39
+ *
40
+ * ONE hook serves EVERY partner, the way the inbound one serves every vendor: `name` selects which.
41
+ */
42
+ export declare abstract class WebhookSignerCallback {
43
+ /**
44
+ * Produce the headers that authenticate ONE outbound request, or throw to refuse to send it.
45
+ *
46
+ * @param name the string on the contract's `@AuthWebhook(name)` — which partner this is.
47
+ * @param request the FINAL request: the absolute url and the exact bytes that are about to go on
48
+ * the wire. See {@link SignableRequest} for why both of those words matter.
49
+ * @returns header name -> value, merged onto the request. An empty map is legal and means "this
50
+ * partner needs no header" — it is not a way to opt out of signing, because returning
51
+ * it is a statement the hook made rather than a binding somebody forgot.
52
+ */
53
+ abstract sign(name: string, request: SignableRequest): Promise<Map<string, string>>;
54
+ }
55
+ /**
56
+ * ONE outbound request, as the thing being signed. Data only.
57
+ *
58
+ * Every field a real vendor scheme needs is here, and the two that make it correct are {@link url}
59
+ * and {@link body}:
60
+ *
61
+ * - {@link url} is the FINAL absolute URL, after every app filter has had its say. A signature
62
+ * computed over the pre-filter URL authenticates a request nobody sent — and for a partner-webhook
63
+ * client the pre-filter URL is the client's own service name, which is not a destination at all.
64
+ * - {@link body} is the EXACT serialized bytes the transport will send, not the DTO. Serialization
65
+ * happens before the filter chain runs and the transport sends this same string verbatim, so the
66
+ * bytes signed and the bytes sent cannot differ. That was impossible while the client owned
67
+ * serialization internally with no seam, which is why outbound webhook senders were forced back
68
+ * to hand-rolling `JSON.stringify` plus a raw HTTP library — a library that re-serializes
69
+ * internally signs one byte sequence and sends another, and the failure is silent.
70
+ */
71
+ export declare class SignableRequest {
72
+ /** The FINAL absolute URL this request is about to be sent to. */
73
+ readonly url: string;
74
+ /** The HTTP method, e.g. 'POST'. */
75
+ readonly httpMethod: string;
76
+ /** The EXACT serialized body, or undefined for a call with no argument. */
77
+ readonly body: string | undefined;
78
+ /**
79
+ * The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a
80
+ * timestamp another filter set, a content-type) reads them, and the signature it returns is
81
+ * merged by the caller — so there is exactly one place headers are added and it is the
82
+ * return value.
83
+ */
84
+ readonly headers: ReadonlyMap<string, string>;
85
+ /** The API contract's class name, e.g. 'PartnerWebhookApi'. */
86
+ readonly contractName: string;
87
+ /** The contract method being called, e.g. 'deliver'. */
88
+ readonly methodName: string;
89
+ constructor(
90
+ /** The FINAL absolute URL this request is about to be sent to. */
91
+ url: string,
92
+ /** The HTTP method, e.g. 'POST'. */
93
+ httpMethod: string,
94
+ /** The EXACT serialized body, or undefined for a call with no argument. */
95
+ body: string | undefined,
96
+ /**
97
+ * The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a
98
+ * timestamp another filter set, a content-type) reads them, and the signature it returns is
99
+ * merged by the caller — so there is exactly one place headers are added and it is the
100
+ * return value.
101
+ */
102
+ headers: ReadonlyMap<string, string>,
103
+ /** The API contract's class name, e.g. 'PartnerWebhookApi'. */
104
+ contractName: string,
105
+ /** The contract method being called, e.g. 'deliver'. */
106
+ methodName: string);
107
+ }
108
+ /**
109
+ * DI identifier for the optional {@link WebhookSignerCallback} binding. It is a Symbol (not the
110
+ * class) so the app container's inversify autobind never auto-constructs this token, keeping
111
+ * `@optional() @inject(WEBHOOK_SIGNER_CALLBACK)` correct — undefined when unbound, which is what
112
+ * makes the fail-closed refusal reachable. Mirrors WEBHOOK_AUTH_CALLBACK exactly.
113
+ */
114
+ export declare const WEBHOOK_SIGNER_CALLBACK: unique symbol;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WEBHOOK_SIGNER_CALLBACK = exports.SignableRequest = exports.WebhookSignerCallback = void 0;
4
+ /**
5
+ * The OUTBOUND half of `@AuthWebhook(name)` — the exact mirror of `WebhookAuthCallback`, which is
6
+ * the INBOUND half.
7
+ *
8
+ * `@AuthWebhook('partner-hmac')` on a contract says: this hop is authenticated by a signature over
9
+ * the request, in a scheme the FRAMEWORK does not know. Which side of it you are on decides which
10
+ * hook runs, and nothing else changes:
11
+ *
12
+ * | | who signs | who verifies | the hook |
13
+ * |---|---|---|---|
14
+ * | a vendor posts to US | the vendor | us | `WebhookAuthCallback.verifyWebhook` (http-routing) |
15
+ * | WE post to a partner | us | the partner | `WebhookSignerCallback.sign` (here) |
16
+ *
17
+ * The symmetry is the point: ONE decorator on ONE contract describes the credential, and the same
18
+ * `name` selects the same vendor's scheme on both sides. A server that receives Twilio's callbacks
19
+ * and a client that delivers to a partner read identically.
20
+ *
21
+ * ## Why a hook and not an `@AuthHmac` decorator
22
+ *
23
+ * Because there is no such thing as "the" HMAC scheme. Twilio signs the full absolute URL with its
24
+ * sorted parameters; Slack signs `v0:{timestamp}:{body}`; Meta signs the raw body alone; GitHub and
25
+ * Stripe each differ again in prefix, header name and encoding. A decorator that took a secret
26
+ * would have to pick one of those and be wrong for everyone else, and a framework that shipped five
27
+ * vendors' crypto would be shipping five things to keep in step with five vendors. So the scheme
28
+ * lives in the app's hook, the VENDOR lives on the contract, and the framework carries neither.
29
+ *
30
+ * ## It FAILS CLOSED
31
+ *
32
+ * With no `WebhookSignerCallback` bound, every outbound `@AuthWebhook` call THROWS rather than
33
+ * going out unsigned — exactly as an unbound `WebhookAuthCallback` 401s every inbound
34
+ * `@AuthWebhook` endpoint. An unsigned delivery is not a degraded delivery; it is a request the
35
+ * partner is obliged to reject, and quietly sending one hides the missing binding until a partner
36
+ * complains.
37
+ *
38
+ * ```typescript
39
+ * // AppModule.ts
40
+ * options.bind(WEBHOOK_SIGNER_CALLBACK).to(PartnerHmacSigner);
41
+ * ```
42
+ *
43
+ * ONE hook serves EVERY partner, the way the inbound one serves every vendor: `name` selects which.
44
+ */
45
+ class WebhookSignerCallback {
46
+ }
47
+ exports.WebhookSignerCallback = WebhookSignerCallback;
48
+ /**
49
+ * ONE outbound request, as the thing being signed. Data only.
50
+ *
51
+ * Every field a real vendor scheme needs is here, and the two that make it correct are {@link url}
52
+ * and {@link body}:
53
+ *
54
+ * - {@link url} is the FINAL absolute URL, after every app filter has had its say. A signature
55
+ * computed over the pre-filter URL authenticates a request nobody sent — and for a partner-webhook
56
+ * client the pre-filter URL is the client's own service name, which is not a destination at all.
57
+ * - {@link body} is the EXACT serialized bytes the transport will send, not the DTO. Serialization
58
+ * happens before the filter chain runs and the transport sends this same string verbatim, so the
59
+ * bytes signed and the bytes sent cannot differ. That was impossible while the client owned
60
+ * serialization internally with no seam, which is why outbound webhook senders were forced back
61
+ * to hand-rolling `JSON.stringify` plus a raw HTTP library — a library that re-serializes
62
+ * internally signs one byte sequence and sends another, and the failure is silent.
63
+ */
64
+ class SignableRequest {
65
+ url;
66
+ httpMethod;
67
+ body;
68
+ headers;
69
+ contractName;
70
+ methodName;
71
+ constructor(
72
+ /** The FINAL absolute URL this request is about to be sent to. */
73
+ url,
74
+ /** The HTTP method, e.g. 'POST'. */
75
+ httpMethod,
76
+ /** The EXACT serialized body, or undefined for a call with no argument. */
77
+ body,
78
+ /**
79
+ * The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a
80
+ * timestamp another filter set, a content-type) reads them, and the signature it returns is
81
+ * merged by the caller — so there is exactly one place headers are added and it is the
82
+ * return value.
83
+ */
84
+ headers,
85
+ /** The API contract's class name, e.g. 'PartnerWebhookApi'. */
86
+ contractName,
87
+ /** The contract method being called, e.g. 'deliver'. */
88
+ methodName) {
89
+ this.url = url;
90
+ this.httpMethod = httpMethod;
91
+ this.body = body;
92
+ this.headers = headers;
93
+ this.contractName = contractName;
94
+ this.methodName = methodName;
95
+ }
96
+ }
97
+ exports.SignableRequest = SignableRequest;
98
+ /**
99
+ * DI identifier for the optional {@link WebhookSignerCallback} binding. It is a Symbol (not the
100
+ * class) so the app container's inversify autobind never auto-constructs this token, keeping
101
+ * `@optional() @inject(WEBHOOK_SIGNER_CALLBACK)` correct — undefined when unbound, which is what
102
+ * makes the fail-closed refusal reachable. Mirrors WEBHOOK_AUTH_CALLBACK exactly.
103
+ */
104
+ // webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)
105
+ exports.WEBHOOK_SIGNER_CALLBACK = Symbol.for('WebhookSignerCallback');
106
+ //# sourceMappingURL=WebhookSignerCallback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebhookSignerCallback.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/WebhookSignerCallback.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAsB,qBAAqB;CAY1C;AAZD,sDAYC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAa,eAAe;IAGJ;IAEA;IAEA;IAOA;IAEA;IAEA;IAjBpB;IACI,kEAAkE;IAClD,GAAW;IAC3B,oCAAoC;IACpB,UAAkB;IAClC,2EAA2E;IAC3D,IAAwB;IACxC;;;;;OAKG;IACa,OAAoC;IACpD,+DAA+D;IAC/C,YAAoB;IACpC,wDAAwD;IACxC,UAAkB;QAflB,QAAG,GAAH,GAAG,CAAQ;QAEX,eAAU,GAAV,UAAU,CAAQ;QAElB,SAAI,GAAJ,IAAI,CAAoB;QAOxB,YAAO,GAAP,OAAO,CAA6B;QAEpC,iBAAY,GAAZ,YAAY,CAAQ;QAEpB,eAAU,GAAV,UAAU,CAAQ;IACnC,CAAC;CACP;AApBD,0CAoBC;AAED;;;;;GAKG;AACH,mNAAmN;AACtM,QAAA,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC","sourcesContent":["/**\n * The OUTBOUND half of `@AuthWebhook(name)` — the exact mirror of `WebhookAuthCallback`, which is\n * the INBOUND half.\n *\n * `@AuthWebhook('partner-hmac')` on a contract says: this hop is authenticated by a signature over\n * the request, in a scheme the FRAMEWORK does not know. Which side of it you are on decides which\n * hook runs, and nothing else changes:\n *\n * | | who signs | who verifies | the hook |\n * |---|---|---|---|\n * | a vendor posts to US | the vendor | us | `WebhookAuthCallback.verifyWebhook` (http-routing) |\n * | WE post to a partner | us | the partner | `WebhookSignerCallback.sign` (here) |\n *\n * The symmetry is the point: ONE decorator on ONE contract describes the credential, and the same\n * `name` selects the same vendor's scheme on both sides. A server that receives Twilio's callbacks\n * and a client that delivers to a partner read identically.\n *\n * ## Why a hook and not an `@AuthHmac` decorator\n *\n * Because there is no such thing as \"the\" HMAC scheme. Twilio signs the full absolute URL with its\n * sorted parameters; Slack signs `v0:{timestamp}:{body}`; Meta signs the raw body alone; GitHub and\n * Stripe each differ again in prefix, header name and encoding. A decorator that took a secret\n * would have to pick one of those and be wrong for everyone else, and a framework that shipped five\n * vendors' crypto would be shipping five things to keep in step with five vendors. So the scheme\n * lives in the app's hook, the VENDOR lives on the contract, and the framework carries neither.\n *\n * ## It FAILS CLOSED\n *\n * With no `WebhookSignerCallback` bound, every outbound `@AuthWebhook` call THROWS rather than\n * going out unsigned — exactly as an unbound `WebhookAuthCallback` 401s every inbound\n * `@AuthWebhook` endpoint. An unsigned delivery is not a degraded delivery; it is a request the\n * partner is obliged to reject, and quietly sending one hides the missing binding until a partner\n * complains.\n *\n * ```typescript\n * // AppModule.ts\n * options.bind(WEBHOOK_SIGNER_CALLBACK).to(PartnerHmacSigner);\n * ```\n *\n * ONE hook serves EVERY partner, the way the inbound one serves every vendor: `name` selects which.\n */\nexport abstract class WebhookSignerCallback {\n /**\n * Produce the headers that authenticate ONE outbound request, or throw to refuse to send it.\n *\n * @param name the string on the contract's `@AuthWebhook(name)` — which partner this is.\n * @param request the FINAL request: the absolute url and the exact bytes that are about to go on\n * the wire. See {@link SignableRequest} for why both of those words matter.\n * @returns header name -> value, merged onto the request. An empty map is legal and means \"this\n * partner needs no header\" — it is not a way to opt out of signing, because returning\n * it is a statement the hook made rather than a binding somebody forgot.\n */\n abstract sign(name: string, request: SignableRequest): Promise<Map<string, string>>;\n}\n\n/**\n * ONE outbound request, as the thing being signed. Data only.\n *\n * Every field a real vendor scheme needs is here, and the two that make it correct are {@link url}\n * and {@link body}:\n *\n * - {@link url} is the FINAL absolute URL, after every app filter has had its say. A signature\n * computed over the pre-filter URL authenticates a request nobody sent — and for a partner-webhook\n * client the pre-filter URL is the client's own service name, which is not a destination at all.\n * - {@link body} is the EXACT serialized bytes the transport will send, not the DTO. Serialization\n * happens before the filter chain runs and the transport sends this same string verbatim, so the\n * bytes signed and the bytes sent cannot differ. That was impossible while the client owned\n * serialization internally with no seam, which is why outbound webhook senders were forced back\n * to hand-rolling `JSON.stringify` plus a raw HTTP library — a library that re-serializes\n * internally signs one byte sequence and sends another, and the failure is silent.\n */\nexport class SignableRequest {\n constructor(\n /** The FINAL absolute URL this request is about to be sent to. */\n public readonly url: string,\n /** The HTTP method, e.g. 'POST'. */\n public readonly httpMethod: string,\n /** The EXACT serialized body, or undefined for a call with no argument. */\n public readonly body: string | undefined,\n /**\n * The headers as they stand. READ-ONLY here: a scheme that signs existing headers (a\n * timestamp another filter set, a content-type) reads them, and the signature it returns is\n * merged by the caller — so there is exactly one place headers are added and it is the\n * return value.\n */\n public readonly headers: ReadonlyMap<string, string>,\n /** The API contract's class name, e.g. 'PartnerWebhookApi'. */\n public readonly contractName: string,\n /** The contract method being called, e.g. 'deliver'. */\n public readonly methodName: string,\n ) {}\n}\n\n/**\n * DI identifier for the optional {@link WebhookSignerCallback} binding. It is a Symbol (not the\n * class) so the app container's inversify autobind never auto-constructs this token, keeping\n * `@optional() @inject(WEBHOOK_SIGNER_CALLBACK)` correct — undefined when unbound, which is what\n * makes the fail-closed refusal reachable. Mirrors WEBHOOK_AUTH_CALLBACK exactly.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const WEBHOOK_SIGNER_CALLBACK = Symbol.for('WebhookSignerCallback');\n"]}
package/src/index.d.ts CHANGED
@@ -16,10 +16,27 @@
16
16
  * // inject the factory, then one client per contract
17
17
  * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));
18
18
  * const response = await server2.fetchValue(req);
19
+ *
20
+ * // a client whose destination arrives per call: ONE filter, and nothing else changes
21
+ * const partner = factory.createRpcClient(PartnerWebhookApi, new ClientConfig('partner-webhooks'), [
22
+ * new ClientFilterDefinition(1000, new ContextBaseUrlFilter()),
23
+ * ]);
19
24
  * ```
20
25
  */
21
26
  export { ClientHttpFactory } from './ClientHttpFactory';
22
27
  export { NodeProxyClient, NODE_PROXY_CLIENT_PROVIDER } from './NodeProxyClient';
23
28
  export { ClientConfig } from './ClientConfig';
29
+ export { ContextBaseUrlFilter } from './ContextBaseUrlFilter';
30
+ export { MissingRuntimeBaseUrlError } from './MissingRuntimeBaseUrlError';
31
+ export { MissingSharedSecretError, MissingWebhookSignerError } from './OutboundAuthErrors';
32
+ export { WebhookSignerCallback, SignableRequest, WEBHOOK_SIGNER_CALLBACK } from './WebhookSignerCallback';
33
+ export { SsrfPolicy, SsrfTestingPolicy } from './SsrfPolicy';
34
+ export { SsrfGuardFilter } from './SsrfGuardFilter';
35
+ export { SsrfRefusedError } from './SsrfRefusedError';
36
+ export { OutboundAuthFilter } from './OutboundAuthFilter';
37
+ export { InternalAddressRules } from './InternalAddressRules';
38
+ export { AddressResolver, DnsAddressResolver } from './AddressResolver';
24
39
  export { ProxyClient, ClientErrorTranslator, TranslatedFailure } from '@webpieces/http-client-core';
40
+ export { ClientRequest, ClientFilterDefinition } from '@webpieces/http-client-core';
41
+ export type { ClientFilter, ClientFilters } from '@webpieces/http-client-core';
25
42
  export type { ApiPrototype } from '@webpieces/http-client-core';
package/src/index.js CHANGED
@@ -17,10 +17,15 @@
17
17
  * // inject the factory, then one client per contract
18
18
  * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));
19
19
  * const response = await server2.fetchValue(req);
20
+ *
21
+ * // a client whose destination arrives per call: ONE filter, and nothing else changes
22
+ * const partner = factory.createRpcClient(PartnerWebhookApi, new ClientConfig('partner-webhooks'), [
23
+ * new ClientFilterDefinition(1000, new ContextBaseUrlFilter()),
24
+ * ]);
20
25
  * ```
21
26
  */
22
27
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.TranslatedFailure = exports.ClientErrorTranslator = exports.ProxyClient = exports.ClientConfig = exports.NODE_PROXY_CLIENT_PROVIDER = exports.NodeProxyClient = exports.ClientHttpFactory = void 0;
28
+ exports.ClientFilterDefinition = exports.ClientRequest = exports.TranslatedFailure = exports.ClientErrorTranslator = exports.ProxyClient = exports.DnsAddressResolver = exports.AddressResolver = exports.InternalAddressRules = exports.OutboundAuthFilter = exports.SsrfRefusedError = exports.SsrfGuardFilter = exports.SsrfTestingPolicy = exports.SsrfPolicy = exports.WEBHOOK_SIGNER_CALLBACK = exports.SignableRequest = exports.WebhookSignerCallback = exports.MissingWebhookSignerError = exports.MissingSharedSecretError = exports.MissingRuntimeBaseUrlError = exports.ContextBaseUrlFilter = exports.ClientConfig = exports.NODE_PROXY_CLIENT_PROVIDER = exports.NodeProxyClient = exports.ClientHttpFactory = void 0;
24
29
  var ClientHttpFactory_1 = require("./ClientHttpFactory");
25
30
  Object.defineProperty(exports, "ClientHttpFactory", { enumerable: true, get: function () { return ClientHttpFactory_1.ClientHttpFactory; } });
26
31
  var NodeProxyClient_1 = require("./NodeProxyClient");
@@ -28,9 +33,46 @@ Object.defineProperty(exports, "NodeProxyClient", { enumerable: true, get: funct
28
33
  Object.defineProperty(exports, "NODE_PROXY_CLIENT_PROVIDER", { enumerable: true, get: function () { return NodeProxyClient_1.NODE_PROXY_CLIENT_PROVIDER; } });
29
34
  var ClientConfig_1 = require("./ClientConfig");
30
35
  Object.defineProperty(exports, "ClientConfig", { enumerable: true, get: function () { return ClientConfig_1.ClientConfig; } });
36
+ // Install this on the ONE client whose destination is data. Installing it IS the opt-in; a client
37
+ // without it ignores an ambient OVERRIDE_BASE_URL entirely.
38
+ var ContextBaseUrlFilter_1 = require("./ContextBaseUrlFilter");
39
+ Object.defineProperty(exports, "ContextBaseUrlFilter", { enumerable: true, get: function () { return ContextBaseUrlFilter_1.ContextBaseUrlFilter; } });
40
+ var MissingRuntimeBaseUrlError_1 = require("./MissingRuntimeBaseUrlError");
41
+ Object.defineProperty(exports, "MissingRuntimeBaseUrlError", { enumerable: true, get: function () { return MissingRuntimeBaseUrlError_1.MissingRuntimeBaseUrlError; } });
42
+ // The two ways outbound auth refuses to send: a binding a contract's auth mode requires is absent.
43
+ // Typed, so a delivery worker tells 'we are misconfigured' from 'the partner registered something
44
+ // hostile' (SsrfRefusedError) without matching message text.
45
+ var OutboundAuthErrors_1 = require("./OutboundAuthErrors");
46
+ Object.defineProperty(exports, "MissingSharedSecretError", { enumerable: true, get: function () { return OutboundAuthErrors_1.MissingSharedSecretError; } });
47
+ Object.defineProperty(exports, "MissingWebhookSignerError", { enumerable: true, get: function () { return OutboundAuthErrors_1.MissingWebhookSignerError; } });
48
+ // The OUTBOUND half of @AuthWebhook(name) — the mirror of http-routing's WebhookAuthCallback. Bind
49
+ // one, or every outbound @AuthWebhook call throws rather than delivering unsigned.
50
+ var WebhookSignerCallback_1 = require("./WebhookSignerCallback");
51
+ Object.defineProperty(exports, "WebhookSignerCallback", { enumerable: true, get: function () { return WebhookSignerCallback_1.WebhookSignerCallback; } });
52
+ Object.defineProperty(exports, "SignableRequest", { enumerable: true, get: function () { return WebhookSignerCallback_1.SignableRequest; } });
53
+ Object.defineProperty(exports, "WEBHOOK_SIGNER_CALLBACK", { enumerable: true, get: function () { return WebhookSignerCallback_1.WEBHOOK_SIGNER_CALLBACK; } });
54
+ // The SSRF policy a re-pointed URL is judged under, and the refusal it produces. Automatic, armed by
55
+ // the ACT of re-pointing; the ONLY way to relax it is naming SsrfTestingPolicy at a call site.
56
+ var SsrfPolicy_1 = require("./SsrfPolicy");
57
+ Object.defineProperty(exports, "SsrfPolicy", { enumerable: true, get: function () { return SsrfPolicy_1.SsrfPolicy; } });
58
+ Object.defineProperty(exports, "SsrfTestingPolicy", { enumerable: true, get: function () { return SsrfPolicy_1.SsrfTestingPolicy; } });
59
+ var SsrfGuardFilter_1 = require("./SsrfGuardFilter");
60
+ Object.defineProperty(exports, "SsrfGuardFilter", { enumerable: true, get: function () { return SsrfGuardFilter_1.SsrfGuardFilter; } });
61
+ var SsrfRefusedError_1 = require("./SsrfRefusedError");
62
+ Object.defineProperty(exports, "SsrfRefusedError", { enumerable: true, get: function () { return SsrfRefusedError_1.SsrfRefusedError; } });
63
+ var OutboundAuthFilter_1 = require("./OutboundAuthFilter");
64
+ Object.defineProperty(exports, "OutboundAuthFilter", { enumerable: true, get: function () { return OutboundAuthFilter_1.OutboundAuthFilter; } });
65
+ var InternalAddressRules_1 = require("./InternalAddressRules");
66
+ Object.defineProperty(exports, "InternalAddressRules", { enumerable: true, get: function () { return InternalAddressRules_1.InternalAddressRules; } });
67
+ var AddressResolver_1 = require("./AddressResolver");
68
+ Object.defineProperty(exports, "AddressResolver", { enumerable: true, get: function () { return AddressResolver_1.AddressResolver; } });
69
+ Object.defineProperty(exports, "DnsAddressResolver", { enumerable: true, get: function () { return AddressResolver_1.DnsAddressResolver; } });
31
70
  // The isomorphic engine, re-exported so a server app needs one import.
32
71
  var http_client_core_1 = require("@webpieces/http-client-core");
33
72
  Object.defineProperty(exports, "ProxyClient", { enumerable: true, get: function () { return http_client_core_1.ProxyClient; } });
34
73
  Object.defineProperty(exports, "ClientErrorTranslator", { enumerable: true, get: function () { return http_client_core_1.ClientErrorTranslator; } });
35
74
  Object.defineProperty(exports, "TranslatedFailure", { enumerable: true, get: function () { return http_client_core_1.TranslatedFailure; } });
75
+ var http_client_core_2 = require("@webpieces/http-client-core");
76
+ Object.defineProperty(exports, "ClientRequest", { enumerable: true, get: function () { return http_client_core_2.ClientRequest; } });
77
+ Object.defineProperty(exports, "ClientFilterDefinition", { enumerable: true, get: function () { return http_client_core_2.ClientFilterDefinition; } });
36
78
  //# sourceMappingURL=index.js.map
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;;;;;;;;;;;;;;;;;;;GAmBG;;;AAEH,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,qDAAgF;AAAvE,kHAAA,eAAe,OAAA;AAAE,6HAAA,0BAA0B,OAAA;AACpD,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,uEAAuE;AACvE,gEAAoG;AAA3F,+GAAA,WAAW,OAAA;AAAE,yHAAA,qBAAqB,OAAA;AAAE,qHAAA,iBAAiB,OAAA","sourcesContent":["/**\n * @webpieces/http-client-node\n *\n * The SERVER-side HTTP client. Reads an API contract's decorators and generates type-safe HTTP\n * clients from it — the same contract the callee's controller implements.\n *\n * Node-only, so unlike @webpieces/http-client-browser it is fully inversify-wired and reads the\n * magic context straight out of the AsyncLocalStorage-backed RequestContext. There is no\n * ContextReader indirection, because a server has exactly one right answer, and a call made\n * OUTSIDE `RequestContext.run(...)` throws instead of silently dropping the trace.\n *\n * Usage:\n * ```typescript\n * import { ClientHttpFactory, ClientConfig } from '@webpieces/http-client-node';\n *\n * // inject the factory, then one client per contract\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n * const response = await server2.fetchValue(req);\n * ```\n */\n\nexport { ClientHttpFactory } from './ClientHttpFactory';\nexport { NodeProxyClient, NODE_PROXY_CLIENT_PROVIDER } from './NodeProxyClient';\nexport { ClientConfig } from './ClientConfig';\n\n// The isomorphic engine, re-exported so a server app needs one import.\nexport { ProxyClient, ClientErrorTranslator, TranslatedFailure } from '@webpieces/http-client-core';\nexport type { ApiPrototype } from '@webpieces/http-client-core';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAEH,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,qDAAgF;AAAvE,kHAAA,eAAe,OAAA;AAAE,6HAAA,0BAA0B,OAAA;AACpD,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,kGAAkG;AAClG,4DAA4D;AAC5D,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,2EAA0E;AAAjE,wIAAA,0BAA0B,OAAA;AAEnC,mGAAmG;AACnG,kGAAkG;AAClG,6DAA6D;AAC7D,2DAA2F;AAAlF,8HAAA,wBAAwB,OAAA;AAAE,+HAAA,yBAAyB,OAAA;AAE5D,mGAAmG;AACnG,mFAAmF;AACnF,iEAA0G;AAAjG,8HAAA,qBAAqB,OAAA;AAAE,wHAAA,eAAe,OAAA;AAAE,gIAAA,uBAAuB,OAAA;AAExE,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,gEAAoG;AAA3F,+GAAA,WAAW,OAAA;AAAE,yHAAA,qBAAqB,OAAA;AAAE,qHAAA,iBAAiB,OAAA;AAC9D,gEAAoF;AAA3E,iHAAA,aAAa,OAAA;AAAE,0HAAA,sBAAsB,OAAA","sourcesContent":["/**\n * @webpieces/http-client-node\n *\n * The SERVER-side HTTP client. Reads an API contract's decorators and generates type-safe HTTP\n * clients from it — the same contract the callee's controller implements.\n *\n * Node-only, so unlike @webpieces/http-client-browser it is fully inversify-wired and reads the\n * magic context straight out of the AsyncLocalStorage-backed RequestContext. There is no\n * ContextReader indirection, because a server has exactly one right answer, and a call made\n * OUTSIDE `RequestContext.run(...)` throws instead of silently dropping the trace.\n *\n * Usage:\n * ```typescript\n * import { ClientHttpFactory, ClientConfig } from '@webpieces/http-client-node';\n *\n * // inject the factory, then one client per contract\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n * const response = await server2.fetchValue(req);\n *\n * // a client whose destination arrives per call: ONE filter, and nothing else changes\n * const partner = factory.createRpcClient(PartnerWebhookApi, new ClientConfig('partner-webhooks'), [\n * new ClientFilterDefinition(1000, new ContextBaseUrlFilter()),\n * ]);\n * ```\n */\n\nexport { ClientHttpFactory } from './ClientHttpFactory';\nexport { NodeProxyClient, NODE_PROXY_CLIENT_PROVIDER } from './NodeProxyClient';\nexport { ClientConfig } from './ClientConfig';\n\n// Install this on the ONE client whose destination is data. Installing it IS the opt-in; a client\n// without it ignores an ambient OVERRIDE_BASE_URL entirely.\nexport { ContextBaseUrlFilter } from './ContextBaseUrlFilter';\nexport { MissingRuntimeBaseUrlError } from './MissingRuntimeBaseUrlError';\n\n// The two ways outbound auth refuses to send: a binding a contract's auth mode requires is absent.\n// Typed, so a delivery worker tells 'we are misconfigured' from 'the partner registered something\n// hostile' (SsrfRefusedError) without matching message text.\nexport { MissingSharedSecretError, MissingWebhookSignerError } from './OutboundAuthErrors';\n\n// The OUTBOUND half of @AuthWebhook(name) — the mirror of http-routing's WebhookAuthCallback. Bind\n// one, or every outbound @AuthWebhook call throws rather than delivering unsigned.\nexport { WebhookSignerCallback, SignableRequest, WEBHOOK_SIGNER_CALLBACK } from './WebhookSignerCallback';\n\n// The SSRF policy a re-pointed URL is judged under, and the refusal it produces. Automatic, armed by\n// the ACT of re-pointing; the ONLY way to relax it is naming SsrfTestingPolicy at a call site.\nexport { SsrfPolicy, SsrfTestingPolicy } from './SsrfPolicy';\nexport { SsrfGuardFilter } from './SsrfGuardFilter';\nexport { SsrfRefusedError } from './SsrfRefusedError';\nexport { OutboundAuthFilter } from './OutboundAuthFilter';\nexport { InternalAddressRules } from './InternalAddressRules';\nexport { AddressResolver, DnsAddressResolver } from './AddressResolver';\n\n// The isomorphic engine, re-exported so a server app needs one import.\nexport { ProxyClient, ClientErrorTranslator, TranslatedFailure } from '@webpieces/http-client-core';\nexport { ClientRequest, ClientFilterDefinition } from '@webpieces/http-client-core';\nexport type { ClientFilter, ClientFilters } from '@webpieces/http-client-core';\nexport type { ApiPrototype } from '@webpieces/http-client-core';\n"]}