402-mcp 3.10.0 → 3.11.1
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 +49 -33
- package/build/config.d.ts +3 -0
- package/build/config.js +9 -0
- package/build/config.js.map +1 -1
- package/build/fetch/errors.d.ts +5 -0
- package/build/fetch/errors.js +8 -0
- package/build/fetch/errors.js.map +1 -1
- package/build/fetch/hns-resolve.d.ts +12 -0
- package/build/fetch/hns-resolve.js +67 -0
- package/build/fetch/hns-resolve.js.map +1 -0
- package/build/fetch/resilient-fetch.d.ts +25 -0
- package/build/fetch/resilient-fetch.js +69 -7
- package/build/fetch/resilient-fetch.js.map +1 -1
- package/build/fetch/ssrf-guard.d.ts +18 -4
- package/build/fetch/ssrf-guard.js +55 -7
- package/build/fetch/ssrf-guard.js.map +1 -1
- package/build/fetch/transport.d.ts +12 -0
- package/build/fetch/transport.js +64 -0
- package/build/fetch/transport.js.map +1 -0
- package/build/index.js +9 -1
- package/build/index.js.map +1 -1
- package/build/l402/bolt11.js +2 -2
- package/build/l402/bolt11.js.map +1 -1
- package/build/store/cashu-tokens.js +1 -1
- package/build/store/cashu-tokens.js.map +1 -1
- package/build/store/credentials.js.map +1 -1
- package/build/store/encryption.js +11 -2
- package/build/store/encryption.js.map +1 -1
- package/build/tools/balance.js +1 -1
- package/build/tools/balance.js.map +1 -1
- package/build/tools/buy-credits.d.ts +1 -0
- package/build/tools/buy-credits.js +13 -0
- package/build/tools/buy-credits.js.map +1 -1
- package/build/tools/config.js +1 -1
- package/build/tools/config.js.map +1 -1
- package/build/tools/credentials.js +1 -1
- package/build/tools/credentials.js.map +1 -1
- package/build/tools/fetch.d.ts +4 -0
- package/build/tools/fetch.js +28 -11
- package/build/tools/fetch.js.map +1 -1
- package/build/tools/redeem-cashu.js.map +1 -1
- package/build/tools/safe-error.js +2 -2
- package/build/tools/safe-error.js.map +1 -1
- package/build/tools/search.d.ts +2 -2
- package/build/tools/search.js +9 -6
- package/build/tools/search.js.map +1 -1
- package/build/tools/store-token.js +1 -1
- package/build/tools/store-token.js.map +1 -1
- package/build/wallet/cashu.js +1 -1
- package/build/wallet/cashu.js.map +1 -1
- package/build/wallet/nwc.js +1 -1
- package/build/wallet/nwc.js.map +1 -1
- package/package.json +18 -2
package/README.md
CHANGED
|
@@ -33,39 +33,6 @@ Ask Claude: *"Search for paid joke APIs using l402_search"* — no wallet needed
|
|
|
33
33
|
|
|
34
34
|
Ready to make paid calls? See the [full quickstart guide](./docs/quickstart.md) to set up a wallet and watch your agent pay for its first API call.
|
|
35
35
|
|
|
36
|
-
## Configuration
|
|
37
|
-
|
|
38
|
-
| Variable | Default | Description |
|
|
39
|
-
|----------|---------|-------------|
|
|
40
|
-
| `NWC_URI` | - | Nostr Wallet Connect URI for autonomous Lightning payments |
|
|
41
|
-
| `CASHU_TOKENS` | - | Path to Cashu token store file |
|
|
42
|
-
| `MAX_AUTO_PAY_SATS` | 1000 | Safety cap; payments above this require human confirmation |
|
|
43
|
-
| `CREDENTIAL_STORE` | `~/.402-mcp/credentials.json` | Persistent macaroon/credential storage |
|
|
44
|
-
| `TRANSPORT` | `stdio` | Transport mode: `stdio` or `http` |
|
|
45
|
-
| `PORT` | 3402 | HTTP server port (when `TRANSPORT=http`) |
|
|
46
|
-
|
|
47
|
-
## Tools
|
|
48
|
-
|
|
49
|
-
### Core L402 (any server)
|
|
50
|
-
|
|
51
|
-
| Tool | Description |
|
|
52
|
-
|------|-------------|
|
|
53
|
-
| `l402_config` | Introspect payment capabilities (wallets, limits, credential count) |
|
|
54
|
-
| `l402_discover` | Probe an endpoint to discover pricing without paying |
|
|
55
|
-
| `l402_fetch` | HTTP request with L402 support; auto-pays if within budget |
|
|
56
|
-
| `l402_pay` | Pay a specific invoice (NWC, Cashu, or human-in-the-loop) |
|
|
57
|
-
| `l402_credentials` | List stored credentials and cached balances |
|
|
58
|
-
| `l402_balance` | Check cached credit balance for a server |
|
|
59
|
-
| `l402_search` | Discover L402 services on Nostr relays (kind 31402 announcements) |
|
|
60
|
-
| `l402_store_token` | Store an L402 token obtained from a payment page |
|
|
61
|
-
|
|
62
|
-
### toll-booth extensions
|
|
63
|
-
|
|
64
|
-
| Tool | Description |
|
|
65
|
-
|------|-------------|
|
|
66
|
-
| `l402_buy_credits` | Browse and purchase volume discount tiers |
|
|
67
|
-
| `l402_redeem_cashu` | Redeem Cashu tokens directly (avoids Lightning round-trip) |
|
|
68
|
-
|
|
69
36
|
## How it works
|
|
70
37
|
|
|
71
38
|
```mermaid
|
|
@@ -100,6 +67,53 @@ Agent: "I need routing data from routing.trotters.cc"
|
|
|
100
67
|
|
|
101
68
|
For detailed architecture and payment flow diagrams, see [docs/architecture.md](./docs/architecture.md).
|
|
102
69
|
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
| Variable | Default | Description |
|
|
73
|
+
|----------|---------|-------------|
|
|
74
|
+
| `NWC_URI` | - | Nostr Wallet Connect URI for autonomous Lightning payments |
|
|
75
|
+
| `CASHU_TOKENS` | - | Path to Cashu token store file |
|
|
76
|
+
| `MAX_AUTO_PAY_SATS` | 1000 | Safety cap; payments above this require human confirmation |
|
|
77
|
+
| `CREDENTIAL_STORE` | `~/.402-mcp/credentials.json` | Persistent macaroon/credential storage |
|
|
78
|
+
| `TRANSPORT` | `stdio` | Transport mode: `stdio` or `http` |
|
|
79
|
+
| `PORT` | 3402 | HTTP server port (when `TRANSPORT=http`) |
|
|
80
|
+
| `TRANSPORT_PREFERENCE` | `clearnet` | Preferred network transport: `clearnet`, `tor`, or `hns` |
|
|
81
|
+
| `TOR_PROXY` | - | SOCKS5 proxy for `.onion` addresses (e.g. `socks5h://127.0.0.1:9050`) |
|
|
82
|
+
| `SOCKS_PROXY` | - | Generic SOCKS5 proxy for all requests when set |
|
|
83
|
+
| `HNS_GATEWAY_URL` | - | HTTP gateway for Handshake (`.hns`) domains (e.g. `https://hns.to`) |
|
|
84
|
+
|
|
85
|
+
### Transport selection and fallback
|
|
86
|
+
|
|
87
|
+
When a kind 31402 event advertises multiple URLs (one per transport), 402-mcp selects the best one based on your configuration:
|
|
88
|
+
|
|
89
|
+
1. **Preference first** — if `TRANSPORT_PREFERENCE=tor` and a `.onion` URL is available, it is tried first.
|
|
90
|
+
2. **Availability fallback** — if the preferred transport is unreachable (proxy not configured, timeout), the client falls back to the next URL in the list.
|
|
91
|
+
3. **Clearnet default** — if no preference is set, clearnet URLs are tried before `.onion` or HNS entries.
|
|
92
|
+
|
|
93
|
+
Services can announce multiple endpoints for the **same service** (same pricing, same macaroon key) on different transports. This is purely for censorship resistance; you do not need to re-authenticate when switching transports. To reach Tor or HNS endpoints you must configure the corresponding proxy/gateway env vars above.
|
|
94
|
+
|
|
95
|
+
## Tools
|
|
96
|
+
|
|
97
|
+
### Core L402 (any server)
|
|
98
|
+
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|------|-------------|
|
|
101
|
+
| `l402_config` | Introspect payment capabilities (wallets, limits, credential count) |
|
|
102
|
+
| `l402_discover` | Probe an endpoint to discover pricing without paying |
|
|
103
|
+
| `l402_fetch` | HTTP request with L402 support; auto-pays if within budget |
|
|
104
|
+
| `l402_pay` | Pay a specific invoice (NWC, Cashu, or human-in-the-loop) |
|
|
105
|
+
| `l402_credentials` | List stored credentials and cached balances |
|
|
106
|
+
| `l402_balance` | Check cached credit balance for a server |
|
|
107
|
+
| `l402_search` | Discover L402 services on Nostr relays (kind 31402 announcements) |
|
|
108
|
+
| `l402_store_token` | Store an L402 token obtained from a payment page |
|
|
109
|
+
|
|
110
|
+
### toll-booth extensions
|
|
111
|
+
|
|
112
|
+
| Tool | Description |
|
|
113
|
+
|------|-------------|
|
|
114
|
+
| `l402_buy_credits` | Browse and purchase volume discount tiers |
|
|
115
|
+
| `l402_redeem_cashu` | Redeem Cashu tokens directly (avoids Lightning round-trip) |
|
|
116
|
+
|
|
103
117
|
## Payment methods
|
|
104
118
|
|
|
105
119
|
Three payment rails, tried in priority order:
|
|
@@ -120,6 +134,8 @@ The agent can override the method per-call, or you can configure only the method
|
|
|
120
134
|
|
|
121
135
|
## Ecosystem
|
|
122
136
|
|
|
137
|
+
Browse live L402 services at [402.pub](https://402.pub) — the decentralised marketplace for payment-gated APIs.
|
|
138
|
+
|
|
123
139
|
| Project | Role |
|
|
124
140
|
|---------|------|
|
|
125
141
|
| [toll-booth](https://github.com/TheCryptoDonkey/toll-booth) | Payment-rail agnostic HTTP 402 middleware |
|
package/build/config.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface L402Config {
|
|
|
14
14
|
ssrfAllowPrivate: boolean;
|
|
15
15
|
corsOrigin: string | false;
|
|
16
16
|
bindAddress: string;
|
|
17
|
+
transportPreference: string[];
|
|
18
|
+
torProxy: string | undefined;
|
|
19
|
+
hnsGatewayUrl: string;
|
|
17
20
|
}
|
|
18
21
|
/** Loads and validates configuration from environment variables, applying defaults. */
|
|
19
22
|
export declare function loadConfig(): L402Config;
|
package/build/config.js
CHANGED
|
@@ -26,6 +26,12 @@ export function loadConfig() {
|
|
|
26
26
|
if (transport !== 'stdio' && transport !== 'http') {
|
|
27
27
|
throw new Error(`TRANSPORT must be 'stdio' or 'http'; got '${transport}'`);
|
|
28
28
|
}
|
|
29
|
+
const transportPref = process.env.TRANSPORT_PREFERENCE;
|
|
30
|
+
const transportPreference = transportPref
|
|
31
|
+
? transportPref.split(',').map(s => s.trim()).filter(Boolean)
|
|
32
|
+
: ['onion', 'hns', 'https', 'http'];
|
|
33
|
+
const torProxy = process.env.TOR_PROXY || process.env.SOCKS_PROXY || undefined;
|
|
34
|
+
const hnsGatewayUrl = process.env.HNS_GATEWAY_URL || 'https://query.hdns.io/';
|
|
29
35
|
const config = {
|
|
30
36
|
nwcUri,
|
|
31
37
|
cashuTokensPath: process.env.CASHU_TOKENS,
|
|
@@ -42,6 +48,9 @@ export function loadConfig() {
|
|
|
42
48
|
ssrfAllowPrivate: process.env.SSRF_ALLOW_PRIVATE === 'true',
|
|
43
49
|
corsOrigin: process.env.CORS_ORIGIN || false,
|
|
44
50
|
bindAddress: process.env.BIND_ADDRESS ?? '127.0.0.1',
|
|
51
|
+
transportPreference,
|
|
52
|
+
torProxy,
|
|
53
|
+
hnsGatewayUrl,
|
|
45
54
|
};
|
|
46
55
|
assertNonNegativeInt('MAX_AUTO_PAY_SATS', config.maxAutoPaySats);
|
|
47
56
|
assertNonNegativeInt('MAX_SPEND_PER_MINUTE_SATS', config.maxSpendPerMinuteSats);
|
package/build/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/build/fetch/errors.d.ts
CHANGED
|
@@ -19,3 +19,8 @@ export declare class DowngradeError extends Error {
|
|
|
19
19
|
readonly name = "DowngradeError";
|
|
20
20
|
constructor(originalUrl: string, redirectUrl: string);
|
|
21
21
|
}
|
|
22
|
+
export declare class TransportUnavailableError extends Error {
|
|
23
|
+
readonly name = "TransportUnavailableError";
|
|
24
|
+
readonly url: string;
|
|
25
|
+
constructor(url: string, cause?: string);
|
|
26
|
+
}
|
package/build/fetch/errors.js
CHANGED
|
@@ -30,4 +30,12 @@ export class DowngradeError extends Error {
|
|
|
30
30
|
super(`HTTPS downgrade blocked: ${originalUrl} redirected to ${redirectUrl}`);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
export class TransportUnavailableError extends Error {
|
|
34
|
+
name = 'TransportUnavailableError';
|
|
35
|
+
url;
|
|
36
|
+
constructor(url, cause) {
|
|
37
|
+
super(`Transport unavailable for ${url}${cause ? `: ${cause}` : ''}`);
|
|
38
|
+
this.url = url;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
33
41
|
//# sourceMappingURL=errors.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/fetch/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,SAAU,SAAQ,KAAK;IAChB,IAAI,GAAG,WAAW,CAAA;IAEpC,YAAY,MAAc,EAAE,GAAW;QACrC,KAAK,CAAC,iBAAiB,MAAM,KAAK,GAAG,GAAG,CAAC,CAAA;IAC3C,CAAC;CACF;AAED,MAAM,OAAO,YAAa,SAAQ,KAAK;IACnB,IAAI,GAAG,cAAc,CAAA;IAEvC,YAAY,EAAU,EAAE,GAAW;QACjC,KAAK,CAAC,2BAA2B,EAAE,OAAO,GAAG,GAAG,CAAC,CAAA;IACnD,CAAC;CACF;AAED,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC1B,IAAI,GAAG,qBAAqB,CAAA;IAE9C,YAAY,QAAgB,EAAE,GAAW,EAAE,KAAY;QACrD,KAAK,CAAC,wBAAwB,QAAQ,cAAc,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IAC1F,CAAC;CACF;AAED,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC5B,IAAI,GAAG,uBAAuB,CAAA;IACvC,QAAQ,CAAQ;IAEzB,YAAY,QAAgB;QAC1B,KAAK,CAAC,0CAA0C,QAAQ,QAAQ,CAAC,CAAA;QACjE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IACrB,IAAI,GAAG,gBAAgB,CAAA;IAEzC,YAAY,WAAmB,EAAE,WAAmB;QAClD,KAAK,CAAC,4BAA4B,WAAW,kBAAkB,WAAW,EAAE,CAAC,CAAA;IAC/E,CAAC;CACF"}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/fetch/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,SAAU,SAAQ,KAAK;IAChB,IAAI,GAAG,WAAW,CAAA;IAEpC,YAAY,MAAc,EAAE,GAAW;QACrC,KAAK,CAAC,iBAAiB,MAAM,KAAK,GAAG,GAAG,CAAC,CAAA;IAC3C,CAAC;CACF;AAED,MAAM,OAAO,YAAa,SAAQ,KAAK;IACnB,IAAI,GAAG,cAAc,CAAA;IAEvC,YAAY,EAAU,EAAE,GAAW;QACjC,KAAK,CAAC,2BAA2B,EAAE,OAAO,GAAG,GAAG,CAAC,CAAA;IACnD,CAAC;CACF;AAED,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC1B,IAAI,GAAG,qBAAqB,CAAA;IAE9C,YAAY,QAAgB,EAAE,GAAW,EAAE,KAAY;QACrD,KAAK,CAAC,wBAAwB,QAAQ,cAAc,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IAC1F,CAAC;CACF;AAED,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC5B,IAAI,GAAG,uBAAuB,CAAA;IACvC,QAAQ,CAAQ;IAEzB,YAAY,QAAgB;QAC1B,KAAK,CAAC,0CAA0C,QAAQ,QAAQ,CAAC,CAAA;QACjE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IACrB,IAAI,GAAG,gBAAgB,CAAA;IAEzC,YAAY,WAAmB,EAAE,WAAmB;QAClD,KAAK,CAAC,4BAA4B,WAAW,kBAAkB,WAAW,EAAE,CAAC,CAAA;IAC/E,CAAC;CACF;AAED,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAChC,IAAI,GAAG,2BAA2B,CAAA;IAC3C,GAAG,CAAQ;IAEpB,YAAY,GAAW,EAAE,KAAc;QACrC,KAAK,CAAC,6BAA6B,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;QACrE,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;IAChB,CAAC;CACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ResolvedAddress {
|
|
2
|
+
address: string;
|
|
3
|
+
family: 4 | 6;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a Handshake (HNS) hostname via a DNS-over-HTTPS gateway.
|
|
7
|
+
*
|
|
8
|
+
* Tries an A record first; falls back to AAAA if no A records are returned.
|
|
9
|
+
* Throws if neither resolves, the gateway returns an error, or the request
|
|
10
|
+
* times out.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveHns(hostname: string, gatewayUrl: string, timeoutMs?: number): Promise<ResolvedAddress>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Strict IPv4: exactly four decimal octets 0-255 (rejects octal, hex, shorthand)
|
|
2
|
+
const IPV4_RE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
3
|
+
// IPv6: hex digits, colons, and dots (dots appear in mixed notation like ::ffff:93.184.216.34)
|
|
4
|
+
const IPV6_RE = /^[0-9a-fA-F:.]+$/;
|
|
5
|
+
function isValidIpFormat(address, family) {
|
|
6
|
+
if (family === 4) {
|
|
7
|
+
const m = IPV4_RE.exec(address);
|
|
8
|
+
if (!m)
|
|
9
|
+
return false;
|
|
10
|
+
return [m[1], m[2], m[3], m[4]].every(octet => {
|
|
11
|
+
const n = Number(octet);
|
|
12
|
+
// Reject leading zeros (octal ambiguity: "0177" vs "177")
|
|
13
|
+
return n >= 0 && n <= 255 && String(n) === octet;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
// IPv6: basic format check — hex digits and colons only, at least one colon
|
|
17
|
+
return IPV6_RE.test(address) && address.includes(':');
|
|
18
|
+
}
|
|
19
|
+
async function queryDns(hostname, gatewayUrl, type, signal) {
|
|
20
|
+
const url = `${gatewayUrl}dns-query?name=${encodeURIComponent(hostname)}&type=${type}`;
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
headers: { Accept: 'application/dns-json' },
|
|
23
|
+
signal,
|
|
24
|
+
redirect: 'error', // Prevent gateway redirects to internal services
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`DNS query failed: HTTP ${response.status} for ${hostname} (${type})`);
|
|
28
|
+
}
|
|
29
|
+
const data = (await response.json());
|
|
30
|
+
return data.Answer ?? [];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a Handshake (HNS) hostname via a DNS-over-HTTPS gateway.
|
|
34
|
+
*
|
|
35
|
+
* Tries an A record first; falls back to AAAA if no A records are returned.
|
|
36
|
+
* Throws if neither resolves, the gateway returns an error, or the request
|
|
37
|
+
* times out.
|
|
38
|
+
*/
|
|
39
|
+
export async function resolveHns(hostname, gatewayUrl, timeoutMs = 5000) {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
// Try A record first
|
|
44
|
+
const aAnswers = await queryDns(hostname, gatewayUrl, 'A', controller.signal);
|
|
45
|
+
const aRecord = aAnswers.find(a => a.type === 1);
|
|
46
|
+
if (aRecord) {
|
|
47
|
+
if (!isValidIpFormat(aRecord.data, 4)) {
|
|
48
|
+
throw new Error(`HNS gateway returned invalid IPv4 address for ${hostname}`);
|
|
49
|
+
}
|
|
50
|
+
return { address: aRecord.data, family: 4 };
|
|
51
|
+
}
|
|
52
|
+
// Fall back to AAAA
|
|
53
|
+
const aaaaAnswers = await queryDns(hostname, gatewayUrl, 'AAAA', controller.signal);
|
|
54
|
+
const aaaaRecord = aaaaAnswers.find(a => a.type === 28);
|
|
55
|
+
if (aaaaRecord) {
|
|
56
|
+
if (!isValidIpFormat(aaaaRecord.data, 6)) {
|
|
57
|
+
throw new Error(`HNS gateway returned invalid IPv6 address for ${hostname}`);
|
|
58
|
+
}
|
|
59
|
+
return { address: aaaaRecord.data, family: 6 };
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`No DNS records found for ${hostname}`);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
clearTimeout(timeoutId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=hns-resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hns-resolve.js","sourceRoot":"","sources":["../../src/fetch/hns-resolve.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ValidateUrlOptions } from './ssrf-guard.js';
|
|
1
2
|
export interface ResilientFetchOptions {
|
|
2
3
|
timeoutMs?: number;
|
|
3
4
|
retries?: number;
|
|
@@ -11,6 +12,10 @@ export interface ResilientFetchConfig {
|
|
|
11
12
|
backoffMs?: number;
|
|
12
13
|
maxResponseBytes?: number;
|
|
13
14
|
ssrfAllowPrivate?: boolean;
|
|
15
|
+
/** HNS (Handshake) resolver — called on NXDOMAIN to resolve alternative TLDs */
|
|
16
|
+
resolveHns?: ValidateUrlOptions['resolveHns'];
|
|
17
|
+
/** Whether a Tor SOCKS proxy is configured — enables .onion URL routing */
|
|
18
|
+
hasTorProxy?: boolean;
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
21
|
* Create a resilient fetch function with SSRF protection, timeout, and retry.
|
|
@@ -19,3 +24,23 @@ export interface ResilientFetchConfig {
|
|
|
19
24
|
* with two args, and accepts an optional third arg for per-call overrides.
|
|
20
25
|
*/
|
|
21
26
|
export declare function createResilientFetch(fetchFn: typeof fetch, config?: ResilientFetchConfig): (url: string | URL, init?: RequestInit, options?: ResilientFetchOptions) => Promise<Response>;
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when the error is a transport-level failure that should trigger
|
|
29
|
+
* a fallback to the next URL rather than surfacing to the caller.
|
|
30
|
+
*
|
|
31
|
+
* - `TransportUnavailableError` — thrown by ssrf-guard for .onion without Tor, etc.
|
|
32
|
+
* - Node error codes: ECONNREFUSED, ETIMEDOUT, ENOTFOUND, UND_ERR_CONNECT_TIMEOUT
|
|
33
|
+
* - ECONNRESET is intentionally excluded — a mid-response reset is not a transport
|
|
34
|
+
* failure that can be resolved by trying a different URL.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isTransportError(err: unknown): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Try each URL in order, falling back on transport-level failures.
|
|
39
|
+
*
|
|
40
|
+
* - Connection failures (ECONNREFUSED, ETIMEDOUT, ENOTFOUND, UND_ERR_CONNECT_TIMEOUT,
|
|
41
|
+
* TransportUnavailableError) are swallowed and the next URL is tried.
|
|
42
|
+
* - Any other error (HTTP-level, SSRF block, decode error) is re-thrown immediately
|
|
43
|
+
* without trying the remaining URLs.
|
|
44
|
+
* - Throws the last transport error if all URLs are exhausted.
|
|
45
|
+
*/
|
|
46
|
+
export declare function withTransportFallback(urls: string[], init: RequestInit, fetchFn: (url: string | URL, init?: RequestInit, options?: ResilientFetchOptions) => Promise<Response>, options?: ResilientFetchOptions): Promise<Response>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateUrl } from './ssrf-guard.js';
|
|
2
|
-
import { SsrfError, TimeoutError, RetryExhaustedError, DowngradeError, ResponseTooLargeError } from './errors.js';
|
|
2
|
+
import { SsrfError, TimeoutError, RetryExhaustedError, DowngradeError, ResponseTooLargeError, TransportUnavailableError } from './errors.js';
|
|
3
3
|
const MAX_REDIRECTS = 5;
|
|
4
4
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
5
|
const DEFAULT_RETRIES = 2;
|
|
@@ -57,6 +57,10 @@ export function createResilientFetch(fetchFn, config = {}) {
|
|
|
57
57
|
const globalBackoff = config.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
58
58
|
const globalMaxResponseBytes = config.maxResponseBytes ?? 0;
|
|
59
59
|
const allowPrivate = config.ssrfAllowPrivate ?? false;
|
|
60
|
+
const ssrfOptions = {
|
|
61
|
+
resolveHns: config.resolveHns,
|
|
62
|
+
hasTorProxy: config.hasTorProxy ?? false,
|
|
63
|
+
};
|
|
60
64
|
return async function resilientFetch(url, init, options) {
|
|
61
65
|
const timeoutMs = options?.timeoutMs ?? globalTimeout;
|
|
62
66
|
const retries = options?.retries ?? globalRetries;
|
|
@@ -66,7 +70,7 @@ export function createResilientFetch(fetchFn, config = {}) {
|
|
|
66
70
|
// SSRF check on the initial URL (never retried).
|
|
67
71
|
// The resolved address is used to pin HTTP connections to the validated IP,
|
|
68
72
|
// closing the DNS rebinding TOCTOU window.
|
|
69
|
-
const resolved = await validateUrl(urlStr, allowPrivate);
|
|
73
|
+
const resolved = await validateUrl(urlStr, allowPrivate, ssrfOptions);
|
|
70
74
|
const totalAttempts = 1 + retries;
|
|
71
75
|
let lastError;
|
|
72
76
|
for (let attempt = 0; attempt < totalAttempts; attempt++) {
|
|
@@ -75,7 +79,7 @@ export function createResilientFetch(fetchFn, config = {}) {
|
|
|
75
79
|
await sleep(delay);
|
|
76
80
|
}
|
|
77
81
|
try {
|
|
78
|
-
let response = await fetchWithTimeoutAndRedirects(fetchFn, urlStr, init, timeoutMs, allowPrivate, urlStr, resolved);
|
|
82
|
+
let response = await fetchWithTimeoutAndRedirects(fetchFn, urlStr, init, timeoutMs, allowPrivate, urlStr, resolved, ssrfOptions);
|
|
79
83
|
// If retryable status and we have retries left, drain body and continue
|
|
80
84
|
if (retryOn(response.status) && attempt < totalAttempts - 1) {
|
|
81
85
|
try {
|
|
@@ -131,12 +135,15 @@ export function createResilientFetch(fetchFn, config = {}) {
|
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
137
|
// Final attempt failed
|
|
134
|
-
if (retries === 0)
|
|
135
|
-
|
|
138
|
+
if (retries === 0) {
|
|
139
|
+
if (lastError)
|
|
140
|
+
throw lastError;
|
|
141
|
+
throw new Error('Request failed');
|
|
142
|
+
}
|
|
136
143
|
throw new RetryExhaustedError(totalAttempts, urlStr, lastError);
|
|
137
144
|
};
|
|
138
145
|
}
|
|
139
|
-
async function fetchWithTimeoutAndRedirects(fetchFn, url, init, timeoutMs, allowPrivate, originalUrl, resolved) {
|
|
146
|
+
async function fetchWithTimeoutAndRedirects(fetchFn, url, init, timeoutMs, allowPrivate, originalUrl, resolved, ssrfOptions = {}) {
|
|
140
147
|
let currentUrl = url;
|
|
141
148
|
let currentInit = init ? { ...init } : {};
|
|
142
149
|
let redirectCount = 0;
|
|
@@ -196,7 +203,16 @@ async function fetchWithTimeoutAndRedirects(fetchFn, url, init, timeoutMs, allow
|
|
|
196
203
|
throw new DowngradeError(originalUrl, currentUrl);
|
|
197
204
|
}
|
|
198
205
|
// SSRF check on redirect target; capture the resolved address for pinning
|
|
199
|
-
currentResolved = await validateUrl(currentUrl, allowPrivate);
|
|
206
|
+
currentResolved = await validateUrl(currentUrl, allowPrivate, ssrfOptions);
|
|
207
|
+
// Strip Authorization header on cross-origin redirects to prevent credential leakage.
|
|
208
|
+
// Per Fetch spec, credentials should not follow cross-origin redirects.
|
|
209
|
+
const redirectOrigin = new URL(currentUrl).origin;
|
|
210
|
+
const originalOrigin = new URL(originalUrl).origin;
|
|
211
|
+
if (redirectOrigin !== originalOrigin && currentInit.headers) {
|
|
212
|
+
const headers = new Headers(currentInit.headers);
|
|
213
|
+
headers.delete('Authorization');
|
|
214
|
+
currentInit = { ...currentInit, headers };
|
|
215
|
+
}
|
|
200
216
|
// 301/302/303 change POST to GET and drop body
|
|
201
217
|
if ([301, 302, 303].includes(response.status)) {
|
|
202
218
|
const method = currentInit.method?.toUpperCase();
|
|
@@ -207,4 +223,50 @@ async function fetchWithTimeoutAndRedirects(fetchFn, url, init, timeoutMs, allow
|
|
|
207
223
|
// 307/308 preserve method and body (no change needed)
|
|
208
224
|
}
|
|
209
225
|
}
|
|
226
|
+
/** Error codes that indicate a transport-level failure (connect refused, DNS, timeout). */
|
|
227
|
+
const TRANSPORT_ERROR_CODES = new Set(['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'UND_ERR_CONNECT_TIMEOUT']);
|
|
228
|
+
/**
|
|
229
|
+
* Returns true when the error is a transport-level failure that should trigger
|
|
230
|
+
* a fallback to the next URL rather than surfacing to the caller.
|
|
231
|
+
*
|
|
232
|
+
* - `TransportUnavailableError` — thrown by ssrf-guard for .onion without Tor, etc.
|
|
233
|
+
* - Node error codes: ECONNREFUSED, ETIMEDOUT, ENOTFOUND, UND_ERR_CONNECT_TIMEOUT
|
|
234
|
+
* - ECONNRESET is intentionally excluded — a mid-response reset is not a transport
|
|
235
|
+
* failure that can be resolved by trying a different URL.
|
|
236
|
+
*/
|
|
237
|
+
export function isTransportError(err) {
|
|
238
|
+
if (err instanceof TransportUnavailableError)
|
|
239
|
+
return true;
|
|
240
|
+
if (err instanceof Error) {
|
|
241
|
+
const code = err.code;
|
|
242
|
+
if (code && TRANSPORT_ERROR_CODES.has(code))
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Try each URL in order, falling back on transport-level failures.
|
|
249
|
+
*
|
|
250
|
+
* - Connection failures (ECONNREFUSED, ETIMEDOUT, ENOTFOUND, UND_ERR_CONNECT_TIMEOUT,
|
|
251
|
+
* TransportUnavailableError) are swallowed and the next URL is tried.
|
|
252
|
+
* - Any other error (HTTP-level, SSRF block, decode error) is re-thrown immediately
|
|
253
|
+
* without trying the remaining URLs.
|
|
254
|
+
* - Throws the last transport error if all URLs are exhausted.
|
|
255
|
+
*/
|
|
256
|
+
export async function withTransportFallback(urls, init, fetchFn, options) {
|
|
257
|
+
let lastError;
|
|
258
|
+
for (const url of urls) {
|
|
259
|
+
try {
|
|
260
|
+
return await fetchFn(url, init, options);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
if (isTransportError(err)) {
|
|
264
|
+
lastError = err;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
throw lastError ?? new Error('All transports exhausted');
|
|
271
|
+
}
|
|
210
272
|
//# sourceMappingURL=resilient-fetch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resilient-fetch.js","sourceRoot":"","sources":["../../src/fetch/resilient-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"resilient-fetch.js","sourceRoot":"","sources":["../../src/fetch/resilient-fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ResolvedAddress } from './hns-resolve.js';
|
|
2
|
+
export type { ResolvedAddress };
|
|
3
|
+
export interface ValidateUrlOptions {
|
|
4
|
+
/** Resolver for HNS (Handshake) names — called on NXDOMAIN (ENOTFOUND) */
|
|
5
|
+
resolveHns?: (hostname: string) => Promise<ResolvedAddress>;
|
|
6
|
+
/** Whether a Tor SOCKS proxy is available — required for .onion URLs */
|
|
7
|
+
hasTorProxy?: boolean;
|
|
4
8
|
}
|
|
5
9
|
/**
|
|
6
10
|
* Validate a URL against SSRF rules and return the resolved IP address.
|
|
@@ -14,5 +18,15 @@ export interface ResolvedAddress {
|
|
|
14
18
|
*
|
|
15
19
|
* When `allowPrivate` is true, no resolution or validation is performed and
|
|
16
20
|
* `undefined` is returned.
|
|
21
|
+
*
|
|
22
|
+
* `.onion` hostnames:
|
|
23
|
+
* - If `options.hasTorProxy` is true, returns `undefined` (route via SOCKS proxy; skip SSRF).
|
|
24
|
+
* - Otherwise throws `TransportUnavailableError`.
|
|
25
|
+
*
|
|
26
|
+
* HNS fallback:
|
|
27
|
+
* - If standard DNS fails with NXDOMAIN (ENOTFOUND) and `options.resolveHns` is provided,
|
|
28
|
+
* it is tried as a fallback. The resolved IP still goes through blocked-range checks.
|
|
29
|
+
* - Non-NXDOMAIN DNS errors propagate unchanged.
|
|
30
|
+
* - If both DNS and HNS fail, throws `TransportUnavailableError`.
|
|
17
31
|
*/
|
|
18
|
-
export declare function validateUrl(url: string, allowPrivate?: boolean): Promise<ResolvedAddress | undefined>;
|
|
32
|
+
export declare function validateUrl(url: string, allowPrivate?: boolean, options?: ValidateUrlOptions): Promise<ResolvedAddress | undefined>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { promises as dns } from 'node:dns';
|
|
2
|
-
import { SsrfError } from './errors.js';
|
|
2
|
+
import { SsrfError, TransportUnavailableError } from './errors.js';
|
|
3
3
|
function isBlockedIp(address, family) {
|
|
4
4
|
if (family === 6) {
|
|
5
5
|
const lower = address.toLowerCase();
|
|
@@ -79,6 +79,16 @@ function isBlockedIp(address, family) {
|
|
|
79
79
|
return 'broadcast';
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate a single resolved address against blocked ranges.
|
|
84
|
+
* Throws SsrfError if the address is in a blocked range.
|
|
85
|
+
*/
|
|
86
|
+
function assertNotBlocked(address, family, url) {
|
|
87
|
+
const reason = isBlockedIp(address, family);
|
|
88
|
+
if (reason) {
|
|
89
|
+
throw new SsrfError(reason, url);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
82
92
|
/**
|
|
83
93
|
* Validate a URL against SSRF rules and return the resolved IP address.
|
|
84
94
|
*
|
|
@@ -91,8 +101,18 @@ function isBlockedIp(address, family) {
|
|
|
91
101
|
*
|
|
92
102
|
* When `allowPrivate` is true, no resolution or validation is performed and
|
|
93
103
|
* `undefined` is returned.
|
|
104
|
+
*
|
|
105
|
+
* `.onion` hostnames:
|
|
106
|
+
* - If `options.hasTorProxy` is true, returns `undefined` (route via SOCKS proxy; skip SSRF).
|
|
107
|
+
* - Otherwise throws `TransportUnavailableError`.
|
|
108
|
+
*
|
|
109
|
+
* HNS fallback:
|
|
110
|
+
* - If standard DNS fails with NXDOMAIN (ENOTFOUND) and `options.resolveHns` is provided,
|
|
111
|
+
* it is tried as a fallback. The resolved IP still goes through blocked-range checks.
|
|
112
|
+
* - Non-NXDOMAIN DNS errors propagate unchanged.
|
|
113
|
+
* - If both DNS and HNS fail, throws `TransportUnavailableError`.
|
|
94
114
|
*/
|
|
95
|
-
export async function validateUrl(url, allowPrivate = false) {
|
|
115
|
+
export async function validateUrl(url, allowPrivate = false, options = {}) {
|
|
96
116
|
if (allowPrivate)
|
|
97
117
|
return undefined;
|
|
98
118
|
let parsed;
|
|
@@ -108,19 +128,47 @@ export async function validateUrl(url, allowPrivate = false) {
|
|
|
108
128
|
// Strip bracket notation and IPv6 zone/scope IDs (e.g. fe80::1%25eth0)
|
|
109
129
|
// so the guard is self-contained rather than relying on upstream normalisation.
|
|
110
130
|
const hostname = parsed.hostname.replace(/^\[/, '').replace(/\]$/, '').split('%')[0];
|
|
131
|
+
// .onion: route via Tor SOCKS proxy — never attempt DNS resolution
|
|
132
|
+
if (hostname.endsWith('.onion')) {
|
|
133
|
+
if (options.hasTorProxy)
|
|
134
|
+
return undefined;
|
|
135
|
+
throw new TransportUnavailableError(url, 'Tor proxy required for .onion addresses');
|
|
136
|
+
}
|
|
111
137
|
// Resolve ALL addresses to prevent multi-homed bypass where one A/AAAA
|
|
112
138
|
// record is public but another resolves to a private/blocked IP.
|
|
113
|
-
|
|
139
|
+
let results;
|
|
140
|
+
try {
|
|
141
|
+
results = await dns.lookup(hostname, { all: true });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const dnsError = err;
|
|
145
|
+
// NXDOMAIN — try HNS fallback
|
|
146
|
+
if (dnsError.code === 'ENOTFOUND') {
|
|
147
|
+
if (options.resolveHns) {
|
|
148
|
+
let hnsResult;
|
|
149
|
+
try {
|
|
150
|
+
hnsResult = await options.resolveHns(hostname);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new TransportUnavailableError(url, 'DNS and HNS resolution both failed');
|
|
154
|
+
}
|
|
155
|
+
// Validate the HNS-resolved IP against blocked ranges
|
|
156
|
+
assertNotBlocked(hnsResult.address, hnsResult.family, url);
|
|
157
|
+
return hnsResult;
|
|
158
|
+
}
|
|
159
|
+
throw new TransportUnavailableError(url, 'NXDOMAIN and no HNS resolver configured');
|
|
160
|
+
}
|
|
161
|
+
// Non-NXDOMAIN errors (EAI_AGAIN, ESERVFAIL, etc.) propagate unchanged
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
114
164
|
if (results.length === 0) {
|
|
115
165
|
throw new SsrfError('DNS resolution returned no addresses', url);
|
|
116
166
|
}
|
|
117
167
|
for (const { address: addr, family: fam } of results) {
|
|
118
|
-
|
|
119
|
-
if (reason) {
|
|
120
|
-
throw new SsrfError(reason, url);
|
|
121
|
-
}
|
|
168
|
+
assertNotBlocked(addr, fam, url);
|
|
122
169
|
}
|
|
123
170
|
// Return the first result for IP pinning (all have been validated)
|
|
171
|
+
// dns.lookup returns family as number (4 or 6); cast to the narrower union type
|
|
124
172
|
return { address: results[0].address, family: results[0].family };
|
|
125
173
|
}
|
|
126
174
|
//# sourceMappingURL=ssrf-guard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssrf-guard.js","sourceRoot":"","sources":["../../src/fetch/ssrf-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,GAAG,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"ssrf-guard.js","sourceRoot":"","sources":["../../src/fetch/ssrf-guard.ts"],"names":[],"mappings":"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"}
|