@aifinpay/agent 2.0.0-rc.8 → 2.0.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.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/PAYMENT_RECEIPTS.md +114 -0
  3. package/README.md +85 -28
  4. package/dist/agent.d.ts +8 -1
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +50 -17
  7. package/dist/agent.js.map +1 -1
  8. package/dist/agentHistory.d.ts +55 -0
  9. package/dist/agentHistory.d.ts.map +1 -0
  10. package/dist/agentHistory.js +145 -0
  11. package/dist/agentHistory.js.map +1 -0
  12. package/dist/agentPassport.d.ts +71 -7
  13. package/dist/agentPassport.d.ts.map +1 -1
  14. package/dist/agentPassport.js +135 -27
  15. package/dist/agentPassport.js.map +1 -1
  16. package/dist/aifp1.d.ts +94 -5
  17. package/dist/aifp1.d.ts.map +1 -1
  18. package/dist/aifp1.js +370 -114
  19. package/dist/aifp1.js.map +1 -1
  20. package/dist/apiUrl.d.ts +3 -0
  21. package/dist/apiUrl.d.ts.map +1 -0
  22. package/dist/apiUrl.js +8 -0
  23. package/dist/apiUrl.js.map +1 -0
  24. package/dist/chains.d.ts +7 -0
  25. package/dist/chains.d.ts.map +1 -1
  26. package/dist/chains.js +19 -1
  27. package/dist/chains.js.map +1 -1
  28. package/dist/crossChain.d.ts.map +1 -1
  29. package/dist/crossChain.js.map +1 -1
  30. package/dist/crypto.d.ts.map +1 -1
  31. package/dist/crypto.js.map +1 -1
  32. package/dist/deploymentResolver.d.ts +95 -0
  33. package/dist/deploymentResolver.d.ts.map +1 -0
  34. package/dist/deploymentResolver.js +169 -0
  35. package/dist/deploymentResolver.js.map +1 -0
  36. package/dist/errors.d.ts +17 -0
  37. package/dist/errors.d.ts.map +1 -1
  38. package/dist/errors.js +14 -0
  39. package/dist/errors.js.map +1 -1
  40. package/dist/facilitators/aifinpay.d.ts +4 -5
  41. package/dist/facilitators/aifinpay.d.ts.map +1 -1
  42. package/dist/facilitators/aifinpay.js +50 -21
  43. package/dist/facilitators/aifinpay.js.map +1 -1
  44. package/dist/facilitators/base.d.ts +12 -1
  45. package/dist/facilitators/base.d.ts.map +1 -1
  46. package/dist/facilitators/coinbase.d.ts.map +1 -1
  47. package/dist/facilitators/coinbase.js +3 -7
  48. package/dist/facilitators/coinbase.js.map +1 -1
  49. package/dist/facilitators/detect.d.ts.map +1 -1
  50. package/dist/facilitators/detect.js +2 -7
  51. package/dist/facilitators/detect.js.map +1 -1
  52. package/dist/facilitators/standard-x402.d.ts.map +1 -1
  53. package/dist/facilitators/standard-x402.js +40 -30
  54. package/dist/facilitators/standard-x402.js.map +1 -1
  55. package/dist/generated/solanaV14Deployments.generated.d.ts +25 -0
  56. package/dist/generated/solanaV14Deployments.generated.d.ts.map +1 -0
  57. package/dist/generated/solanaV14Deployments.generated.js +39 -0
  58. package/dist/generated/solanaV14Deployments.generated.js.map +1 -0
  59. package/dist/generated/splitterRoutes.generated.d.ts +24 -0
  60. package/dist/generated/splitterRoutes.generated.d.ts.map +1 -0
  61. package/dist/generated/splitterRoutes.generated.js +453 -0
  62. package/dist/generated/splitterRoutes.generated.js.map +1 -0
  63. package/dist/generated/v14Deployments.generated.d.ts +49 -0
  64. package/dist/generated/v14Deployments.generated.d.ts.map +1 -0
  65. package/dist/generated/v14Deployments.generated.js +415 -0
  66. package/dist/generated/v14Deployments.generated.js.map +1 -0
  67. package/dist/index.d.ts +27 -13
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +16 -7
  70. package/dist/index.js.map +1 -1
  71. package/dist/localIdentity.d.ts +17 -0
  72. package/dist/localIdentity.d.ts.map +1 -0
  73. package/dist/localIdentity.js +95 -0
  74. package/dist/localIdentity.js.map +1 -0
  75. package/dist/settlement.d.ts +34 -4
  76. package/dist/settlement.d.ts.map +1 -1
  77. package/dist/settlement.js +233 -75
  78. package/dist/settlement.js.map +1 -1
  79. package/dist/settlementHttp.d.ts +10 -0
  80. package/dist/settlementHttp.d.ts.map +1 -0
  81. package/dist/settlementHttp.js +78 -0
  82. package/dist/settlementHttp.js.map +1 -0
  83. package/dist/settlementV14.d.ts +10 -9
  84. package/dist/settlementV14.d.ts.map +1 -1
  85. package/dist/settlementV14.js +43 -55
  86. package/dist/settlementV14.js.map +1 -1
  87. package/dist/solanaDeploymentResolver.d.ts +98 -0
  88. package/dist/solanaDeploymentResolver.d.ts.map +1 -0
  89. package/dist/solanaDeploymentResolver.js +162 -0
  90. package/dist/solanaDeploymentResolver.js.map +1 -0
  91. package/dist/solanaV14Deployments.generated.d.ts +25 -0
  92. package/dist/solanaV14Deployments.generated.d.ts.map +1 -0
  93. package/dist/solanaV14Deployments.generated.js +39 -0
  94. package/dist/solanaV14Deployments.generated.js.map +1 -0
  95. package/dist/spendLedger.d.ts.map +1 -1
  96. package/dist/spendLedger.js +1 -3
  97. package/dist/spendLedger.js.map +1 -1
  98. package/dist/splitterRoutes.d.ts +11 -4
  99. package/dist/splitterRoutes.d.ts.map +1 -1
  100. package/dist/splitterRoutes.generated.d.ts +11 -9
  101. package/dist/splitterRoutes.generated.d.ts.map +1 -1
  102. package/dist/splitterRoutes.generated.js +72 -18
  103. package/dist/splitterRoutes.generated.js.map +1 -1
  104. package/dist/splitterRoutes.js +7 -5
  105. package/dist/splitterRoutes.js.map +1 -1
  106. package/dist/unifiedAgent.d.ts +96 -28
  107. package/dist/unifiedAgent.d.ts.map +1 -1
  108. package/dist/unifiedAgent.js +219 -348
  109. package/dist/unifiedAgent.js.map +1 -1
  110. package/dist/v14Deployments.generated.d.ts +49 -0
  111. package/dist/v14Deployments.generated.d.ts.map +1 -0
  112. package/dist/v14Deployments.generated.js +413 -0
  113. package/dist/v14Deployments.generated.js.map +1 -0
  114. package/dist/wallet.d.ts.map +1 -1
  115. package/dist/wallet.js +2 -2
  116. package/dist/wallet.js.map +1 -1
  117. package/package.json +27 -6
  118. package/dist/cli.d.ts +0 -3
  119. package/dist/cli.d.ts.map +0 -1
  120. package/dist/cli.js +0 -127
  121. package/dist/cli.js.map +0 -1
package/dist/aifp1.js CHANGED
@@ -34,7 +34,10 @@
34
34
  // the whole protocol without an RPC endpoint.
35
35
  // ──────────────────────────────────────────────────────────────────────────
36
36
  import { createHash } from "node:crypto";
37
+ import { keccak256, stringToHex } from "viem";
37
38
  import { AiFinPayError } from "./errors.js";
39
+ import { SettlementConfirmationPendingError } from "./settlement.js";
40
+ import { settlementHttp, SettlementHttpError } from "./settlementHttp.js";
38
41
  // ── Errors ────────────────────────────────────────────────────────────────
39
42
  //
40
43
  // Specific classes rather than one message, because the recoveries differ: a
@@ -55,25 +58,30 @@ export class Aifp1SettlementUnsupportedError extends Aifp1Error {
55
58
  * Carries the tx hash because that is the only thing standing between the
56
59
  * caller and a lost payment: the quote is still settleable through
57
60
  * `POST /v1/pay` by hand with the same Idempotency-Key, and without the hash
58
- * in the error there is nothing to retry with.
61
+ * in the error there is nothing to retry with. `recovery` additionally preserves
62
+ * the quote nonce and issuer domain needed to sign a fresh receipt claim.
59
63
  */
60
64
  export class Aifp1PayError extends Aifp1Error {
61
65
  txRef;
62
66
  quoteId;
63
- constructor(msg, txRef, quoteId) {
67
+ recovery;
68
+ constructor(msg, txRef, quoteId, recovery) {
64
69
  super(msg);
65
70
  this.txRef = txRef;
66
71
  this.quoteId = quoteId;
72
+ this.recovery = recovery;
67
73
  }
68
74
  }
75
+ /** Recover an already settled Polygon payment. Never sends an on-chain transaction. */
76
+ export function recoverAifp1Payment(recovery, signer, options = {}) {
77
+ return submitPayment({ ...signer, agentId: signer.payerAddress, fetchImpl: signer.fetchImpl ?? fetch }, recovery.apiBaseUrl, recovery.quote, recovery.txRef, recovery.asset, { paymentIssuer: recovery.paymentIssuer, ...options });
78
+ }
69
79
  /** The gateway rejected a receipt we believed covered the request. */
70
80
  export class Aifp1ReceiptRejectedError extends Aifp1Error {
71
81
  }
72
82
  export function describeQuote(q) {
73
83
  const ns = q.native_settlement;
74
- const pay = ns
75
- ? { amount: formatUnits(ns.total_wei, ns.decimals), asset: ns.asset }
76
- : null;
84
+ const pay = ns ? { amount: formatUnits(ns.total_wei, ns.decimals), asset: ns.asset } : null;
77
85
  // Fee as basis points, from the split the quote already carries. Stated so the
78
86
  // agent sees the rate, not just the total — the same reason the 402 does.
79
87
  let feeBps = null;
@@ -83,9 +91,13 @@ export function describeQuote(q) {
83
91
  if (total > 0n)
84
92
  feeBps = Number(((total - merchant) * 10000n) / total);
85
93
  }
86
- catch { /* leave null rather than guess */ }
87
- const where = q.scope === "merchant" ? `anything on ${q.merchant_id}`
88
- : q.scope === "prefix" ? `any path under ${q.resource}`
94
+ catch {
95
+ /* leave null rather than guess */
96
+ }
97
+ const where = q.scope === "merchant"
98
+ ? `anything on ${q.merchant_id}`
99
+ : q.scope === "prefix"
100
+ ? `any path under ${q.resource}`
89
101
  : q.resource;
90
102
  const payPart = pay ? `${pay.amount} ${pay.asset}` : `$${q.amount}`;
91
103
  const headline = `Pay ${payPart} ($${q.amount}) for ${q.requests} request${q.requests === 1 ? "" : "s"} ` +
@@ -139,7 +151,9 @@ export function scopeCovers(scope, resource, path) {
139
151
  * and a single-segment path → "/" (the whole site).
140
152
  */
141
153
  export function prefixHint(path) {
142
- const segs = String(path || "/").split("/").filter(Boolean);
154
+ const segs = String(path || "/")
155
+ .split("/")
156
+ .filter(Boolean);
143
157
  return segs.length > 1 ? `/${segs[0]}/` : "/";
144
158
  }
145
159
  /**
@@ -166,16 +180,22 @@ export function prefixHint(path) {
166
180
  * treated as "allow all", because that is the shape a config bug takes.
167
181
  */
168
182
  export const DEFAULT_GATEWAY_ORIGINS = ["https://gateway.aifinpay.io"];
169
- export function parseGatewayUrl(url, allowedOrigins = DEFAULT_GATEWAY_ORIGINS) {
183
+ export function parseGatewayUrl(url, allowedOrigins = DEFAULT_GATEWAY_ORIGINS, resourcePathMode = "gateway") {
184
+ if (resourcePathMode !== "gateway" && resourcePathMode !== "direct") {
185
+ throw new Aifp1Error("resourcePathMode must be 'gateway' or 'direct'");
186
+ }
170
187
  const u = new URL(url);
171
188
  if (!allowedOrigins.length) {
172
- throw new Aifp1Error("no allowed gateway origins configured — refusing to pay any host. "
173
- + "Pass gatewayOrigins explicitly for a self-hosted gateway.");
189
+ throw new Aifp1Error("no allowed gateway origins configured — refusing to pay any host. " +
190
+ "Pass gatewayOrigins explicitly for a self-hosted gateway.");
174
191
  }
175
192
  if (!allowedOrigins.includes(u.origin)) {
176
- throw new Aifp1Error(`${u.origin} is not a known AiFinPay gateway (allowed: ${allowedOrigins.join(", ")}). `
177
- + "Refusing to settle: a 402 is unauthenticated, so paying an unrecognised host "
178
- + "means paying whoever answered.");
193
+ throw new Aifp1Error(`${u.origin} is not a known AiFinPay gateway (allowed: ${allowedOrigins.join(", ")}). ` +
194
+ "Refusing to settle: a 402 is unauthenticated, so paying an unrecognised host " +
195
+ "means paying whoever answered.");
196
+ }
197
+ if (resourcePathMode === "direct") {
198
+ return { site: u.origin, slug: "", restPath: u.pathname };
179
199
  }
180
200
  const segments = u.pathname.split("/").filter(Boolean);
181
201
  if (segments.length === 0) {
@@ -228,9 +248,7 @@ export function jwtExpiryMs(jwt) {
228
248
  * A random key would satisfy the header requirement and none of the above.
229
249
  */
230
250
  export function idempotencyKeyFor(p) {
231
- const digest = createHash("sha256")
232
- .update(`aifp1|${p.quoteId}|${p.asset}|${p.chain}|${p.txRef}`)
233
- .digest("hex");
251
+ const digest = createHash("sha256").update(`aifp1|${p.quoteId}|${p.asset}|${p.chain}|${p.txRef}`).digest("hex");
234
252
  return `aifp1-${digest}`; // 70 chars — well under the server's 200 limit
235
253
  }
236
254
  /**
@@ -272,10 +290,14 @@ export class Aifp1ReceiptCache {
272
290
  async coalesce(site, buy) {
273
291
  const running = this.inflight.get(site);
274
292
  if (running) {
275
- await running.catch(() => { });
293
+ // The winner may already have paid. Share its failure and recovery
294
+ // context; swallowing it would let every waiter buy another batch.
295
+ await running;
276
296
  return "retry";
277
297
  }
278
- const p = buy().finally(() => { this.inflight.delete(site); });
298
+ const p = buy().finally(() => {
299
+ this.inflight.delete(site);
300
+ });
279
301
  this.inflight.set(site, p);
280
302
  return p;
281
303
  }
@@ -297,11 +319,11 @@ export class Aifp1ReceiptCache {
297
319
  */
298
320
  find(site, restPath, merchantId) {
299
321
  const now = Date.now();
300
- return this.entries.find((e) => e.site === site
301
- && (merchantId === undefined || e.merchantId === merchantId)
302
- && e.remaining > 0
303
- && e.expiresAt - EXPIRY_MARGIN_MS > now
304
- && scopeCovers(e.scope, e.resource, restPath));
322
+ return this.entries.find((e) => e.site === site &&
323
+ (merchantId === undefined || e.merchantId === merchantId) &&
324
+ e.remaining > 0 &&
325
+ e.expiresAt - EXPIRY_MARGIN_MS > now &&
326
+ scopeCovers(e.scope, e.resource, restPath));
305
327
  }
306
328
  put(entry) {
307
329
  this.entries.push(entry);
@@ -321,7 +343,16 @@ export class Aifp1ReceiptCache {
321
343
  const now = Date.now();
322
344
  return this.entries.filter((e) => e.expiresAt > now);
323
345
  }
324
- get size() { return this.entries.length; }
346
+ /**
347
+ * Redacted view for dashboards and MCP tools: everything except the bearer
348
+ * `jwt` (a credential — only the paying agent holds it). Safe to log.
349
+ */
350
+ summary() {
351
+ return this.list().map(({ jwt: _jwt, ...rest }) => rest);
352
+ }
353
+ get size() {
354
+ return this.entries.length;
355
+ }
325
356
  }
326
357
  /** Default batch size: 1000 billing units — the $0.10 floor, and the size the
327
358
  * gateway's own how_to_pay examples suggest (routes/gateway.js). If an
@@ -338,7 +369,7 @@ export class Aifp1ReceiptCache {
338
369
  * dollars. Fixing the constant would have fixed today and broken the next
339
370
  * re-price, so the count is derived instead.
340
371
  */
341
- const DEFAULT_BATCH_USD = 0.10;
372
+ const DEFAULT_BATCH_USD = 0.1;
342
373
  /** Used only when a 402 omits base_unit_price_usd — matches $0.10 at $0.0005. */
343
374
  const FALLBACK_UNITS = 200;
344
375
  /** Units to buy so the batch is worth about DEFAULT_BATCH_USD. */
@@ -397,13 +428,25 @@ function validateCanonicalQuoteEconomics(quote) {
397
428
  const protocol = BigInt(s.protocol_fee_units);
398
429
  const creator = BigInt(s.creator_units);
399
430
  const expectedProtocol = gross / 100n;
400
- if (s.fee_on_top !== false
401
- || gross <= 0n
402
- || payer !== gross
403
- || total !== gross
404
- || creator !== 0n
405
- || protocol !== expectedProtocol
406
- || merchant !== gross - expectedProtocol) {
431
+ // Older backend clients consume this unfortunately named breakdown. It
432
+ // cannot be dropped during a rolling upgrade. Only accept it as inclusive
433
+ // when the explicit semantics and every leg agree with the canonical data.
434
+ const legacy = s.fee_on_top;
435
+ const matches = (value, expected) => typeof value === "string" && /^\d+$/.test(value) && BigInt(value) === expected;
436
+ const inclusiveLegacy = s.settlement_semantics === "gross-inclusive" &&
437
+ legacy !== null &&
438
+ typeof legacy === "object" &&
439
+ !Array.isArray(legacy) &&
440
+ matches(legacy.provider, merchant) &&
441
+ matches(legacy.treasury, protocol) &&
442
+ matches(legacy.creator, creator);
443
+ if ((s.fee_on_top !== false && !inclusiveLegacy) ||
444
+ gross <= 0n ||
445
+ payer !== gross ||
446
+ total !== gross ||
447
+ creator !== 0n ||
448
+ protocol !== expectedProtocol ||
449
+ merchant !== gross - expectedProtocol) {
407
450
  throw new Error("split mismatch");
408
451
  }
409
452
  }
@@ -414,8 +457,7 @@ function validateCanonicalQuoteEconomics(quote) {
414
457
  /** Is this response the AIFP-1 paywall asking for money? */
415
458
  function isAifp1Challenge(body) {
416
459
  const b = body;
417
- return !!b && b.protocol === "AIFP-1" && typeof b.merchant_id === "string"
418
- && typeof b.resource === "string";
460
+ return !!b && b.protocol === "AIFP-1" && typeof b.merchant_id === "string" && typeof b.resource === "string";
419
461
  }
420
462
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
421
463
  /** Billing units the gateway says are left, or null when it did not say. */
@@ -434,16 +476,22 @@ function quotaRemaining(resp) {
434
476
  * already has.
435
477
  */
436
478
  export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
437
- const { site, restPath } = parseGatewayUrl(url, opts.gatewayOrigins ?? DEFAULT_GATEWAY_ORIGINS);
479
+ if (opts.maxAmountUsd !== undefined && (!Number.isFinite(opts.maxAmountUsd) || opts.maxAmountUsd <= 0)) {
480
+ throw new Aifp1QuoteError("maxAmountUsd must be a positive finite USD amount");
481
+ }
482
+ const direct = opts.resourcePathMode === "direct";
483
+ let { site, restPath } = parseGatewayUrl(url, opts.gatewayOrigins ?? DEFAULT_GATEWAY_ORIGINS, opts.resourcePathMode);
484
+ let directMerchant;
438
485
  // A paid flow sends the request twice, so the body has to survive being sent
439
486
  // twice. A stream cannot, and finding that out after paying would mean money
440
487
  // moved for a request that can no longer be made.
441
- if (init.body !== undefined && init.body !== null
442
- && typeof init.body === "object"
443
- && typeof init.body.getReader === "function") {
444
- throw new Aifp1Error("aifp1Fetch cannot use a stream body: a paywalled request is sent twice "
445
- + "(once to learn the price, once with the receipt) and a stream can only "
446
- + "be read once. Pass a string, Buffer, or URLSearchParams.");
488
+ if (init.body !== undefined &&
489
+ init.body !== null &&
490
+ typeof init.body === "object" &&
491
+ typeof init.body.getReader === "function") {
492
+ throw new Aifp1Error("aifp1Fetch cannot use a stream body: a paywalled request is sent twice " +
493
+ "(once to learn the price, once with the receipt) and a stream can only " +
494
+ "be read once. Pass a string, Buffer, or URLSearchParams.");
447
495
  }
448
496
  const send = (receiptJwt) => {
449
497
  // new Headers() rather than a spread: RequestInit.headers is legally a
@@ -451,6 +499,8 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
451
499
  // silently dropping every header the caller set, including their auth.
452
500
  const headers = new Headers(init.headers);
453
501
  headers.set("AIFP-Agent-Id", deps.agentId);
502
+ if (direct)
503
+ headers.delete("AIFP-Receipt");
454
504
  if (receiptJwt)
455
505
  headers.set("AIFP-Receipt", receiptJwt);
456
506
  return deps.fetchImpl(url, {
@@ -459,13 +509,32 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
459
509
  // The receipt is a bearer token. fetch follows redirects by default and
460
510
  // only strips authorization/cookie/host on a cross-origin hop — a custom
461
511
  // header rides along, so one 302 from a merchant upstream would hand the
462
- // batch to another host. Stop at the redirect and let the caller decide.
463
- ...(receiptJwt ? { redirect: "manual" } : {}),
512
+ // batch to another host. Direct probes also stop here: a redirect must
513
+ // not turn an untrusted origin's 402 into an authorized purchase.
514
+ ...(receiptJwt || direct ? { redirect: "manual" } : {}),
464
515
  });
465
516
  };
466
517
  // 1. Spend a batch we already own, if one covers this path.
467
- const held = deps.cache.find(site, restPath);
468
- const resp = await send(held?.jwt);
518
+ let held = direct ? undefined : deps.cache.find(site, restPath);
519
+ let resp = await send(held?.jwt);
520
+ if (direct && resp.status === 402) {
521
+ // A direct origin can serve several merchants. Learn this resource's
522
+ // merchant before attaching a bearer receipt, even for a cached batch.
523
+ const probe = await resp
524
+ .clone()
525
+ .json()
526
+ .catch(() => null);
527
+ if (isAifp1Challenge(probe)) {
528
+ if (probe.resource !== restPath) {
529
+ throw new Aifp1QuoteError("direct AIFP-1 challenge resource must match the full URL pathname");
530
+ }
531
+ directMerchant = probe.merchant_id;
532
+ site = `direct:${JSON.stringify([site, directMerchant])}`;
533
+ held = deps.cache.find(site, restPath);
534
+ if (held)
535
+ resp = await send(held.jwt);
536
+ }
537
+ }
469
538
  if (held) {
470
539
  // Absent header, not "zero left": the gateway sets AIFP-Quota-Remaining
471
540
  // only on a metered paid call, and `Number(null)` is 0 — reading it
@@ -489,12 +558,17 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
489
558
  // away a paid receipt and settles a second one on the next call.
490
559
  //
491
560
  // The gateway labels its own answers: {"error":"AIFP-403", ...}.
492
- const detail = await resp.clone().text().catch(() => "");
561
+ const detail = await resp
562
+ .clone()
563
+ .text()
564
+ .catch(() => "");
493
565
  let ours = false;
494
566
  try {
495
567
  ours = JSON.parse(detail)?.error === "AIFP-403";
496
568
  }
497
- catch { /* upstream body, not ours */ }
569
+ catch {
570
+ /* upstream body, not ours */
571
+ }
498
572
  if (!ours)
499
573
  return resp; // the merchant's 403, receipt intact
500
574
  deps.cache.evict(held);
@@ -504,9 +578,15 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
504
578
  }
505
579
  // 402 — but only AIFP-1 is ours. x402 and anything else passes through
506
580
  // untouched; call() owns those flows.
507
- const challenge = await resp.clone().json().catch(() => null);
581
+ const challenge = await resp
582
+ .clone()
583
+ .json()
584
+ .catch(() => null);
508
585
  if (!isAifp1Challenge(challenge))
509
586
  return resp;
587
+ if (direct && (challenge.merchant_id !== directMerchant || challenge.resource !== restPath)) {
588
+ throw new Aifp1QuoteError("direct AIFP-1 resource or merchant changed during receipt verification");
589
+ }
510
590
  // The batch we were spending is spent (or expired) — the gateway just said
511
591
  // so. Forget it before quoting the next one, or find() would keep handing
512
592
  // back a receipt that cannot pay for anything.
@@ -521,16 +601,27 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
521
601
  // spend it, and if it does not they buy their own, which is a different
522
602
  // batch rather than a duplicate.
523
603
  const buyBatch = async () => {
604
+ if (typeof deps.signPaymentAuthorization !== "function" || !/^0x[0-9a-fA-F]{40}$/.test(deps.payerAddress || "")) {
605
+ throw new Aifp1QuoteError("a settlement wallet signer is required to receive a payment receipt");
606
+ }
524
607
  // 2. Quote.
525
608
  const apiBase = (opts.apiBaseUrl ?? DEFAULT_API_BASE).replace(/\/$/, "");
526
609
  const { scope, resource } = resolveScope(opts, challenge);
527
610
  const quote = await requestQuote(deps, apiBase, {
528
611
  merchant_id: challenge.merchant_id,
612
+ payer: deps.payerAddress,
529
613
  ...(resource !== undefined ? { resource } : {}),
530
614
  scope,
531
615
  units: opts.units ?? defaultUnitsFor(challenge),
532
616
  agent_id: deps.agentId,
533
- });
617
+ }, opts.apiTimeoutMs);
618
+ if (quote.payer && quote.payer.toLowerCase() !== deps.payerAddress.toLowerCase()) {
619
+ throw new Aifp1QuoteError("quote names a different paying wallet");
620
+ }
621
+ if (quote.payment_authorization && quote.payment_authorization.scheme !== "wallet-signature-v1") {
622
+ throw new Aifp1QuoteError("unsupported receipt authorization scheme");
623
+ }
624
+ trustedPaymentIssuer(quote, opts.paymentIssuer);
534
625
  // The merchant we are about to pay must be the merchant that refused us.
535
626
  // Cheap, and the failure it catches is the one worth catching.
536
627
  if (quote.merchant_id !== challenge.merchant_id) {
@@ -543,10 +634,59 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
543
634
  }
544
635
  // 3. Budget. The quote states the batch total in USD, so both caps are
545
636
  // checked against the real figure, before anything is signed.
546
- const amountUsd = Number(quote.amount);
637
+ let amountUsd = Number(quote.amount);
547
638
  if (!Number.isFinite(amountUsd) || amountUsd < 0) {
548
639
  throw new Aifp1QuoteError(`quote ${quote.quote_id} has an unusable amount "${quote.amount}"`);
549
640
  }
641
+ if (opts.settlementPin) {
642
+ const price = opts.nativeUsdPrice;
643
+ const age = price ? Date.now() - price.observedAtMs : NaN;
644
+ if (!price ||
645
+ !Number.isFinite(price.usd) ||
646
+ price.usd <= 0 ||
647
+ !Number.isFinite(age) ||
648
+ age < -5_000 ||
649
+ age > 60_000) {
650
+ throw new Aifp1QuoteError("a fresh independent nativeUsdPrice is required before native payment; quote-provided FX is not trusted");
651
+ }
652
+ const native = quote.native_settlement;
653
+ if (!native || !/^[0-9]{1,78}$/.test(native.total_wei)) {
654
+ throw new Aifp1QuoteError("quote has no valid native debit to check against the independent price");
655
+ }
656
+ const debitUsd = (Number(BigInt(native.total_wei)) / 1e18) * price.usd;
657
+ if (!Number.isFinite(debitUsd) ||
658
+ debitUsd <= 0 ||
659
+ Math.abs(debitUsd - amountUsd) > Math.max(0.02 * amountUsd, 1e-6)) {
660
+ throw new Aifp1QuoteError("native debit disagrees with the independent USD price");
661
+ }
662
+ // Reserve and charge the larger amount, so rounding/FX tolerance cannot
663
+ // turn into a cumulative bypass of the user's daily USD budget.
664
+ amountUsd = Math.max(amountUsd, debitUsd);
665
+ const call = quote.settlement_call;
666
+ const args = call?.args;
667
+ if (native.asset !== "POL" || native.decimals !== 18 || !quote.accepted_assets.includes("POL")) {
668
+ throw new Aifp1QuoteError("native receipt settlement requires accepted POL with 18 decimals");
669
+ }
670
+ if (!call ||
671
+ call.chain !== "polygon" ||
672
+ call.splitter_version !== "1.3" ||
673
+ call.contract?.toLowerCase() !== opts.settlementPin.splitter.toLowerCase() ||
674
+ call.asset !== "POL" ||
675
+ call.function !== "payNative((bytes32,address,uint256,address,uint256,string))" ||
676
+ call.arg_encoding !== "tuple" ||
677
+ call.value_wei !== native.total_wei ||
678
+ args?.payment_id?.toLowerCase() !== keccak256(stringToHex(quote.quote_id)).toLowerCase() ||
679
+ args?.merchant?.toLowerCase() !== quote.pay_to.polygon?.toLowerCase() ||
680
+ args?.gross_amount !== native.total_wei ||
681
+ args?.order_id !== quote.quote_id ||
682
+ args?.valid_until !== Math.floor(quoteExpiryMs / 1000) ||
683
+ args?.ip_creator?.toLowerCase() !== "0x0000000000000000000000000000000000000000") {
684
+ throw new Aifp1QuoteError("quote settlement_call does not match the reviewed v1.3 payment; refusing a payment the receipt verifier cannot accept");
685
+ }
686
+ }
687
+ if (opts.maxAmountUsd !== undefined && amountUsd > opts.maxAmountUsd) {
688
+ throw new Aifp1QuoteError(`quote ${quote.quote_id} costs $${amountUsd}, above maxAmountUsd $${opts.maxAmountUsd}`);
689
+ }
550
690
  if (!deps.checkPerCall(amountUsd))
551
691
  return null;
552
692
  const reservation = await deps.reserveDaily(amountUsd);
@@ -557,13 +697,13 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
557
697
  // 4. Settle on-chain, then exchange the tx for a receipt.
558
698
  const native = quote.native_settlement;
559
699
  if (!native) {
560
- throw new Aifp1SettlementUnsupportedError(`quote ${quote.quote_id} carries no native_settlement — the backend had no live POL rate, `
561
- + `and the deployed B2BSplitter has no ERC-20 entrypoint, so this SDK cannot settle `
562
- + `${quote.accepted_assets.join("/")}. Retry when a POL rate is available or settle the quote yourself.`);
700
+ throw new Aifp1SettlementUnsupportedError(`quote ${quote.quote_id} carries no native_settlement — the backend had no live POL rate, ` +
701
+ `and the deployed B2BSplitter has no ERC-20 entrypoint, so this SDK cannot settle ` +
702
+ `${quote.accepted_assets.join("/")}. Retry when a POL rate is available or settle the quote yourself.`);
563
703
  }
564
704
  if (!quote.accepted_chains.includes("polygon") || !quote.pay_to.polygon) {
565
- throw new Aifp1SettlementUnsupportedError(`quote ${quote.quote_id} does not accept polygon (accepts ${quote.accepted_chains.join(", ")}) — `
566
- + `AIFP-1 settlement verification is Polygon-only server-side (aifp/verify-settlement.js)`);
705
+ throw new Aifp1SettlementUnsupportedError(`quote ${quote.quote_id} does not accept polygon (accepts ${quote.accepted_chains.join(", ")}) — ` +
706
+ `AIFP-1 settlement verification is Polygon-only server-side (aifp/verify-settlement.js)`);
567
707
  }
568
708
  // The caps above were checked in USD; the wallet is about to be debited in
569
709
  // wei. Nothing tied the two together, so a quote could pass a $0.10 cap and
@@ -576,60 +716,88 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
576
716
  // numbers must agree, and if they do not, the safe reading is not "trust
577
717
  // the USD" — it is that this quote is not what it says it is.
578
718
  const quotedRate = Number(native.rate_usd);
579
- const weiUsd = Number.isFinite(quotedRate) && quotedRate > 0
580
- ? (Number(BigInt(native.total_wei)) / 1e18) * quotedRate
581
- : NaN;
719
+ const weiUsd = Number.isFinite(quotedRate) && quotedRate > 0 ? (Number(BigInt(native.total_wei)) / 1e18) * quotedRate : NaN;
582
720
  if (!Number.isFinite(weiUsd)) {
583
- throw new Aifp1QuoteError(`quote ${quote.quote_id} states total_wei ${native.total_wei} at rate "${native.rate_usd}" — `
584
- + `unusable, and the caps were checked against $${amountUsd}`);
721
+ throw new Aifp1QuoteError(`quote ${quote.quote_id} states total_wei ${native.total_wei} at rate "${native.rate_usd}" — ` +
722
+ `unusable, and the caps were checked against $${amountUsd}`);
585
723
  }
586
724
  // 2% covers native conversion rounding and a
587
725
  // rate printed to six places; anything wider is a disagreement, not drift.
588
726
  if (Math.abs(weiUsd - amountUsd) > Math.max(0.02 * amountUsd, 1e-6)) {
589
- throw new Aifp1QuoteError(`quote ${quote.quote_id} would debit ${native.total_wei} wei ≈ $${weiUsd.toFixed(6)} `
590
- + `but states $${amountUsd} — refusing to sign a payment the budget caps did not see`);
727
+ throw new Aifp1QuoteError(`quote ${quote.quote_id} would debit ${native.total_wei} wei ≈ $${weiUsd.toFixed(6)} ` +
728
+ `but states $${amountUsd} — refusing to sign a payment the budget caps did not see`);
591
729
  }
592
730
  const nativeGross = BigInt(native.gross_wei ?? native.total_wei);
593
731
  const nativeMerchant = BigInt(native.merchant_wei);
594
732
  const nativeTreasury = BigInt(native.treasury_wei);
595
733
  const nativeCreator = BigInt(native.creator_wei);
596
- if (native.settlement_semantics !== "gross-inclusive"
597
- || BigInt(native.payer_total_wei ?? native.total_wei) !== nativeGross
598
- || BigInt(native.total_wei) !== nativeGross
599
- || nativeCreator !== 0n
600
- || nativeTreasury !== nativeGross / 100n
601
- || nativeMerchant !== nativeGross - nativeTreasury) {
734
+ if (native.settlement_semantics !== "gross-inclusive" ||
735
+ BigInt(native.payer_total_wei ?? native.total_wei) !== nativeGross ||
736
+ BigInt(native.total_wei) !== nativeGross ||
737
+ nativeCreator !== 0n ||
738
+ nativeTreasury !== nativeGross / 100n ||
739
+ nativeMerchant !== nativeGross - nativeTreasury) {
602
740
  throw new Aifp1QuoteError(`quote ${quote.quote_id} native settlement does not match canonical AIFP-1 gross split`);
603
741
  }
604
742
  const validUntil = BigInt(native.valid_until ?? Math.floor(quoteExpiryMs / 1000));
605
743
  if (validUntil !== BigInt(Math.floor(quoteExpiryMs / 1000))) {
606
744
  throw new Aifp1QuoteError(`quote ${quote.quote_id} valid_until does not match expires_at`);
607
745
  }
608
- const txRef = await deps.settle({
609
- merchantWallet: quote.pay_to.polygon,
610
- grossWei: nativeGross,
611
- merchantWei: nativeMerchant,
612
- treasuryWei: nativeTreasury,
613
- creatorWei: nativeCreator,
614
- validUntil,
615
- // The binding the server verifies on-chain: the Payment event's orderId
616
- // must equal the quote id, or /v1/pay answers order_id_mismatch.
617
- orderId: quote.quote_id,
618
- });
746
+ let txRef;
747
+ try {
748
+ txRef = await deps.settle({
749
+ merchantWallet: quote.pay_to.polygon,
750
+ grossWei: nativeGross,
751
+ merchantWei: nativeMerchant,
752
+ treasuryWei: nativeTreasury,
753
+ creatorWei: nativeCreator,
754
+ validUntil,
755
+ // The binding the server verifies on-chain: the Payment event's orderId
756
+ // must equal the quote id, or /v1/pay answers order_id_mismatch.
757
+ orderId: quote.quote_id,
758
+ });
759
+ }
760
+ catch (error) {
761
+ if (error instanceof SettlementConfirmationPendingError && error.stage === "settlement") {
762
+ // Treat unknown confirmation as spent until reconciliation. Releasing
763
+ // the reservation here would let a retry spend the same budget again.
764
+ settled = true;
765
+ const recovery = {
766
+ apiBaseUrl: apiBase,
767
+ quote,
768
+ txRef: error.txHash,
769
+ asset: native.asset,
770
+ paymentIssuer: opts.paymentIssuer,
771
+ };
772
+ try {
773
+ if (typeof reservation === "string")
774
+ await deps.commit(reservation, amountUsd);
775
+ }
776
+ catch {
777
+ throw new Aifp1PayError("payment broadcast; confirmation and budget reconciliation required", error.txHash, quote.quote_id, recovery);
778
+ }
779
+ throw new Aifp1PayError("payment broadcast; recover its confirmation and receipt without paying again", error.txHash, quote.quote_id, recovery);
780
+ }
781
+ throw error;
782
+ }
619
783
  // Money has moved. The reservation becomes settled spend here and not one
620
784
  // line later — everything below can still fail, and none of those failures
621
785
  // give the funds back.
622
786
  settled = true;
623
- if (typeof reservation === "string")
624
- await deps.commit(reservation, amountUsd);
625
- const paid = await submitPayment(deps, apiBase, quote, txRef, native.asset, opts);
626
- // 5. Retry the original request with the receipt.
627
- const receiptResp = await send(paid.receipt);
628
- if (receiptResp.status === 402) {
629
- const body = await receiptResp.clone().text().catch(() => "");
630
- throw new Aifp1ReceiptRejectedError(`gateway still answered 402 for ${restPath} with a freshly settled receipt `
631
- + `${paid.receipt_id} (tx ${txRef}): ${body.slice(0, 300)}`);
787
+ try {
788
+ if (typeof reservation === "string")
789
+ await deps.commit(reservation, amountUsd);
790
+ }
791
+ catch {
792
+ throw new Aifp1PayError("payment settled; budget reconciliation and receipt recovery required", txRef, quote.quote_id, {
793
+ apiBaseUrl: apiBase,
794
+ quote,
795
+ txRef,
796
+ asset: native.asset,
797
+ paymentIssuer: opts.paymentIssuer,
798
+ });
632
799
  }
800
+ const paid = await submitPayment(deps, apiBase, quote, txRef, native.asset, opts);
633
801
  // 6. Keep the batch. This is the whole point of the design: the next call
634
802
  // this receipt covers costs a header, not a transaction.
635
803
  const entry = {
@@ -644,13 +812,6 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
644
812
  expiresAt: Date.parse(paid.expires_at),
645
813
  amountUsd,
646
814
  };
647
- // The gateway's own count if it gave one; otherwise the batch minus the
648
- // weight of the route that refused us, which is the only figure we can
649
- // justify locally.
650
- const remaining = quotaRemaining(receiptResp);
651
- entry.remaining = remaining !== null
652
- ? remaining
653
- : Math.max(0, paid.unit_quota - challenge.unit_weight);
654
815
  // Cache unconditionally. This used to be guarded on a finite expiresAt,
655
816
  // which meant an unparseable expires_at silently binned a batch that had
656
817
  // just been settled on-chain — money spent, JWT existing nowhere else,
@@ -666,9 +827,35 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
666
827
  // and answers 402 the moment the receipt really is spent or stale.
667
828
  entry.expiresAt = jwtExpiryMs(paid.receipt) ?? Number.POSITIVE_INFINITY;
668
829
  }
830
+ // Store the purchased access BEFORE contacting the merchant again. A
831
+ // transport failure here must not discard a paid batch and buy it twice.
669
832
  deps.cache.put(entry);
833
+ let receiptResp;
834
+ try {
835
+ receiptResp = await send(paid.receipt);
836
+ }
837
+ catch {
838
+ throw new Aifp1PayError("payment settled and receipt cached; content request failed, retry without paying again", txRef, quote.quote_id, {
839
+ apiBaseUrl: apiBase,
840
+ quote,
841
+ txRef,
842
+ asset: native.asset,
843
+ paymentIssuer: opts.paymentIssuer,
844
+ });
845
+ }
846
+ if (receiptResp.status === 402) {
847
+ throw new Aifp1ReceiptRejectedError(`gateway still answered 402 for ${restPath} with settled receipt ${paid.receipt_id} (tx ${txRef}); recover the existing payment`);
848
+ }
849
+ // The gateway's own count if it gave one; otherwise the batch minus the
850
+ // weight of the route that refused us, which is the only figure we can
851
+ // justify locally.
852
+ const remaining = quotaRemaining(receiptResp);
853
+ entry.remaining = remaining !== null ? remaining : Math.max(0, paid.unit_quota - challenge.unit_weight);
670
854
  deps.onPaid?.({
671
- merchantId: paid.merchant_id, amountUsd, txRef, receiptId: paid.receipt_id,
855
+ merchantId: paid.merchant_id,
856
+ amountUsd,
857
+ txRef,
858
+ receiptId: paid.receipt_id,
672
859
  });
673
860
  return receiptResp;
674
861
  }
@@ -688,19 +875,19 @@ export async function aifp1Fetch(deps, url, init = {}, opts = {}) {
688
875
  return buyBatch();
689
876
  }
690
877
  // ── /v1/quote ─────────────────────────────────────────────────────────────
691
- async function requestQuote(deps, apiBase, body) {
878
+ async function requestQuote(deps, apiBase, body, timeoutMs) {
692
879
  let r;
880
+ let text;
693
881
  try {
694
- r = await deps.fetchImpl(`${apiBase}/v1/quote`, {
882
+ ({ response: r, text } = await settlementHttp(deps.fetchImpl, `${apiBase}/v1/quote`, {
695
883
  method: "POST",
696
884
  headers: { "content-type": "application/json", "AIFP-Agent-Id": deps.agentId },
697
885
  body: JSON.stringify(body),
698
- });
886
+ }, timeoutMs));
699
887
  }
700
888
  catch (e) {
701
889
  throw new Aifp1QuoteError(`POST ${apiBase}/v1/quote failed: ${e.message}`);
702
890
  }
703
- const text = await r.text();
704
891
  if (!r.ok) {
705
892
  // The server's own detail is the useful part — a scope refused for a
706
893
  // non-flat-rated tier, a batch under the minimum, an owner policy — and
@@ -734,11 +921,43 @@ async function submitPayment(deps, apiBase, quote, txRef, asset, opts) {
734
921
  const chain = "polygon";
735
922
  const idempotencyKey = idempotencyKeyFor({ quoteId: quote.quote_id, chain, asset, txRef });
736
923
  const deadline = Date.now() + (opts.settlementConfirmMs ?? DEFAULT_SETTLEMENT_CONFIRM_MS);
924
+ const recovery = {
925
+ apiBaseUrl: apiBase,
926
+ quote,
927
+ txRef,
928
+ asset,
929
+ paymentIssuer: opts.paymentIssuer,
930
+ };
931
+ const failure = (message) => new Aifp1PayError(message, txRef, quote.quote_id, recovery);
737
932
  let lastDetail = "";
738
933
  for (let attempt = 0;; attempt++) {
934
+ if (attempt > 0 && Date.now() >= deadline)
935
+ throw failure("payment confirmation deadline elapsed; recover the existing transaction");
936
+ const expiresAt = Math.floor(Date.now() / 1000) + 240;
937
+ const payer = deps.payerAddress.toLowerCase();
938
+ // Construct this locally; never sign an arbitrary server-provided message.
939
+ const message = paymentAuthorizationMessage({
940
+ quote,
941
+ apiBase,
942
+ chain,
943
+ txRef,
944
+ asset,
945
+ idempotencyKey,
946
+ payer,
947
+ expiresAt,
948
+ issuer: opts.paymentIssuer,
949
+ });
950
+ let signature;
951
+ try {
952
+ signature = await deps.signPaymentAuthorization(message);
953
+ }
954
+ catch (e) {
955
+ throw failure(`payment already settled; wallet could not sign receipt authorization: ${e.message}`);
956
+ }
739
957
  let r;
958
+ let text;
740
959
  try {
741
- r = await deps.fetchImpl(`${apiBase}/v1/pay`, {
960
+ ({ response: r, text } = await settlementHttp(deps.fetchImpl, `${apiBase}/v1/pay`, {
742
961
  method: "POST",
743
962
  headers: {
744
963
  "content-type": "application/json",
@@ -746,28 +965,65 @@ async function submitPayment(deps, apiBase, quote, txRef, asset, opts) {
746
965
  "AIFP-Agent-Id": deps.agentId,
747
966
  },
748
967
  body: JSON.stringify({
749
- quote_id: quote.quote_id, chain, asset, tx_ref: txRef, agent_id: deps.agentId,
968
+ quote_id: quote.quote_id,
969
+ chain,
970
+ asset,
971
+ tx_ref: txRef,
972
+ agent_id: deps.agentId,
973
+ payment_authorization: { payer, expires_at: expiresAt, signature },
750
974
  }),
751
- });
975
+ }, Math.max(1, Math.min(opts.apiTimeoutMs ?? 15_000, deadline - Date.now()))));
752
976
  }
753
977
  catch (e) {
754
- throw new Aifp1PayError(`POST ${apiBase}/v1/pay failed after settling: ${e.message}`, txRef, quote.quote_id);
978
+ if (Date.now() >= deadline || (e instanceof SettlementHttpError && e.code !== "request_timeout")) {
979
+ throw failure(`POST ${apiBase}/v1/pay failed after settling: ${e.message}`);
980
+ }
981
+ await sleep(Math.min(1000 * 2 ** attempt, 8000, Math.max(0, deadline - Date.now())));
982
+ continue;
755
983
  }
756
- const text = await r.text();
757
984
  if (r.ok) {
758
- const paid = JSON.parse(text);
985
+ let paid;
986
+ try {
987
+ paid = JSON.parse(text);
988
+ }
989
+ catch {
990
+ throw failure(`/v1/pay returned invalid JSON after settlement`);
991
+ }
759
992
  if (!paid.receipt) {
760
- throw new Aifp1PayError(`/v1/pay → 200 without a receipt: ${text.slice(0, 300)}`, txRef, quote.quote_id);
993
+ throw failure(`/v1/pay → 200 without a receipt: ${text.slice(0, 300)}`);
761
994
  }
762
995
  return paid;
763
996
  }
764
997
  lastDetail = text.slice(0, 400);
765
- // 425 is the only status worth retrying: the chain has not caught up with
766
- // us yet. Everything else is a verdict.
767
- if (r.status !== 425 || Date.now() >= deadline) {
768
- throw new Aifp1PayError(`POST /v1/pay → ${r.status} after on-chain settlement ${txRef} for quote ${quote.quote_id}: ${lastDetail}`, txRef, quote.quote_id);
998
+ // A 503 after settlement may mean accounting is unavailable, or that a
999
+ // committed response was lost. Retry issuance, never the transfer.
1000
+ if (![425, 503].includes(r.status) || Date.now() >= deadline) {
1001
+ throw failure(`POST /v1/pay → ${r.status} after on-chain settlement ${txRef} for quote ${quote.quote_id}: ${lastDetail}`);
769
1002
  }
770
- await sleep(Math.min(1000 * 2 ** attempt, 8000));
1003
+ await sleep(Math.min(1000 * 2 ** attempt, 8000, Math.max(0, deadline - Date.now())));
1004
+ }
1005
+ }
1006
+ /** Fixed, domain-separated receipt claim; sign with the wallet that settled. */
1007
+ export function paymentAuthorizationMessage(p) {
1008
+ return JSON.stringify([
1009
+ "AiFinPay receipt authorization v1",
1010
+ trustedPaymentIssuer(p.quote, p.issuer),
1011
+ p.quote.quote_id,
1012
+ p.quote.nonce,
1013
+ p.quote.merchant_id,
1014
+ p.quote.network_mode || "live",
1015
+ p.chain,
1016
+ p.txRef,
1017
+ p.asset || "",
1018
+ p.idempotencyKey,
1019
+ p.payer,
1020
+ p.expiresAt,
1021
+ ]);
1022
+ }
1023
+ function trustedPaymentIssuer(quote, configured = "https://api.aifinpay.io") {
1024
+ if (quote.payment_authorization?.domain && quote.payment_authorization.domain !== configured) {
1025
+ throw new Aifp1QuoteError("receipt authorization domain does not match the trusted paymentIssuer");
771
1026
  }
1027
+ return configured;
772
1028
  }
773
1029
  //# sourceMappingURL=aifp1.js.map