agent402-client 0.8.13 → 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 +13 -5
- 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
|
|
@@ -66,7 +68,9 @@ export class Agent402 {
|
|
|
66
68
|
* @param {string} [opts.baseUrl="https://agent402.tools"]
|
|
67
69
|
* @param {typeof fetch} [opts.fetch] an x402-wrapped fetch for wallet-only tools (optional)
|
|
68
70
|
* @param {boolean} [opts.cache=true] cache results in memory (deterministic tools)
|
|
69
|
-
* @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
|
|
70
74
|
* @param {number} [opts.maxPerCallUsd] hard ceiling on a single paid call (USD); over → SpendingLimitError before paying
|
|
71
75
|
* @param {number} [opts.dailyLimitUsd] hard ceiling on rolling-24h paid spend (USD)
|
|
72
76
|
* @param {number} [opts.maxPerHostUsd] hard ceiling on rolling-24h paid spend to one seller host (USD)
|
|
@@ -78,10 +82,14 @@ export class Agent402 {
|
|
|
78
82
|
* Optional buyer-owned result validator. `id` namespaces cache entries;
|
|
79
83
|
* `validate` may return false or throw to reject delivery.
|
|
80
84
|
*/
|
|
81
|
-
constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl =
|
|
85
|
+
constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl = undefined, tlsDistrust = true,
|
|
82
86
|
maxPerCallUsd = null, dailyLimitUsd = null, maxPerHostUsd = null, creditsKey = null,
|
|
83
87
|
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES, outputValidator = null } = {}) {
|
|
84
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
|
+
}
|
|
85
93
|
if (typeof fetchImpl !== "function") throw new Error("No fetch available - pass { fetchImpl } on Node < 18");
|
|
86
94
|
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
87
95
|
this.payFetch = payFetch || null;
|
|
@@ -308,10 +316,10 @@ export class Agent402 {
|
|
|
308
316
|
*
|
|
309
317
|
* @param {string} walletAddress the caller's wallet address (e.g. "0x...")
|
|
310
318
|
* @param {object} [opts]
|
|
311
|
-
* @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)
|
|
312
320
|
* @returns {Promise<{builderCode:string, walletAddress:string}>}
|
|
313
321
|
*/
|
|
314
|
-
static async registerBuilderCode(walletAddress, { fetchImpl =
|
|
322
|
+
static async registerBuilderCode(walletAddress, { fetchImpl = distrustFetch() } = {}) {
|
|
315
323
|
if (!walletAddress || typeof walletAddress !== "string") throw new Error("walletAddress is required");
|
|
316
324
|
const r = await fetchImpl("https://api.base.dev/v1/agents/builder-codes", {
|
|
317
325
|
method: "POST",
|
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
|
+
}
|