@webpieces/http-client-node 0.4.700 → 0.4.702
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -51
- package/package.json +5 -5
- package/src/AddressResolver.d.ts +11 -1
- package/src/AddressResolver.js +12 -2
- package/src/AddressResolver.js.map +1 -1
- package/src/ClientConfig.d.ts +30 -36
- package/src/ClientConfig.js +21 -21
- package/src/ClientConfig.js.map +1 -1
- package/src/ClientHttpFactory.d.ts +30 -18
- package/src/ClientHttpFactory.js +29 -17
- package/src/ClientHttpFactory.js.map +1 -1
- package/src/ContextBaseUrlFilter.d.ts +80 -0
- package/src/ContextBaseUrlFilter.js +95 -0
- package/src/ContextBaseUrlFilter.js.map +1 -0
- package/src/CreateRpcClientCompileAssertions.d.ts +17 -0
- package/src/CreateRpcClientCompileAssertions.js +50 -0
- package/src/CreateRpcClientCompileAssertions.js.map +1 -0
- package/src/MissingRuntimeBaseUrlError.d.ts +20 -0
- package/src/MissingRuntimeBaseUrlError.js +28 -0
- package/src/MissingRuntimeBaseUrlError.js.map +1 -0
- package/src/NodeProxyClient.d.ts +26 -27
- package/src/NodeProxyClient.js +60 -47
- package/src/NodeProxyClient.js.map +1 -1
- package/src/OutboundAuthErrors.d.ts +42 -0
- package/src/OutboundAuthErrors.js +54 -0
- package/src/OutboundAuthErrors.js.map +1 -0
- package/src/OutboundAuthFilter.d.ts +56 -0
- package/src/OutboundAuthFilter.js +103 -0
- package/src/OutboundAuthFilter.js.map +1 -0
- package/src/SsrfGuardFilter.d.ts +11 -0
- package/src/SsrfGuardFilter.js +23 -4
- package/src/SsrfGuardFilter.js.map +1 -1
- package/src/SsrfPolicy.d.ts +39 -45
- package/src/SsrfPolicy.js +49 -34
- package/src/SsrfPolicy.js.map +1 -1
- package/src/WebhookSignerCallback.d.ts +114 -0
- package/src/WebhookSignerCallback.js +106 -0
- package/src/WebhookSignerCallback.js.map +1 -0
- package/src/index.d.ts +13 -10
- package/src/index.js +30 -24
- package/src/index.js.map +1 -1
- package/src/ContextBaseUrlOverrideFilter.d.ts +0 -38
- package/src/ContextBaseUrlOverrideFilter.js +0 -57
- package/src/ContextBaseUrlOverrideFilter.js.map +0 -1
- package/src/HostPolicy.d.ts +0 -118
- package/src/HostPolicy.js +0 -169
- package/src/HostPolicy.js.map +0 -1
- package/src/RuntimeHostErrors.d.ts +0 -42
- package/src/RuntimeHostErrors.js +0 -54
- package/src/RuntimeHostErrors.js.map +0 -1
package/src/SsrfPolicy.js
CHANGED
|
@@ -1,52 +1,67 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SsrfPolicy = void 0;
|
|
3
|
+
exports.SsrfTestingPolicy = exports.SsrfPolicy = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* What a
|
|
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.
|
|
6
7
|
*
|
|
7
|
-
* DATA ONLY — the enforcement is {@link SsrfGuardFilter}
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
16
|
*/
|
|
13
17
|
class SsrfPolicy {
|
|
14
|
-
allowedSchemes;
|
|
15
|
-
allowInternalAddresses;
|
|
16
|
-
maxRedirects;
|
|
17
|
-
allowInternalReason;
|
|
18
|
-
constructor(
|
|
19
18
|
/**
|
|
20
|
-
* URL schemes that may be sent to, with their colons
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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.
|
|
24
23
|
*/
|
|
25
|
-
allowedSchemes
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
* loopback / RFC1918 / link-local / metadata refusals are skipped — scheme checking and the
|
|
29
|
-
* redirect cap still apply.
|
|
30
|
-
*/
|
|
31
|
-
allowInternalAddresses,
|
|
24
|
+
allowedSchemes = new Set(['https:']);
|
|
25
|
+
/** When true, the loopback / RFC1918 / link-local / metadata refusals are skipped. */
|
|
26
|
+
allowInternalAddresses = false;
|
|
32
27
|
/**
|
|
33
28
|
* How many redirects may be followed, each one re-judged under this same policy. Small on
|
|
34
29
|
* purpose: a legitimate webhook endpoint does not need a redirect chain, and every hop is
|
|
35
30
|
* another chance for the destination to move somewhere we did not agree to.
|
|
36
31
|
*/
|
|
37
|
-
maxRedirects
|
|
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(
|
|
38
56
|
/**
|
|
39
|
-
* WHY internal addresses
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* message nobody will find.
|
|
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.
|
|
43
60
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
47
|
-
this.maxRedirects = maxRedirects;
|
|
48
|
-
this.allowInternalReason = allowInternalReason;
|
|
61
|
+
reason) {
|
|
62
|
+
super();
|
|
63
|
+
this.allowInternalReason = reason;
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
|
-
exports.
|
|
66
|
+
exports.SsrfTestingPolicy = SsrfTestingPolicy;
|
|
52
67
|
//# sourceMappingURL=SsrfPolicy.js.map
|
package/src/SsrfPolicy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SsrfPolicy.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/SsrfPolicy.ts"],"names":[],"mappings":";;;AAAA
|
|
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,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
|
@@ -14,26 +14,29 @@
|
|
|
14
14
|
* import { ClientHttpFactory, ClientConfig } from '@webpieces/http-client-node';
|
|
15
15
|
*
|
|
16
16
|
* // inject the factory, then one client per contract
|
|
17
|
-
* const server2 = factory.createRpcClient(
|
|
18
|
-
* Server2Api,
|
|
19
|
-
* new ClientConfig('server2', new DeployedServiceHost()),
|
|
20
|
-
* [], // this client's outbound filters
|
|
21
|
-
* );
|
|
17
|
+
* const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));
|
|
22
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
|
+
* ]);
|
|
23
24
|
* ```
|
|
24
25
|
*/
|
|
25
26
|
export { ClientHttpFactory } from './ClientHttpFactory';
|
|
26
27
|
export { NodeProxyClient, NODE_PROXY_CLIENT_PROVIDER } from './NodeProxyClient';
|
|
27
28
|
export { ClientConfig } from './ClientConfig';
|
|
28
|
-
export {
|
|
29
|
-
export {
|
|
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';
|
|
30
34
|
export { SsrfGuardFilter } from './SsrfGuardFilter';
|
|
31
35
|
export { SsrfRefusedError } from './SsrfRefusedError';
|
|
32
|
-
export {
|
|
36
|
+
export { OutboundAuthFilter } from './OutboundAuthFilter';
|
|
33
37
|
export { InternalAddressRules } from './InternalAddressRules';
|
|
34
38
|
export { AddressResolver, DnsAddressResolver } from './AddressResolver';
|
|
35
|
-
export { ContextBaseUrlOverrideFilter, BASE_URL_OVERRIDE_PRIORITY, SSRF_GUARD_PRIORITY } from './ContextBaseUrlOverrideFilter';
|
|
36
39
|
export { ProxyClient, ClientErrorTranslator, TranslatedFailure } from '@webpieces/http-client-core';
|
|
37
40
|
export { ClientRequest, ClientFilterDefinition } from '@webpieces/http-client-core';
|
|
38
|
-
export type { ClientFilter } from '@webpieces/http-client-core';
|
|
41
|
+
export type { ClientFilter, ClientFilters } from '@webpieces/http-client-core';
|
|
39
42
|
export type { ApiPrototype } from '@webpieces/http-client-core';
|
package/src/index.js
CHANGED
|
@@ -15,16 +15,17 @@
|
|
|
15
15
|
* import { ClientHttpFactory, ClientConfig } from '@webpieces/http-client-node';
|
|
16
16
|
*
|
|
17
17
|
* // inject the factory, then one client per contract
|
|
18
|
-
* const server2 = factory.createRpcClient(
|
|
19
|
-
* Server2Api,
|
|
20
|
-
* new ClientConfig('server2', new DeployedServiceHost()),
|
|
21
|
-
* [], // this client's outbound filters
|
|
22
|
-
* );
|
|
18
|
+
* const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));
|
|
23
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
|
+
* ]);
|
|
24
25
|
* ```
|
|
25
26
|
*/
|
|
26
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
-
exports.ClientFilterDefinition = exports.ClientRequest = exports.TranslatedFailure = exports.ClientErrorTranslator = exports.ProxyClient = exports.
|
|
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;
|
|
28
29
|
var ClientHttpFactory_1 = require("./ClientHttpFactory");
|
|
29
30
|
Object.defineProperty(exports, "ClientHttpFactory", { enumerable: true, get: function () { return ClientHttpFactory_1.ClientHttpFactory; } });
|
|
30
31
|
var NodeProxyClient_1 = require("./NodeProxyClient");
|
|
@@ -32,35 +33,40 @@ Object.defineProperty(exports, "NodeProxyClient", { enumerable: true, get: funct
|
|
|
32
33
|
Object.defineProperty(exports, "NODE_PROXY_CLIENT_PROVIDER", { enumerable: true, get: function () { return NodeProxyClient_1.NODE_PROXY_CLIENT_PROVIDER; } });
|
|
33
34
|
var ClientConfig_1 = require("./ClientConfig");
|
|
34
35
|
Object.defineProperty(exports, "ClientConfig", { enumerable: true, get: function () { return ClientConfig_1.ClientConfig; } });
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Object.defineProperty(exports, "
|
|
39
|
-
|
|
40
|
-
Object.defineProperty(exports, "
|
|
41
|
-
// The
|
|
42
|
-
//
|
|
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.
|
|
43
56
|
var SsrfPolicy_1 = require("./SsrfPolicy");
|
|
44
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; } });
|
|
45
59
|
var SsrfGuardFilter_1 = require("./SsrfGuardFilter");
|
|
46
60
|
Object.defineProperty(exports, "SsrfGuardFilter", { enumerable: true, get: function () { return SsrfGuardFilter_1.SsrfGuardFilter; } });
|
|
47
61
|
var SsrfRefusedError_1 = require("./SsrfRefusedError");
|
|
48
62
|
Object.defineProperty(exports, "SsrfRefusedError", { enumerable: true, get: function () { return SsrfRefusedError_1.SsrfRefusedError; } });
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Object.defineProperty(exports, "RuntimeHostEndpointUnsupportedError", { enumerable: true, get: function () { return RuntimeHostErrors_1.RuntimeHostEndpointUnsupportedError; } });
|
|
52
|
-
Object.defineProperty(exports, "MissingRuntimeBaseUrlError", { enumerable: true, get: function () { return RuntimeHostErrors_1.MissingRuntimeBaseUrlError; } });
|
|
63
|
+
var OutboundAuthFilter_1 = require("./OutboundAuthFilter");
|
|
64
|
+
Object.defineProperty(exports, "OutboundAuthFilter", { enumerable: true, get: function () { return OutboundAuthFilter_1.OutboundAuthFilter; } });
|
|
53
65
|
var InternalAddressRules_1 = require("./InternalAddressRules");
|
|
54
66
|
Object.defineProperty(exports, "InternalAddressRules", { enumerable: true, get: function () { return InternalAddressRules_1.InternalAddressRules; } });
|
|
55
67
|
var AddressResolver_1 = require("./AddressResolver");
|
|
56
68
|
Object.defineProperty(exports, "AddressResolver", { enumerable: true, get: function () { return AddressResolver_1.AddressResolver; } });
|
|
57
69
|
Object.defineProperty(exports, "DnsAddressResolver", { enumerable: true, get: function () { return AddressResolver_1.DnsAddressResolver; } });
|
|
58
|
-
// The built-in outbound filter that carries a per-call destination from the RequestContext into the
|
|
59
|
-
// send path, and the two framework priorities an app orders its own filters against.
|
|
60
|
-
var ContextBaseUrlOverrideFilter_1 = require("./ContextBaseUrlOverrideFilter");
|
|
61
|
-
Object.defineProperty(exports, "ContextBaseUrlOverrideFilter", { enumerable: true, get: function () { return ContextBaseUrlOverrideFilter_1.ContextBaseUrlOverrideFilter; } });
|
|
62
|
-
Object.defineProperty(exports, "BASE_URL_OVERRIDE_PRIORITY", { enumerable: true, get: function () { return ContextBaseUrlOverrideFilter_1.BASE_URL_OVERRIDE_PRIORITY; } });
|
|
63
|
-
Object.defineProperty(exports, "SSRF_GUARD_PRIORITY", { enumerable: true, get: function () { return ContextBaseUrlOverrideFilter_1.SSRF_GUARD_PRIORITY; } });
|
|
64
70
|
// The isomorphic engine, re-exported so a server app needs one import.
|
|
65
71
|
var http_client_core_1 = require("@webpieces/http-client-core");
|
|
66
72
|
Object.defineProperty(exports, "ProxyClient", { enumerable: true, get: function () { return http_client_core_1.ProxyClient; } });
|
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
|
|
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"]}
|
|
@@ -1,38 +0,0 @@
|
|
|
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;
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|