agent402-client 0.8.12 → 0.8.14
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 +19 -0
- package/index.js +75 -20
- package/package.json +2 -1
- package/tls-distrust.js +110 -0
package/README.md
CHANGED
|
@@ -351,6 +351,25 @@ const payFetch = wrapFetchWithPayment(fetch, client);
|
|
|
351
351
|
Pairs with `maxPerCallUsd` / `dailyLimitUsd` (how much) and
|
|
352
352
|
`withNetworkPreference` (which chain).
|
|
353
353
|
|
|
354
|
+
## Distrusted certificate authorities (`distrustFetch`)
|
|
355
|
+
|
|
356
|
+
Since 0.8.14 the SDK's own requests refuse a TLS chain that passes through one
|
|
357
|
+
of the intermediate CAs listed in `DISTRUSTED_SPKI_SHA256` (keys reported
|
|
358
|
+
recoverable in Mozilla bug 2083449, which still validate against Node's bundled
|
|
359
|
+
store). The check is keyed on each certificate's SubjectPublicKeyInfo hash and
|
|
360
|
+
runs on a dedicated dispatcher, so nothing changes for the rest of your process.
|
|
361
|
+
Give your payment wrapper the same base fetch so the 402 and the paid retry go
|
|
362
|
+
through it too:
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
import { distrustFetch } from "agent402-client";
|
|
366
|
+
const payFetch = wrapFetchWithPayment(distrustFetch(), client);
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
It applies on Node with the default dispatcher; with a proxy or custom
|
|
370
|
+
dispatcher installed, requests go out through yours unchanged. Pass
|
|
371
|
+
`{ tlsDistrust: false }` to send the default fetch as is.
|
|
372
|
+
|
|
354
373
|
## Legal
|
|
355
374
|
|
|
356
375
|
Use of the hosted instance at agent402.tools is subject to its [Terms of Service](https://agent402.tools/terms) (acceptable-use policy included) and [Privacy Policy](https://agent402.tools/privacy). This package is MIT-licensed; the hosted server is AGPL-3.0. Both are provided as-is without warranty, and self-hosted deployments are their operator's responsibility.
|
package/index.js
CHANGED
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
// // paid tools: pass an x402-wrapped fetch (your wallet signs)
|
|
19
19
|
// const a = new Agent402({ fetch: payFetch });
|
|
20
20
|
import { createHash, randomBytes } from "node:crypto";
|
|
21
|
+
import { distrustFetch } from "./tls-distrust.js";
|
|
22
|
+
export { distrustFetch, distrustDispatcher, distrustCheckServerIdentity, DISTRUSTED_SPKI_SHA256 } from "./tls-distrust.js";
|
|
21
23
|
|
|
22
24
|
// Keep in lockstep with package.json. Every request the SDK issues carries
|
|
23
25
|
// `User-Agent: agent402-client/<version>` - a standard header, no extra
|
|
24
26
|
// network calls - so a seller can attribute traffic (and settled payments)
|
|
25
27
|
// to this SDK. Product token only; nothing about the caller rides along.
|
|
26
|
-
const VERSION = "0.8.
|
|
28
|
+
const VERSION = "0.8.14";
|
|
27
29
|
const USER_AGENT = `agent402-client/${VERSION}`;
|
|
28
30
|
// 32MB: about a hundred times any realistic response from this catalog (the
|
|
29
31
|
// largest is a base64 image at a few MB), so it cannot break a legitimate
|
|
@@ -33,13 +35,42 @@ const DEFAULT_MAX_RESPONSE_BYTES = 32 * 1024 * 1024;
|
|
|
33
35
|
|
|
34
36
|
const leadingZeroBits = (buf) => { let n = 0; for (const b of buf) { if (b === 0) { n += 8; continue; } n += Math.clz32(b) - 24; break; } return n; };
|
|
35
37
|
|
|
38
|
+
// Discovery reads (catalog, find, route, skill packs, leaderboard, a
|
|
39
|
+
// proof-of-work challenge) settle nothing, so a 503 the server sheds under
|
|
40
|
+
// load is safe to repeat. It is retried only when it names its own delay
|
|
41
|
+
// (a Retry-After header or a retryAfterSeconds body field), at most
|
|
42
|
+
// DISCOVERY_ATTEMPTS tries in all, each wait capped at DISCOVERY_MAX_WAIT_MS.
|
|
43
|
+
// A paid or priced call never goes through here: a repeat of one could settle
|
|
44
|
+
// twice.
|
|
45
|
+
const DISCOVERY_ATTEMPTS = 3;
|
|
46
|
+
const DISCOVERY_MAX_WAIT_MS = 5000;
|
|
47
|
+
|
|
48
|
+
/** The wait (ms) a shed 503 asks for, or null when `r` is not one. */
|
|
49
|
+
async function shedRetryWaitMs(r, maxWaitMs = DISCOVERY_MAX_WAIT_MS) {
|
|
50
|
+
if (!r || r.status !== 503) return null;
|
|
51
|
+
const header = r.headers && typeof r.headers.get === "function" ? r.headers.get("retry-after") : null;
|
|
52
|
+
let seconds = header != null && String(header).trim() !== "" ? Number(header) : NaN;
|
|
53
|
+
if (!Number.isFinite(seconds)) {
|
|
54
|
+
try {
|
|
55
|
+
const body = await (typeof r.clone === "function" ? r.clone() : r).json();
|
|
56
|
+
seconds = Number(body && body.retryAfterSeconds);
|
|
57
|
+
} catch { seconds = NaN; }
|
|
58
|
+
}
|
|
59
|
+
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
60
|
+
return Math.min(seconds * 1000, maxWaitMs);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
64
|
+
|
|
36
65
|
export class Agent402 {
|
|
37
66
|
/**
|
|
38
67
|
* @param {object} [opts]
|
|
39
68
|
* @param {string} [opts.baseUrl="https://agent402.tools"]
|
|
40
69
|
* @param {typeof fetch} [opts.fetch] an x402-wrapped fetch for wallet-only tools (optional)
|
|
41
70
|
* @param {boolean} [opts.cache=true] cache results in memory (deterministic tools)
|
|
42
|
-
* @param {typeof fetch} [opts.fetchImpl] plain fetch (defaults to global fetch)
|
|
71
|
+
* @param {typeof fetch} [opts.fetchImpl] plain fetch (defaults to global fetch through distrustFetch)
|
|
72
|
+
* @param {boolean} [opts.tlsDistrust=true] send the default fetch through distrustFetch (refuses a TLS
|
|
73
|
+
* chain through a distrusted intermediate CA); false sends it as is
|
|
43
74
|
* @param {number} [opts.maxPerCallUsd] hard ceiling on a single paid call (USD); over → SpendingLimitError before paying
|
|
44
75
|
* @param {number} [opts.dailyLimitUsd] hard ceiling on rolling-24h paid spend (USD)
|
|
45
76
|
* @param {number} [opts.maxPerHostUsd] hard ceiling on rolling-24h paid spend to one seller host (USD)
|
|
@@ -51,10 +82,14 @@ export class Agent402 {
|
|
|
51
82
|
* Optional buyer-owned result validator. `id` namespaces cache entries;
|
|
52
83
|
* `validate` may return false or throw to reject delivery.
|
|
53
84
|
*/
|
|
54
|
-
constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl =
|
|
85
|
+
constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl = undefined, tlsDistrust = true,
|
|
55
86
|
maxPerCallUsd = null, dailyLimitUsd = null, maxPerHostUsd = null, creditsKey = null,
|
|
56
87
|
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES, outputValidator = null } = {}) {
|
|
57
88
|
this.creditsKey = typeof creditsKey === "string" && /^a402_[A-Za-z0-9_-]{32,64}$/.test(creditsKey) ? creditsKey : null;
|
|
89
|
+
if (fetchImpl === undefined) {
|
|
90
|
+
fetchImpl = globalThis.fetch;
|
|
91
|
+
if (typeof fetchImpl === "function" && tlsDistrust !== false) fetchImpl = distrustFetch(fetchImpl);
|
|
92
|
+
}
|
|
58
93
|
if (typeof fetchImpl !== "function") throw new Error("No fetch available - pass { fetchImpl } on Node < 18");
|
|
59
94
|
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
60
95
|
this.payFetch = payFetch || null;
|
|
@@ -62,6 +97,8 @@ export class Agent402 {
|
|
|
62
97
|
// above). Caller-supplied init.headers still win on a key collision.
|
|
63
98
|
this.f = (url, init = {}) => fetchImpl(url, { ...init, headers: { "User-Agent": USER_AGENT, ...(init.headers || {}) } });
|
|
64
99
|
this._catalog = null;
|
|
100
|
+
// The wait between shed-503 retries of a discovery read (tests replace it).
|
|
101
|
+
this._sleep = sleepMs;
|
|
65
102
|
this._cache = cache ? new Map() : null;
|
|
66
103
|
// Salt for the default Idempotency-Key: one per client instance, so the key
|
|
67
104
|
// for (slug, params) is stable across call() invocations on THIS client
|
|
@@ -147,10 +184,35 @@ export class Agent402 {
|
|
|
147
184
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
148
185
|
}
|
|
149
186
|
|
|
187
|
+
/**
|
|
188
|
+
* GET a discovery surface, retrying a shed 503 (see DISCOVERY_ATTEMPTS).
|
|
189
|
+
* Returns the response once it is 2xx; otherwise throws an Error naming the
|
|
190
|
+
* operation, the surface and the last status, with `.status`, `.surface`
|
|
191
|
+
* and `.attempts` set.
|
|
192
|
+
*/
|
|
193
|
+
async _discoveryGet(label, path) {
|
|
194
|
+
let r;
|
|
195
|
+
let attempts = 0;
|
|
196
|
+
for (;;) {
|
|
197
|
+
attempts++;
|
|
198
|
+
r = await this.f(`${this.baseUrl}${path}`);
|
|
199
|
+
if (r.ok) return r;
|
|
200
|
+
if (attempts >= DISCOVERY_ATTEMPTS) break;
|
|
201
|
+
const wait = await shedRetryWaitMs(r);
|
|
202
|
+
if (wait == null) break;
|
|
203
|
+
await this._sleep(wait);
|
|
204
|
+
}
|
|
205
|
+
const surface = path.split("?")[0];
|
|
206
|
+
const err = new Error(`${label} failed: HTTP ${r.status} from ${surface}${attempts > 1 ? ` after ${attempts} attempts` : ""}`);
|
|
207
|
+
err.status = r.status;
|
|
208
|
+
err.surface = surface;
|
|
209
|
+
err.attempts = attempts;
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
|
|
150
213
|
async _loadCatalog() {
|
|
151
214
|
if (this._catalog) return this._catalog;
|
|
152
|
-
const r = await this.
|
|
153
|
-
if (!r.ok) throw new Error(`could not load catalog: HTTP ${r.status}`);
|
|
215
|
+
const r = await this._discoveryGet("could not load catalog", "/api/pricing");
|
|
154
216
|
const j = await r.json();
|
|
155
217
|
const m = new Map();
|
|
156
218
|
for (const e of j.endpoints || []) m.set(e.slug, { method: e.method, path: e.path, computePayable: e.computePayable, price: e.price });
|
|
@@ -160,8 +222,7 @@ export class Agent402 {
|
|
|
160
222
|
|
|
161
223
|
/** Resolve a plain-language task to the best-matching tools (route, price, schema, example). */
|
|
162
224
|
async find(task, { k = 5 } = {}) {
|
|
163
|
-
const r = await this.
|
|
164
|
-
if (!r.ok) throw new Error(`find failed: HTTP ${r.status}`);
|
|
225
|
+
const r = await this._discoveryGet("find", `/api/find?q=${encodeURIComponent(task)}&k=${k}`);
|
|
165
226
|
return (await r.json()).results || [];
|
|
166
227
|
}
|
|
167
228
|
|
|
@@ -190,8 +251,7 @@ export class Agent402 {
|
|
|
190
251
|
const includeParam = include === "external" || include === "local" ? include : "all";
|
|
191
252
|
const qs = new URLSearchParams({ q: String(task ?? ""), k: String(top), include: includeParam });
|
|
192
253
|
if (network) qs.set("network", String(network));
|
|
193
|
-
const r = await this.
|
|
194
|
-
if (!r.ok) throw new Error(`route failed: HTTP ${r.status}`);
|
|
254
|
+
const r = await this._discoveryGet("route", `/api/route?${qs}`);
|
|
195
255
|
return r.json();
|
|
196
256
|
}
|
|
197
257
|
|
|
@@ -204,8 +264,7 @@ export class Agent402 {
|
|
|
204
264
|
* to fetch the rendered prompt messages, or hand the slug to an MCP client.
|
|
205
265
|
*/
|
|
206
266
|
async findWorkflows(task, { k = 2 } = {}) {
|
|
207
|
-
const r = await this.
|
|
208
|
-
if (!r.ok) throw new Error(`findWorkflows failed: HTTP ${r.status}`);
|
|
267
|
+
const r = await this._discoveryGet("findWorkflows", `/api/find?q=${encodeURIComponent(task)}&k=${k}`);
|
|
209
268
|
return (await r.json()).packs || [];
|
|
210
269
|
}
|
|
211
270
|
|
|
@@ -216,8 +275,7 @@ export class Agent402 {
|
|
|
216
275
|
*/
|
|
217
276
|
async getWorkflowPrompt(slug, args = {}) {
|
|
218
277
|
const qs = new URLSearchParams(Object.entries(args).map(([k, v]) => [k, String(v)])).toString();
|
|
219
|
-
const r = await this.
|
|
220
|
-
if (!r.ok) throw new Error(`getWorkflowPrompt("${slug}") failed: HTTP ${r.status}`);
|
|
278
|
+
const r = await this._discoveryGet(`getWorkflowPrompt("${slug}")`, `/api/skill-packs/${encodeURIComponent(slug)}/prompt${qs ? `?${qs}` : ""}`);
|
|
221
279
|
return r.json();
|
|
222
280
|
}
|
|
223
281
|
|
|
@@ -238,9 +296,7 @@ export class Agent402 {
|
|
|
238
296
|
const top = Math.min(Math.max(parseInt(limit, 10) || 10, 1), 50);
|
|
239
297
|
const sortParam = sort === "calls" ? "calls" : "usd";
|
|
240
298
|
const includeParam = include === "all" ? "all" : "external";
|
|
241
|
-
const
|
|
242
|
-
const r = await this.f(url);
|
|
243
|
-
if (!r.ok) throw new Error(`topSellers failed: HTTP ${r.status}`);
|
|
299
|
+
const r = await this._discoveryGet("topSellers", `/api/leaderboard?top=${top}&sort=${sortParam}&include=${includeParam}`);
|
|
244
300
|
const snap = await r.json();
|
|
245
301
|
return {
|
|
246
302
|
window: snap.windowLabel || snap.windowServed || "24h",
|
|
@@ -260,10 +316,10 @@ export class Agent402 {
|
|
|
260
316
|
*
|
|
261
317
|
* @param {string} walletAddress the caller's wallet address (e.g. "0x...")
|
|
262
318
|
* @param {object} [opts]
|
|
263
|
-
* @param {typeof fetch} [opts.fetchImpl] plain fetch (defaults to global fetch)
|
|
319
|
+
* @param {typeof fetch} [opts.fetchImpl] plain fetch (defaults to global fetch through distrustFetch)
|
|
264
320
|
* @returns {Promise<{builderCode:string, walletAddress:string}>}
|
|
265
321
|
*/
|
|
266
|
-
static async registerBuilderCode(walletAddress, { fetchImpl =
|
|
322
|
+
static async registerBuilderCode(walletAddress, { fetchImpl = distrustFetch() } = {}) {
|
|
267
323
|
if (!walletAddress || typeof walletAddress !== "string") throw new Error("walletAddress is required");
|
|
268
324
|
const r = await fetchImpl("https://api.base.dev/v1/agents/builder-codes", {
|
|
269
325
|
method: "POST",
|
|
@@ -436,8 +492,7 @@ export class Agent402 {
|
|
|
436
492
|
}
|
|
437
493
|
|
|
438
494
|
async _powChallenge(slug) {
|
|
439
|
-
const r = await this.
|
|
440
|
-
if (!r.ok) throw new Error(`proof-of-work challenge for "${slug}" failed: HTTP ${r.status}`);
|
|
495
|
+
const r = await this._discoveryGet(`proof-of-work challenge for "${slug}"`, `/api/pow/challenge?slug=${encodeURIComponent(slug)}`);
|
|
441
496
|
return r.json();
|
|
442
497
|
}
|
|
443
498
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-client",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.14",
|
|
4
4
|
"description": "Agent402 buyer SDK: resolve a task to a tool across 500+ pay-per-call endpoints and call it with payment handled - free via proof-of-work, or over x402 or MPP (a stock mppx fetch works) with a wallet you keep; the buy side of Agentic Finance. Caching, idempotent retries (Idempotency-Key), non-custodial, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"index.js",
|
|
12
|
+
"tls-distrust.js",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
package/tls-distrust.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Refuse TLS chains through a distrusted intermediate CA, for this SDK's own
|
|
2
|
+
// requests only (nothing global changes in the caller's process).
|
|
3
|
+
//
|
|
4
|
+
// The private keys of the intermediate CAs below are recoverable (Mozilla bug
|
|
5
|
+
// 2083449). A cross-signed path makes them validate against "emSign ECC Root
|
|
6
|
+
// CA - G3" in Node's bundled store, and Node checks no revocation by default,
|
|
7
|
+
// so a certificate minted with one of these keys for a seller's host would be
|
|
8
|
+
// accepted, and a forged 402 could name a different payTo. The check is keyed
|
|
9
|
+
// on the SubjectPublicKeyInfo sha256 of every certificate in the presented
|
|
10
|
+
// chain. It can be removed once the Node releases this SDK supports ship a CA
|
|
11
|
+
// store that no longer builds the path.
|
|
12
|
+
//
|
|
13
|
+
// distrustFetch(baseFetch) returns a fetch that sends each request through a
|
|
14
|
+
// dedicated undici Agent whose connect.checkServerIdentity refuses such a
|
|
15
|
+
// chain, then runs Node's own hostname check. It applies only while the
|
|
16
|
+
// process-wide dispatcher is still Node's own default; once the caller has
|
|
17
|
+
// installed one (setGlobalDispatcher: a proxy, a custom CA, timeouts), the
|
|
18
|
+
// request goes out through theirs unchanged and their TLS choices win.
|
|
19
|
+
import tls from "node:tls";
|
|
20
|
+
import { createHash, X509Certificate } from "node:crypto";
|
|
21
|
+
|
|
22
|
+
export const DISTRUSTED_SPKI_SHA256 = Object.freeze({
|
|
23
|
+
"ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105": "EM DV TLS CA - G3B",
|
|
24
|
+
"43bffa19d6ef651451f6841e019931e96f6f2eaeff28ab4b6f1ce4576de2717b": "EM OV TLS CA - G3B",
|
|
25
|
+
"e0254c7fcdc265e2f0f8a428dabb75ab4ae039b4872d62f5ee57f0877d5a8a41": "EM EV TLS CA - G3B",
|
|
26
|
+
"49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f": "EM DV TLS CA - G3C",
|
|
27
|
+
"63ecefee431d49523bc1136c4e72d0206a477086caf606cd5cf025686c109cf8": "EM OV TLS CA - G3C",
|
|
28
|
+
"5c0660a2b164910d8ff589c18e14552e9e0ec237dcefe2cb736d41a4811f81a0": "EM EV TLS CA - G3C",
|
|
29
|
+
"ec4ffa1ad0ac88081314b7aaffef6a91f97cd57291bb5ba686c5415f5eb4f6b6": "EM OV CS CA - G3B",
|
|
30
|
+
"92c063d67e8f14db71d288e11266ffdaf69cbf115f7bab14f3bc2f68d25e0e30": "EM OV CS CA - G3C",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** checkServerIdentity that refuses a chain through a listed key (plus any
|
|
34
|
+
* `extraSpki` hex hashes), else defers to Node's hostname check. */
|
|
35
|
+
export function distrustCheckServerIdentity(extraSpki = []) {
|
|
36
|
+
const extra = new Set([...extraSpki].map((h) => String(h).toLowerCase()));
|
|
37
|
+
const nodeCheck = tls.checkServerIdentity;
|
|
38
|
+
return (host, cert) => {
|
|
39
|
+
const seen = [];
|
|
40
|
+
for (let c = cert; c && typeof c === "object" && !seen.includes(c) && seen.length < 16; c = c.issuerCertificate === c ? null : c.issuerCertificate) {
|
|
41
|
+
seen.push(c);
|
|
42
|
+
if (!c.raw) continue;
|
|
43
|
+
let spki;
|
|
44
|
+
try {
|
|
45
|
+
spki = createHash("sha256").update(new X509Certificate(c.raw).publicKey.export({ type: "spki", format: "der" })).digest("hex");
|
|
46
|
+
} catch {
|
|
47
|
+
return Object.assign(new Error(`Refusing the TLS certificate for ${host}: distrusted intermediate check could not read the certificate chain`), { code: "ERR_TLS_DISTRUSTED_INTERMEDIATE" });
|
|
48
|
+
}
|
|
49
|
+
const name = DISTRUSTED_SPKI_SHA256[spki] || (extra.has(spki) ? "listed key" : null);
|
|
50
|
+
if (name) return Object.assign(new Error(`Refusing the TLS certificate for ${host}: distrusted intermediate (${name}) in the certificate chain`), { code: "ERR_TLS_DISTRUSTED_INTERMEDIATE" });
|
|
51
|
+
}
|
|
52
|
+
return nodeCheck(host, cert);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const GLOBAL_DISPATCHER = Symbol.for("undici.globalDispatcher.1");
|
|
57
|
+
|
|
58
|
+
// Node's own default dispatcher, observed when this module loads. Node fills
|
|
59
|
+
// the slot when fetch is first called (synchronously, before any connection),
|
|
60
|
+
// so when the slot is still empty here a data: URL fetch fills it with Node's
|
|
61
|
+
// instance and nothing else can have put it there. When the slot is already
|
|
62
|
+
// filled at load, whoever filled it is unknown and it is treated as the
|
|
63
|
+
// caller's (no dedicated dispatcher), unless another copy of this code (the
|
|
64
|
+
// client and the MCP server share it) observed that same instance as Node's.
|
|
65
|
+
const OBSERVED = Symbol.for("agent402.tlsDistrust.nodeDefaultDispatcher");
|
|
66
|
+
function observeNodeDefault() {
|
|
67
|
+
if (typeof process !== "object" || !process?.versions?.node || process.versions.bun) return null;
|
|
68
|
+
const current = globalThis[GLOBAL_DISPATCHER];
|
|
69
|
+
if (current) return globalThis[OBSERVED] === current ? current : null;
|
|
70
|
+
if (typeof globalThis.fetch !== "function") return null;
|
|
71
|
+
try { globalThis.fetch("data:,").catch(() => {}); } catch { return null; }
|
|
72
|
+
const observed = globalThis[GLOBAL_DISPATCHER] || null;
|
|
73
|
+
if (observed) Object.defineProperty(globalThis, OBSERVED, { value: observed, configurable: false, enumerable: false, writable: false });
|
|
74
|
+
return observed;
|
|
75
|
+
}
|
|
76
|
+
const NODE_DEFAULT_DISPATCHER = observeNodeDefault();
|
|
77
|
+
|
|
78
|
+
/** True when the process-wide dispatcher is not Node's own default: the
|
|
79
|
+
* caller installed one, or its origin is unknown. */
|
|
80
|
+
export function callerDispatcherInstalled() {
|
|
81
|
+
return !NODE_DEFAULT_DISPATCHER || globalThis[GLOBAL_DISPATCHER] !== NODE_DEFAULT_DISPATCHER;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A dedicated undici Agent carrying the refusal, or null when this platform
|
|
85
|
+
* cannot take one or the caller installed their own global dispatcher (see
|
|
86
|
+
* the header). */
|
|
87
|
+
export async function distrustDispatcher({ extraSpki = [] } = {}) {
|
|
88
|
+
if (callerDispatcherInstalled()) return null;
|
|
89
|
+
const AgentClass = NODE_DEFAULT_DISPATCHER.constructor;
|
|
90
|
+
if (typeof AgentClass !== "function") return null;
|
|
91
|
+
return new AgentClass({ connect: { checkServerIdentity: distrustCheckServerIdentity(extraSpki) } });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Wrap a fetch so every request it makes goes through distrustDispatcher().
|
|
95
|
+
* With no baseFetch it calls globalThis.fetch as it is at request time. A
|
|
96
|
+
* caller's own init.dispatcher, or a global dispatcher the caller installed
|
|
97
|
+
* (checked on every request), is left alone. Pass the result as the base
|
|
98
|
+
* fetch of a payment wrapper (wrapFetchWithPayment(distrustFetch(), client))
|
|
99
|
+
* so the 402 and the paid retry both go through it. */
|
|
100
|
+
export function distrustFetch(baseFetch, opts = {}) {
|
|
101
|
+
if (baseFetch !== undefined && typeof baseFetch !== "function") throw new Error("distrustFetch needs a fetch function");
|
|
102
|
+
let pending = null;
|
|
103
|
+
return async (input, init = {}) => {
|
|
104
|
+
const f = baseFetch || globalThis.fetch;
|
|
105
|
+
if (callerDispatcherInstalled() || init?.dispatcher !== undefined) return f(input, init);
|
|
106
|
+
pending ||= distrustDispatcher(opts).catch(() => null);
|
|
107
|
+
const dispatcher = await pending;
|
|
108
|
+
return f(input, dispatcher ? { ...init, dispatcher } : init);
|
|
109
|
+
};
|
|
110
|
+
}
|