agent402-client 0.8.23 → 0.8.24

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 CHANGED
@@ -398,7 +398,10 @@ can still redeem until its `validBefore`, so the error carries `paid: "unknown"`
398
398
  and `heldUsd`, and `spendingSummary()` reports it under `heldUsd`. A held
399
399
  amount counts for the rolling 24 hours, and longer when the authorization
400
400
  stays valid longer (a seven-day Circle Gateway authorization is held until its
401
- `validBefore` passes). Pass your `x402Client` so the client sees exactly which
401
+ `validBefore` passes). With an `x402Client`, an accept asking to stay valid for
402
+ more than a day (`MAX_VALIDITY_SECONDS`) is refused before anything is signed,
403
+ and a payload signed for longer is refused before it is sent, so no hold
404
+ outlives a day after the send (seven days and an hour for a batched payment). Pass your `x402Client` so the client sees exactly which
402
405
  payments were signed; through a pre-wrapped `fetch` alone it cannot tell, so any
403
406
  failed paid call is held. A paid HTTP-success response that later fails byte,
404
407
  JSON, or output validation remains settled spend. Free proof-of-work calls are
package/index.js CHANGED
@@ -19,8 +19,8 @@
19
19
  // const a = new Agent402({ fetch: payFetch });
20
20
  import { createHash, randomBytes } from "node:crypto";
21
21
  import { AsyncLocalStorage } from "node:async_hooks";
22
- import { quoteOf, guardSigning, networkIdsOf, acceptUsd } from "./known-assets.js";
23
- export { KNOWN_ASSETS, NETWORK_NAMES, knownAsset, acceptUsd, networkIdsOf } from "./known-assets.js";
22
+ import { quoteOf, guardSigning, networkIdsOf, acceptUsd, signedExpirySeconds, MAX_VALIDITY_SECONDS } from "./known-assets.js";
23
+ export { KNOWN_ASSETS, NETWORK_NAMES, knownAsset, acceptUsd, networkIdsOf, MAX_VALIDITY_SECONDS } 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
26
  export { resetRetryFetch, isConnectionReset } from "./reset-retry.js";
@@ -32,7 +32,7 @@ export { AsyncJobPendingError } from "./async-job.js";
32
32
  // `User-Agent: agent402-client/<version>` - a standard header, no extra
33
33
  // network calls - so a seller can attribute traffic (and settled payments)
34
34
  // to this SDK. Product token only; nothing about the caller rides along.
35
- const VERSION = "0.8.23";
35
+ const VERSION = "0.8.24";
36
36
  const USER_AGENT = `agent402-client/${VERSION}`;
37
37
  // A package built on this SDK (an adapter) names itself first: its own
38
38
  // product token, then this one. Anything that is not one well-formed token
@@ -119,13 +119,15 @@ export const HOLD_SLACK_MS = 10 * 60 * 1000;
119
119
 
120
120
  /** When a signed payment payload can no longer settle, in ms since the epoch
121
121
  * plus HOLD_SLACK_MS: an EIP-3009 `authorization.validBefore` or a Permit2
122
- * `deadline`, in seconds. null when the payload names no expiry this reads
123
- * (a Solana transaction, an unknown scheme). */
124
- export function signedValidUntilMs(paymentPayload) {
125
- const p = paymentPayload?.payload;
126
- const v = p?.authorization?.validBefore ?? p?.permit2Authorization?.deadline;
127
- if (v == null || !/^\d{1,15}$/.test(String(v))) return null;
128
- return Number(v) * 1000 + HOLD_SLACK_MS;
122
+ * `deadline`, in seconds, never later than `maxSeconds` after `nowMs` (the
123
+ * longest window this client signs: MAX_VALIDITY_SECONDS, or
124
+ * GATEWAY_MAX_TIMEOUT_SECONDS for a batched payment), so no hold outlives
125
+ * the ceiling a payload must pass to be sent. null when the payload names no
126
+ * expiry this reads (a Solana transaction, an unknown scheme). */
127
+ export function signedValidUntilMs(paymentPayload, { maxSeconds = MAX_VALIDITY_SECONDS, nowMs = Date.now() } = {}) {
128
+ const v = signedExpirySeconds(paymentPayload);
129
+ if (v == null || !Number.isFinite(v)) return null;
130
+ return Math.min(v * 1000, nowMs + maxSeconds * 1000) + HOLD_SLACK_MS;
129
131
  }
130
132
 
131
133
  /** A spend-log entry the caps still count at `now`: anything recorded inside
@@ -688,8 +690,10 @@ export class Agent402 {
688
690
  const d = ctx.refusal;
689
691
  throw new SpendingLimitError(d.reason === "unknown-asset"
690
692
  ? `refusing to pay for "${slug}" - no payment option it asked to sign is in a known asset, so none can be priced`
691
- : d.reason === "signed-amount"
693
+ : d.reason === "signed-amount" || d.reason === "signed-validity"
692
694
  ? `refusing to pay for "${slug}" - ${d.detail}, so the payment was not sent`
695
+ : d.reason === "validity"
696
+ ? `refusing to pay for "${slug}" - ${d.detail}, so nothing was signed`
693
697
  : `refusing to pay $${d.priceUsd} for "${slug}" - the payment it asked to sign is over ${d.limit || "the cap"} ($${d.allowanceUsd} left to sign)`,
694
698
  { limit: d.limit || "signing", slug, priceUsd: d.priceUsd, cap: d.allowanceUsd, atSigning: true, cause: e, ...(e?.paid === "unknown" ? { paid: "unknown", heldUsd: e.heldUsd } : {}) });
695
699
  }
@@ -801,7 +805,7 @@ export class Agent402 {
801
805
  // claim on the deposit until its validBefore whatever the seller answers
802
806
  // (a refusal, a failure, a redirect, a job still running), so its signed
803
807
  // amount is held against the caps from the first send.
804
- const sent = [{ usd: acceptedUsd, until: signedValidUntilMs(payload) }];
808
+ const sent = [{ usd: acceptedUsd, until: signedValidUntilMs(payload, { maxSeconds: GATEWAY_MAX_TIMEOUT_SECONDS }) }];
805
809
  const heldUsd = this._spendHold(reservation, sent);
806
810
  const paidHeaders = { "PAYMENT-SIGNATURE": header, ...asyncHeaders };
807
811
  let r;
package/known-assets.js CHANGED
@@ -134,6 +134,51 @@ export function quoteOf(accepts, x402Version = undefined) {
134
134
  return { max, min, unknown, total: accepts.length };
135
135
  }
136
136
 
137
+ // The longest a signed payment may stay redeemable. An EVM "exact" signer
138
+ // sets validBefore (EIP-3009) or deadline (Permit2) to the moment of signing
139
+ // plus the accept's maxTimeoutSeconds, and the signed payload is a bearer
140
+ // claim until then, so a seller that quotes years gets an authorization it
141
+ // could redeem for years and a held amount that never stops counting. Honest
142
+ // accepts ask for minutes (Agent402's own ask 300 s); a day admits every
143
+ // one of them. An accept that asks for longer is dropped before selection,
144
+ // and a payload signed past it is refused before it is sent.
145
+ export const MAX_VALIDITY_SECONDS = 24 * 3600;
146
+ // Signing reads the clock a moment before the check after it does.
147
+ const VALIDITY_CLOCK_SLACK_SECONDS = 60;
148
+
149
+ /** True when an accept asks for a validity window past MAX_VALIDITY_SECONDS.
150
+ * An accept with no readable maxTimeoutSeconds is left to the check of what
151
+ * was signed (signedValidityProblem). */
152
+ export function acceptValidityTooLong(accept, maxSeconds = MAX_VALIDITY_SECONDS) {
153
+ const raw = accept?.maxTimeoutSeconds;
154
+ if (raw == null || raw === "") return false;
155
+ const n = Number(raw);
156
+ return !Number.isFinite(n) || n > maxSeconds;
157
+ }
158
+
159
+ /** The expiry a signed payload carries, in unix seconds: an EIP-3009
160
+ * `authorization.validBefore` or a Permit2 `deadline`. null when the payload
161
+ * names none (a Solana transaction, an unknown scheme). NaN when it names
162
+ * one that is not a whole number of seconds. */
163
+ export function signedExpirySeconds(paymentPayload) {
164
+ const p = paymentPayload?.payload;
165
+ const v = p?.authorization?.validBefore ?? p?.permit2Authorization?.deadline;
166
+ if (v == null) return null;
167
+ return /^\d{1,15}$/.test(String(v)) ? Number(v) : NaN;
168
+ }
169
+
170
+ /** Why a created payment payload stays redeemable for too long, or null: its
171
+ * expiry is past `nowMs` plus `maxSeconds` (and a minute of clock slack), or
172
+ * it names an expiry that cannot be read. */
173
+ export function signedValidityProblem(paymentPayload, { maxSeconds = MAX_VALIDITY_SECONDS, nowMs = Date.now() } = {}) {
174
+ const exp = signedExpirySeconds(paymentPayload);
175
+ if (exp == null) return null;
176
+ if (!Number.isFinite(exp)) return "the signed authorization's expiry cannot be read";
177
+ const left = exp - Math.floor(nowMs / 1000);
178
+ if (left > maxSeconds + VALIDITY_CLOCK_SLACK_SECONDS) return `the signed authorization stays valid for ${left} seconds, past the ${maxSeconds}-second ceiling`;
179
+ return null;
180
+ }
181
+
137
182
  /** Enforce a USD allowance at signing on an @x402/core x402Client.
138
183
  *
139
184
  * `allowance()` is the most this buyer may sign for the payment being built
@@ -141,8 +186,10 @@ export function quoteOf(accepts, x402Version = undefined) {
141
186
  * for none). A registered policy drops every accept that cannot be priced or
142
187
  * is over the allowance, so the client selects among the ones that fit, and a
143
188
  * hook before payload creation checks the selected accept again and passes
144
- * its USD value to `onSign`. A refusal throws (from the policy) or aborts
145
- * (from the hook) before any signature exists; `onRefuse` hears why first. */
189
+ * its USD value to `onSign`. An accept asking for a validity window past
190
+ * MAX_VALIDITY_SECONDS is dropped and refused the same way. A refusal throws
191
+ * (from the policy) or aborts (from the hook) before any signature exists;
192
+ * `onRefuse` hears why first. */
146
193
  export function guardSigning(client, { allowance, onSign = null, onRefuse = null }) {
147
194
  if (!client || typeof client.registerPolicy !== "function" || typeof client.onBeforePaymentCreation !== "function") {
148
195
  throw new TypeError("guardSigning needs an @x402/core x402Client (registerPolicy and onBeforePaymentCreation)");
@@ -151,7 +198,12 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
151
198
  const refuse = (detail) => { try { onRefuse?.(detail); } catch { /* the refusal stands */ } return detail; };
152
199
  client.registerPolicy((version, accepts) => {
153
200
  const cap = allowance();
154
- const kept = accepts.filter((a) => { const usd = acceptUsd(a, version); return usd != null && usd <= cap; });
201
+ const priced = accepts.filter((a) => { const usd = acceptUsd(a, version); return usd != null && usd <= cap; });
202
+ const kept = priced.filter((a) => !acceptValidityTooLong(a));
203
+ if (!kept.length && priced.length) {
204
+ const d = refuse({ reason: "validity", priceUsd: null, allowanceUsd: cap, detail: `every payment option it may sign asks to stay valid for more than ${MAX_VALIDITY_SECONDS} seconds` });
205
+ throw new Error(`spending guard: ${d.detail}; nothing was signed`);
206
+ }
155
207
  if (!kept.length) {
156
208
  const q = quoteOf(accepts, version);
157
209
  const d = refuse({ reason: q && q.min != null ? "over-allowance" : "unknown-asset", priceUsd: q ? q.min : null, allowanceUsd: cap });
@@ -172,6 +224,10 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
172
224
  refuse({ reason: "over-allowance", priceUsd: usd, allowanceUsd: cap });
173
225
  return { abort: true, reason: `spending guard: the selected payment option asks $${usd}, above the $${cap} this buyer may sign; nothing was signed` };
174
226
  }
227
+ if (acceptValidityTooLong(r)) {
228
+ const d = refuse({ reason: "validity", priceUsd: usd, allowanceUsd: cap, detail: `the selected payment option asks to stay valid for ${r.maxTimeoutSeconds} seconds, past the ${MAX_VALIDITY_SECONDS}-second ceiling` });
229
+ return { abort: true, reason: `spending guard: ${d.detail}; nothing was signed` };
230
+ }
175
231
  onSign?.(usd, r);
176
232
  return undefined;
177
233
  });
@@ -252,9 +308,10 @@ export async function signedPaymentProblem(paymentPayload, requirements, x402Ver
252
308
  }
253
309
 
254
310
  /** Refuse to send a payment payload whose signed amount is not the one the
255
- * allowance passed: registered after payload creation, so the check reads
256
- * what was actually signed, and a throw here stops the payment-aware fetch
257
- * before the payload leaves the process. guardSigning registers it; a
311
+ * allowance passed, or whose expiry is past MAX_VALIDITY_SECONDS from now
312
+ * (signedValidityProblem): registered after payload creation, so the check
313
+ * reads what was actually signed, and a throw here stops the payment-aware
314
+ * fetch before the payload leaves the process. guardSigning registers it; a
258
315
  * client with its own selector (and no guardSigning) can register it alone. */
259
316
  export function guardSignedAmount(client, { allowance = () => Infinity, onRefuse = null } = {}) {
260
317
  if (!client || typeof client.onAfterPaymentCreation !== "function") {
@@ -262,9 +319,10 @@ export function guardSignedAmount(client, { allowance = () => Infinity, onRefuse
262
319
  }
263
320
  client.onAfterPaymentCreation(async ({ paymentRequired, selectedRequirements: r, paymentPayload }) => {
264
321
  const cap = allowance();
265
- const problem = await signedPaymentProblem(paymentPayload, r, paymentRequired?.x402Version, cap);
322
+ const amountProblem = await signedPaymentProblem(paymentPayload, r, paymentRequired?.x402Version, cap);
323
+ const problem = amountProblem || signedValidityProblem(paymentPayload);
266
324
  if (!problem) return;
267
- try { onRefuse?.({ reason: "signed-amount", priceUsd: null, allowanceUsd: cap, detail: problem }); } catch { /* the refusal stands */ }
325
+ try { onRefuse?.({ reason: amountProblem ? "signed-amount" : "signed-validity", priceUsd: null, allowanceUsd: cap, detail: problem }); } catch { /* the refusal stands */ }
268
326
  throw new Error(`spending guard: ${problem}; the payment was not sent`);
269
327
  });
270
328
  return client;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-client",
3
- "version": "0.8.23",
3
+ "version": "0.8.24",
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",