agent402-client 0.8.21 → 0.8.22
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 +8 -0
- package/index.js +10 -2
- package/package.json +2 -1
- package/reset-retry.js +47 -0
package/README.md
CHANGED
|
@@ -166,6 +166,14 @@ What the server does with it depends on how the call was paid:
|
|
|
166
166
|
route with the same body, from its first answer inside the replay window,
|
|
167
167
|
with no second settlement, and marks it `X-Replayed-Answer: 1` (identity-bound
|
|
168
168
|
routes, routes with a lasting effect and streamed calls excepted).
|
|
169
|
+
- **A connection reset before any answer:** the SDK's own requests (proof-of-work,
|
|
170
|
+
credits, discovery) are resent once when the connection failed before the
|
|
171
|
+
server answered, for example on a kept-alive socket the edge had already
|
|
172
|
+
closed (`retryResets: false` turns this off). Your `payFetch` is not wrapped,
|
|
173
|
+
because a retry over a payment wrapper would sign a fresh authorization. To get
|
|
174
|
+
the same resend on the wallet path, put it under the wrapper:
|
|
175
|
+
`wrapFetchWithPayment(resetRetryFetch(distrustFetch()), client)`. The resend
|
|
176
|
+
carries the same signed header, and one authorization settles at most once.
|
|
169
177
|
- **Circle Gateway batched option:** a payment the server has not had confirmed
|
|
170
178
|
by Gateway yet is answered `402` with reason `settlement-pending`; the SDK
|
|
171
179
|
resends the same signed header after the server's `Retry-After` (never a new
|
package/index.js
CHANGED
|
@@ -23,6 +23,8 @@ import { quoteOf, guardSigning, networkIdsOf, acceptUsd } from "./known-assets.j
|
|
|
23
23
|
export { KNOWN_ASSETS, NETWORK_NAMES, knownAsset, acceptUsd, networkIdsOf } from "./known-assets.js";
|
|
24
24
|
import { distrustFetch } from "./tls-distrust.js";
|
|
25
25
|
export { distrustFetch, distrustDispatcher, distrustCheckServerIdentity, DISTRUSTED_SPKI_SHA256 } from "./tls-distrust.js";
|
|
26
|
+
export { resetRetryFetch, isConnectionReset } from "./reset-retry.js";
|
|
27
|
+
import { resetRetryFetch } from "./reset-retry.js";
|
|
26
28
|
import { awaitJob, shouldRequestAsync, AsyncJobPendingError, DEFAULT_ASYNC_TIMEOUT_MS } from "./async-job.js";
|
|
27
29
|
export { AsyncJobPendingError } from "./async-job.js";
|
|
28
30
|
|
|
@@ -30,7 +32,7 @@ export { AsyncJobPendingError } from "./async-job.js";
|
|
|
30
32
|
// `User-Agent: agent402-client/<version>` - a standard header, no extra
|
|
31
33
|
// network calls - so a seller can attribute traffic (and settled payments)
|
|
32
34
|
// to this SDK. Product token only; nothing about the caller rides along.
|
|
33
|
-
const VERSION = "0.8.
|
|
35
|
+
const VERSION = "0.8.22";
|
|
34
36
|
const USER_AGENT = `agent402-client/${VERSION}`;
|
|
35
37
|
// A package built on this SDK (an adapter) names itself first: its own
|
|
36
38
|
// product token, then this one. Anything that is not one well-formed token
|
|
@@ -129,6 +131,8 @@ export class Agent402 {
|
|
|
129
131
|
* sent before agent402-client's in the User-Agent
|
|
130
132
|
* @param {boolean} [opts.tlsDistrust=true] send the default fetch through distrustFetch (refuses a TLS
|
|
131
133
|
* chain through a distrusted intermediate CA); false sends it as is
|
|
134
|
+
* @param {boolean} [opts.retryResets=true] resend the SDK's own request once when its connection failed
|
|
135
|
+
* before any answer (see resetRetryFetch); false sends each once
|
|
132
136
|
* @param {number} [opts.maxPerCallUsd] hard ceiling on a single paid call (USD); over → SpendingLimitError before paying
|
|
133
137
|
* @param {number} [opts.dailyLimitUsd] hard ceiling on rolling-24h paid spend (USD)
|
|
134
138
|
* @param {number} [opts.maxPerHostUsd] hard ceiling on rolling-24h paid spend to one seller host (USD)
|
|
@@ -163,7 +167,7 @@ export class Agent402 {
|
|
|
163
167
|
constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl = undefined, tlsDistrust = true,
|
|
164
168
|
maxPerCallUsd = null, dailyLimitUsd = null, maxPerHostUsd = null, creditsKey = null, x402Client = null, payNetworks = null,
|
|
165
169
|
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES, outputValidator = null,
|
|
166
|
-
respondAsync = "auto", asyncTimeoutMs = DEFAULT_ASYNC_TIMEOUT_MS, onProgress = null, gateway = null, userAgent = null } = {}) {
|
|
170
|
+
respondAsync = "auto", asyncTimeoutMs = DEFAULT_ASYNC_TIMEOUT_MS, onProgress = null, gateway = null, userAgent = null, retryResets = true } = {}) {
|
|
167
171
|
this.userAgent = userAgentOf(userAgent);
|
|
168
172
|
this.respondAsync = respondAsync;
|
|
169
173
|
this.gateway = normalizeGateway(gateway);
|
|
@@ -175,6 +179,10 @@ export class Agent402 {
|
|
|
175
179
|
if (typeof fetchImpl === "function" && tlsDistrust !== false) fetchImpl = distrustFetch(fetchImpl);
|
|
176
180
|
}
|
|
177
181
|
if (typeof fetchImpl !== "function") throw new Error("No fetch available - pass { fetchImpl } on Node < 18");
|
|
182
|
+
// The SDK's own requests resend once when the connection failed before any
|
|
183
|
+
// answer (a closed keep-alive socket). A caller's payFetch is not wrapped:
|
|
184
|
+
// retrying over a payment wrapper would sign a fresh authorization.
|
|
185
|
+
if (retryResets !== false) fetchImpl = resetRetryFetch(fetchImpl);
|
|
178
186
|
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
179
187
|
this.payFetch = payFetch || null;
|
|
180
188
|
// Wrap the plain fetch so every request identifies the SDK (see USER_AGENT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-client",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.22",
|
|
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",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"async-job.js",
|
|
13
13
|
"known-assets.js",
|
|
14
14
|
"tls-distrust.js",
|
|
15
|
+
"reset-retry.js",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"engines": {
|
package/reset-retry.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// resetRetryFetch(baseFetch) returns a fetch that resends a request ONCE when
|
|
2
|
+
// its connection failed before any response arrived: a kept-alive socket the
|
|
3
|
+
// server's edge already closed answers the next write with ECONNRESET (or
|
|
4
|
+
// undici's "other side closed"), and the request never reached the server.
|
|
5
|
+
//
|
|
6
|
+
// Only a transport error is retried, never an HTTP answer, and only for a
|
|
7
|
+
// body that can be sent again (none, a string, bytes, URLSearchParams, a
|
|
8
|
+
// Blob); a streamed body is not. The resend carries the same headers, so a
|
|
9
|
+
// signed payment is resent as signed: an EIP-3009 authorization's nonce can
|
|
10
|
+
// settle only once, and the server answers a re-sent signed payment from its
|
|
11
|
+
// first answer, so the resend can never pay twice. Wrap it UNDER a payment
|
|
12
|
+
// wrapper (wrapFetchWithPayment(resetRetryFetch(fetch), client)), never over
|
|
13
|
+
// one: over a wrapper, a retry would sign a fresh authorization.
|
|
14
|
+
const RESET_CODES = new Set(["ECONNRESET", "EPIPE", "UND_ERR_SOCKET", "UND_ERR_CLOSED", "ECONNABORTED"]);
|
|
15
|
+
|
|
16
|
+
/** Did this fetch error fail on the connection before any response? */
|
|
17
|
+
export function isConnectionReset(err) {
|
|
18
|
+
let e = err;
|
|
19
|
+
for (let i = 0; i < 5 && e; i++) {
|
|
20
|
+
if (RESET_CODES.has(e.code)) return true;
|
|
21
|
+
if (/other side closed|socket hang up/i.test(String(e.message || ""))) return true;
|
|
22
|
+
e = e.cause;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function replayableBody(body) {
|
|
28
|
+
if (body === undefined || body === null || typeof body === "string") return true;
|
|
29
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return true;
|
|
30
|
+
if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return true;
|
|
31
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resetRetryFetch(baseFetch = globalThis.fetch, { pauseMs = 300 } = {}) {
|
|
36
|
+
if (typeof baseFetch !== "function") throw new Error("resetRetryFetch needs a fetch function");
|
|
37
|
+
return async function fetchWithResetRetry(input, init) {
|
|
38
|
+
try {
|
|
39
|
+
return await baseFetch(input, init);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
const isRequest = typeof Request !== "undefined" && input instanceof Request;
|
|
42
|
+
if (!isConnectionReset(e) || init?.signal?.aborted || isRequest || !replayableBody(init?.body)) throw e;
|
|
43
|
+
await new Promise((r) => setTimeout(r, pauseMs));
|
|
44
|
+
return baseFetch(input, init);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|