@haven_ai/sdk 0.8.1-alpha.0 → 0.10.0-alpha.0

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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/base64-CiR5BJhk.d.cts +1352 -0
  3. package/dist/base64-CxkinHPQ.d.ts +1352 -0
  4. package/dist/{chunk-HFOAYTD4.cjs → chunk-22FJ4JQO.cjs} +47 -35
  5. package/dist/chunk-22FJ4JQO.cjs.map +1 -0
  6. package/dist/chunk-6LEXYQ7J.cjs +106 -0
  7. package/dist/chunk-6LEXYQ7J.cjs.map +1 -0
  8. package/dist/chunk-EWTHDEIW.js +103 -0
  9. package/dist/chunk-EWTHDEIW.js.map +1 -0
  10. package/dist/{chunk-HX34HOW4.cjs → chunk-H37YVYZA.cjs} +6 -4
  11. package/dist/chunk-H37YVYZA.cjs.map +1 -0
  12. package/dist/{chunk-WDA3IGJP.js → chunk-MLUWABVT.js} +15 -6
  13. package/dist/chunk-MLUWABVT.js.map +1 -0
  14. package/dist/{chunk-H4APV67M.js → chunk-TD2PHUYP.js} +6 -4
  15. package/dist/chunk-TD2PHUYP.js.map +1 -0
  16. package/dist/edge.cjs +105 -89
  17. package/dist/edge.d.cts +4 -2
  18. package/dist/edge.d.ts +4 -2
  19. package/dist/edge.js +4 -4
  20. package/dist/index.cjs +1138 -500
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +128 -144
  23. package/dist/index.d.ts +128 -144
  24. package/dist/index.js +801 -199
  25. package/dist/index.js.map +1 -1
  26. package/dist/receipt-Cg9a5Seq.d.ts +130 -0
  27. package/dist/receipt-q7ez8Zb2.d.cts +130 -0
  28. package/dist/test-support.cjs +71 -18
  29. package/dist/test-support.cjs.map +1 -1
  30. package/dist/test-support.d.cts +21 -1
  31. package/dist/test-support.d.ts +21 -1
  32. package/dist/test-support.js +53 -3
  33. package/dist/test-support.js.map +1 -1
  34. package/dist/{base64-BjxHPgtC.d.cts → types-BKgqzvnk.d.cts} +238 -1307
  35. package/dist/{base64-BjxHPgtC.d.ts → types-BKgqzvnk.d.ts} +238 -1307
  36. package/package.json +1 -1
  37. package/dist/chunk-H4APV67M.js.map +0 -1
  38. package/dist/chunk-HFOAYTD4.cjs.map +0 -1
  39. package/dist/chunk-HX34HOW4.cjs.map +0 -1
  40. package/dist/chunk-WDA3IGJP.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
- import { selectStandardPaymentOption, buildX402IdempotencyKey, x402AuthorizationAmount, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, assertOwnSettlementChild, parsePaymentRequiredResponse, x402PaymentHeaderNamesSent, SDK_CLIENT_IDENTITY, readClientUpdate, HAVEN_CLIENT_HEADER, isSweepableChain, sweepUsdcAddress, x402PaymentHeaderNamesFor, X402_PAYMENT_HEADER_NAMES, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, isErc7710Option, buildSignedTaxDeclaration, encodeTaxDeclarationHeader, selectX402SettlementScheme, selectErc7710PaymentOption, x402AssetTransferMethod, resolveTokenFromAddress, normalizePaymentRequired, X_TAX_DECLARATION_HEADER } from './chunk-WDA3IGJP.js';
2
- export { DEFAULT_NEXT_TOOL_BY_ACTION, ERC7710_ASSET_TRANSFER_METHOD, HAVEN_CLIENT_HEADER, HAVEN_MINIMUM_NODE_VERSION, HavenTypedDataRefusedError, MAX_TASK_BUDGET_TTL_SECONDS, NEXT_TOOL_SERVER_NAMES, NEXT_TOOL_SERVER_ROLES, SDK_CLIENT_IDENTITY, SDK_VERSION, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, TYPED_DATA_NOT_ALLOWED, X402PaymentHeaderValidationError, X402_LEGACY_PAYMENT_HEADER_NAME, X402_MAX_AUTHORIZATION_WINDOW_SECONDS, X402_PAYMENT_HEADER_NAME, X402_PAYMENT_HEADER_NAMES_SENT, X402_PAYMENT_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, hashDelegation, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isTaskChildTypedData, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenBySymbol, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402V2PaymentEnvelope } from './chunk-WDA3IGJP.js';
3
- import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, X402AlreadySettledError, AgentPaymentWarningCode, MerchantTimeoutError, AgentPaymentNextAction, HavenPaymentStateError, HavenZeroSettlementHashError, X402Erc7710AlreadySettledError, DELEGATION_MANAGER, chainIdForNetwork } from './chunk-H4APV67M.js';
4
- export { AGENT_PAYMENT_FAILURE_CODE_VALUES, AGENT_PAYMENT_NEXT_ACTION_VALUES, AGENT_PAYMENT_PHASE_VALUES, AGENT_PAYMENT_RAIL_VALUES, AgentPaymentFailureCode, AgentPaymentFailureCodeDescriptions, AgentPaymentFailureCodeSchema, AgentPaymentNextAction, AgentPaymentNextActionDescriptions, AgentPaymentNextActionSchema, AgentPaymentPhase, AgentPaymentPhaseDescriptions, AgentPaymentPhaseSchema, AgentPaymentRail, AgentPaymentRailDescriptions, AgentPaymentRailSchema, AgentPaymentWarningCode, CONNECTOR_PACKAGE_NAME, DEFAULT_CONFIRMATION_TIMEOUT_MS, DIRECT_SIGN_CONTEXT_VERSION, ENTRY_POINT_V07, HAVEN_CONNECTOR_CHANNEL, HYBRID_DELEGATOR_DOMAIN_NAME, HYBRID_DELEGATOR_DOMAIN_VERSION, HavenApiError, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, HavenUnsupportedSignerVersionError, HavenUserOpBindingError, HavenZeroSettlementHashError, MerchantTimeoutError, PACKED_USER_OPERATION_FIELDS, ROOT_AUTHORITY, SIGNER_UPDATE_FALLBACK, SignerRefusalCode, TASK_SIGN_CONTEXT_VERSION, X402AlreadySettledError, X402Erc7710AlreadySettledError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, assertUserOpTypedDataBinding, connectorRerunCommand, connectorSpec, connectorUpgradeCommand, deriveDelegateAccountAddress, isConnectorChannel, isPackedUserOperationTypedData, packedUserOperationHash, resolveConnectorChannel, signerUpdateFallback, verifySettlementChild } from './chunk-H4APV67M.js';
5
- import { keccak256, toBytes, concatHex, hashTypedData, encodeAbiParameters } from 'viem';
1
+ import { decodeBase64Json, selectStandardPaymentOption, buildX402IdempotencyKey, x402AuthorizationAmount, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, assertOwnSettlementChild, parsePaymentRequiredResponse, x402PaymentHeaderNamesSent, SDK_CLIENT_IDENTITY, readClientUpdate, HAVEN_CLIENT_HEADER, isSweepableChain, sweepUsdcAddress, x402PaymentHeaderNamesFor, X402_PAYMENT_HEADER_NAMES, toStandardPaymentRequirements, deriveX402PaymentNonce, encodeBase64Json, x402V2PaymentEnvelope, isErc7710Option, buildSignedTaxDeclaration, encodeTaxDeclarationHeader, selectX402SettlementScheme, selectErc7710PaymentOption, x402AssetTransferMethod, resolveTokenFromAddress, normalizePaymentRequired, X_TAX_DECLARATION_HEADER } from './chunk-MLUWABVT.js';
2
+ export { DEFAULT_NEXT_TOOL_BY_ACTION, ERC7710_ASSET_TRANSFER_METHOD, HAVEN_CLIENT_HEADER, HAVEN_MINIMUM_NODE_VERSION, HavenTypedDataRefusedError, MAX_TASK_BUDGET_TTL_SECONDS, NEXT_TOOL_SERVER_NAMES, NEXT_TOOL_SERVER_ROLES, SDK_CLIENT_IDENTITY, SDK_VERSION, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, TYPED_DATA_NOT_ALLOWED, X402PaymentHeaderValidationError, X402_LEGACY_PAYMENT_HEADER_NAME, X402_MAX_AUTHORIZATION_WINDOW_SECONDS, X402_PAYMENT_HEADER_NAME, X402_PAYMENT_HEADER_NAMES_SENT, X402_PAYMENT_NONCE_TAG, X402_PAYMENT_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, deriveX402PaymentNonce, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, hashDelegation, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isTaskChildTypedData, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenBySymbol, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402PaymentHeaderNamesFor, x402RetryHeadersFor, x402V2PaymentEnvelope } from './chunk-MLUWABVT.js';
3
+ import { verifyPaymentReceipt } from './chunk-EWTHDEIW.js';
4
+ export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-EWTHDEIW.js';
5
+ import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, X402AlreadySettledError, MerchantTimeoutError, AgentPaymentWarningCode, AgentPaymentNextAction, HavenPaymentStateError, HavenZeroSettlementHashError, X402Erc7710AlreadySettledError, chainIdForNetwork } from './chunk-TD2PHUYP.js';
6
+ export { AGENT_PAYMENT_FAILURE_CODE_VALUES, AGENT_PAYMENT_NEXT_ACTION_VALUES, AGENT_PAYMENT_PHASE_VALUES, AGENT_PAYMENT_RAIL_VALUES, AgentPaymentFailureCode, AgentPaymentFailureCodeDescriptions, AgentPaymentFailureCodeSchema, AgentPaymentNextAction, AgentPaymentNextActionDescriptions, AgentPaymentNextActionSchema, AgentPaymentPhase, AgentPaymentPhaseDescriptions, AgentPaymentPhaseSchema, AgentPaymentRail, AgentPaymentRailDescriptions, AgentPaymentRailSchema, AgentPaymentWarningCode, CONNECTOR_PACKAGE_NAME, DEFAULT_CONFIRMATION_TIMEOUT_MS, DIRECT_SIGN_CONTEXT_VERSION, ENTRY_POINT_V07, HAVEN_CONNECTOR_CHANNEL, HYBRID_DELEGATOR_DOMAIN_NAME, HYBRID_DELEGATOR_DOMAIN_VERSION, HavenApiError, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, HavenUnsupportedSignerVersionError, HavenUserOpBindingError, HavenZeroSettlementHashError, MerchantTimeoutError, PACKED_USER_OPERATION_FIELDS, ROOT_AUTHORITY, SIGNER_UPDATE_FALLBACK, SignerRefusalCode, TASK_SIGN_CONTEXT_VERSION, X402AlreadySettledError, X402Erc7710AlreadySettledError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, assertUserOpTypedDataBinding, connectorRerunCommand, connectorSpec, connectorUpgradeCommand, deriveDelegateAccountAddress, isConnectorChannel, isPackedUserOperationTypedData, packedUserOperationHash, resolveConnectorChannel, signerUpdateFallback, verifySettlementChild } from './chunk-TD2PHUYP.js';
7
+ import { hashTypedData } from 'viem';
6
8
  import { ethers } from 'ethers';
7
9
  import { AsyncLocalStorage } from 'async_hooks';
8
10
  import { exact } from 'x402/schemes';
@@ -534,6 +536,122 @@ function assertSecureX402RetryTarget(url) {
534
536
  if (!isSecureX402RetryTarget(url)) throw new HavenInsecureRetryTargetError(url);
535
537
  }
536
538
 
539
+ // src/merchant-egress.ts
540
+ var MERCHANT_EGRESS_REFUSED_CODE = "MERCHANT_EGRESS_REFUSED";
541
+ var MerchantEgressRefusedError = class extends HavenError {
542
+ url;
543
+ refusal;
544
+ beforeRequest;
545
+ constructor(message, url, refusal, beforeRequest) {
546
+ super(message, MERCHANT_EGRESS_REFUSED_CODE, 400);
547
+ this.name = "MerchantEgressRefusedError";
548
+ this.url = url;
549
+ this.refusal = refusal;
550
+ this.beforeRequest = beforeRequest;
551
+ }
552
+ };
553
+ var MERCHANT_EGRESS_RESPONSE_CAP_CODE = "MERCHANT_EGRESS_RESPONSE_CAP";
554
+ var MerchantEgressResponseCapError = class extends HavenError {
555
+ url;
556
+ limitBytes;
557
+ constructor(message, limitBytes, url) {
558
+ super(message, MERCHANT_EGRESS_RESPONSE_CAP_CODE, 400);
559
+ this.name = "MerchantEgressResponseCapError";
560
+ this.limitBytes = limitBytes;
561
+ this.url = url;
562
+ }
563
+ };
564
+ var INTERNAL_SUFFIXES = [".internal", ".local", ".localhost", ".test", ".invalid", ".example"];
565
+ function isPublicHttpsMerchantUrl(url) {
566
+ let parsed;
567
+ try {
568
+ parsed = new URL(url);
569
+ } catch {
570
+ return false;
571
+ }
572
+ if (parsed.protocol !== "https:") return false;
573
+ const host = parsed.hostname.toLowerCase().replace(/\.$/, "");
574
+ if (host.includes(":")) return false;
575
+ if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return false;
576
+ if (!host.includes(".")) return false;
577
+ return !INTERNAL_SUFFIXES.some((suffix) => host.endsWith(suffix));
578
+ }
579
+ function publicHttpsMerchantUrlRefusal(url) {
580
+ let parsed;
581
+ try {
582
+ parsed = new URL(url);
583
+ } catch {
584
+ return "it is not a valid URL";
585
+ }
586
+ if (parsed.protocol !== "https:") return `its scheme is ${parsed.protocol.replace(":", "")}, not https`;
587
+ const host = parsed.hostname.toLowerCase().replace(/\.$/, "");
588
+ if (host.includes(":")) return `${host} is an IP-literal host (IPv6)`;
589
+ if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return `${host} is an IP-literal host (IPv4)`;
590
+ if (!host.includes(".")) return `${host} is a single-label host`;
591
+ const suffix = INTERNAL_SUFFIXES.find((s) => host.endsWith(s));
592
+ if (suffix) return `${host} ends in the internal/reserved suffix ${suffix}`;
593
+ return "it is not a public https URL";
594
+ }
595
+ function assertPublicHttpsMerchantUrl(url) {
596
+ if (isPublicHttpsMerchantUrl(url)) return;
597
+ throw new MerchantEgressRefusedError(
598
+ `Hosted egress policy refuses ${url}: ${publicHttpsMerchantUrlRefusal(url)}. Hosted merchant requests go to public https hosts only \u2014 no IP literals, no localhost, no single-label or internal names. Re-quote the merchant at its public https URL.`,
599
+ url,
600
+ "url_not_allowed",
601
+ true
602
+ );
603
+ }
604
+ var HOSTED_DISCOVERY_TIMEOUT_MS = 5e3;
605
+ var HOSTED_EGRESS_TIMEOUTS = {
606
+ quote: 15e3,
607
+ mcpSession: 15e3,
608
+ discovery: HOSTED_DISCOVERY_TIMEOUT_MS,
609
+ // Today's DEFAULT_MERCHANT_TIMEOUT — stated here rather than imported to
610
+ // keep this module dependency-free in both directions.
611
+ delivery: 3e5
612
+ };
613
+ var HOSTED_RESPONSE_BYTE_CAPS = {
614
+ quote: 256 * 1024,
615
+ mcpSession: 256 * 1024,
616
+ discovery: 64 * 1024,
617
+ delivery: 5 * 1024 * 1024
618
+ };
619
+ var HOSTED_MAX_GET_REDIRECTS = 3;
620
+ function strictMerchantEgressPolicy() {
621
+ return {
622
+ assertUrl: assertPublicHttpsMerchantUrl,
623
+ maxResponseBytes: HOSTED_RESPONSE_BYTE_CAPS.delivery,
624
+ responseByteCaps: { ...HOSTED_RESPONSE_BYTE_CAPS },
625
+ maxGetRedirects: HOSTED_MAX_GET_REDIRECTS,
626
+ timeouts: { ...HOSTED_EGRESS_TIMEOUTS }
627
+ };
628
+ }
629
+ function isBodyPhaseTimeout(err) {
630
+ return err instanceof DOMException && err.name === "TimeoutError";
631
+ }
632
+ async function readBodyCapped(response, maxBytes) {
633
+ if (!response.body) return response.text();
634
+ const reader = response.body.getReader();
635
+ const decoder = new TextDecoder();
636
+ let text = "";
637
+ let total = 0;
638
+ while (true) {
639
+ const { done, value } = await reader.read();
640
+ if (done) break;
641
+ total += value.byteLength;
642
+ if (total > maxBytes) {
643
+ await reader.cancel().catch(() => {
644
+ });
645
+ throw new MerchantEgressResponseCapError(
646
+ `Merchant response exceeded the ${maxBytes}-byte cap while it was being read; the read was aborted.`,
647
+ maxBytes
648
+ );
649
+ }
650
+ text += decoder.decode(value, { stream: true });
651
+ }
652
+ return text + decoder.decode();
653
+ }
654
+
537
655
  // src/mcp-merchant-transport.ts
538
656
  var DEFAULT_MERCHANT_TIMEOUT = 3e5;
539
657
  var MCP_NOTIFICATION_TIMEOUT = 1e4;
@@ -547,24 +665,161 @@ var SESSION_NOT_FOUND_NEXT_ACTION = "reinitialize_then_retry_same_payment_header
547
665
  var McpMerchantTransport = class {
548
666
  merchantTimeout;
549
667
  fetchImpl;
668
+ egress;
550
669
  requestId = 0;
551
670
  constructor(options = {}) {
552
671
  this.merchantTimeout = options.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
672
+ this.egress = options.egress;
553
673
  this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
554
674
  }
675
+ /**
676
+ * #3747: the timeout a merchant use runs under. Without an egress policy
677
+ * this is always `merchantTimeout` — byte-identical to the pre-policy
678
+ * behaviour; with one, the effective deadline is
679
+ * `min(policy timeout for the use, merchantTimeout)` (spec review
680
+ * 2026-10-07) — a policy can only TIGHTEN `merchantTimeout`, never extend
681
+ * it.
682
+ */
683
+ budgetFor(use) {
684
+ const policyTimeout = this.egress?.timeouts?.[use];
685
+ return policyTimeout === void 0 ? this.merchantTimeout : Math.min(policyTimeout, this.merchantTimeout);
686
+ }
687
+ /** The per-use response byte cap (falls back to the policy's default). */
688
+ capFor(use) {
689
+ const egress = this.egress;
690
+ if (!egress) return void 0;
691
+ return egress.responseByteCaps?.[use] ?? egress.maxResponseBytes;
692
+ }
555
693
  /** Fetch a merchant with a settlement-sized timeout and caller cancellation. */
556
- async fetch(url, init = {}, timeoutMs = this.merchantTimeout) {
694
+ async fetch(url, init = {}, timeoutMs = this.merchantTimeout, maxBytes) {
695
+ if (!this.egress) {
696
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
697
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
698
+ try {
699
+ return await this.fetchImpl(url, { ...init, signal });
700
+ } catch (err) {
701
+ if (timeoutSignal.aborted) {
702
+ throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
703
+ }
704
+ throw err;
705
+ }
706
+ }
707
+ return this.fetchUnderPolicy(url, init, timeoutMs, maxBytes);
708
+ }
709
+ /**
710
+ * #3747: the policy path. Every URL — the initial request and each redirect
711
+ * hop — is `assertUrl`-checked BEFORE it connects; redirects run `manual`
712
+ * so nothing is ever followed un-checked; GET redirects are followed up to
713
+ * `maxGetRedirects` (a redirect on any other method is refused); response
714
+ * bodies are capped WHILE being read, not after buffering. The timeout
715
+ * signal spans the WHOLE redirect chain, and `MerchantTimeoutError`
716
+ * classification is preserved.
717
+ */
718
+ async fetchUnderPolicy(url, init, timeoutMs, maxBytes) {
719
+ const egress = this.egress;
720
+ try {
721
+ egress.assertUrl(url);
722
+ } catch (err) {
723
+ throw asRefusal(err, url, true);
724
+ }
725
+ const method = (init.method ?? "GET").toUpperCase();
557
726
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
558
727
  const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
728
+ const maxRedirects = egress.maxGetRedirects ?? 0;
729
+ let currentUrl = url;
730
+ let hops = 0;
559
731
  try {
560
- return await this.fetchImpl(url, { ...init, signal });
732
+ while (true) {
733
+ const response = await this.fetchImpl(currentUrl, {
734
+ ...init,
735
+ redirect: init.redirect === "error" ? "error" : "manual",
736
+ signal
737
+ });
738
+ if (!isRedirectStatus(response.status)) return this.capResponse(response, currentUrl, maxBytes);
739
+ if (method !== "GET") {
740
+ throw new MerchantEgressRefusedError(
741
+ `Hosted egress policy refuses the ${response.status} redirect from ${currentUrl}: redirects are followed for GET requests only, and this was a ${method}. The request itself was already sent to the checked URL; nothing was sent to the redirect target.`,
742
+ currentUrl,
743
+ "redirect_on_non_get",
744
+ false
745
+ );
746
+ }
747
+ const location = response.headers.get("location");
748
+ if (!location) {
749
+ throw new MerchantEgressRefusedError(
750
+ `Hosted egress policy refuses the ${response.status} redirect from ${currentUrl}: it carries no Location header, so there is no checked target to follow.`,
751
+ currentUrl,
752
+ "redirect_missing_location",
753
+ false
754
+ );
755
+ }
756
+ let next;
757
+ try {
758
+ next = new URL(location, currentUrl).toString();
759
+ } catch {
760
+ throw new MerchantEgressRefusedError(
761
+ `Hosted egress policy refuses the redirect from ${currentUrl}: its Location header is not a resolvable URL.`,
762
+ currentUrl,
763
+ "redirect_invalid_location",
764
+ false
765
+ );
766
+ }
767
+ if (hops >= maxRedirects) {
768
+ throw new MerchantEgressRefusedError(
769
+ `Hosted egress policy refuses the redirect from ${currentUrl} to ${next}: the ${maxRedirects}-redirect budget for this request is spent.`,
770
+ next,
771
+ "redirect_budget_exceeded",
772
+ false
773
+ );
774
+ }
775
+ try {
776
+ egress.assertUrl(next);
777
+ } catch (err) {
778
+ throw asRefusal(err, next, false);
779
+ }
780
+ hops++;
781
+ currentUrl = next;
782
+ }
561
783
  } catch (err) {
562
784
  if (timeoutSignal.aborted) {
563
- throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
785
+ throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${currentUrl}`);
564
786
  }
565
787
  throw err;
566
788
  }
567
789
  }
790
+ /**
791
+ * #3747: enforce `maxResponseBytes` at the STREAM, so every later read —
792
+ * `.text()`, `.json()`, `.clone().text()` — rejects mid-read the moment the
793
+ * cap is crossed. A body that never ends can never buffer past the cap.
794
+ */
795
+ capResponse(response, url, maxBytes) {
796
+ const cap = maxBytes ?? this.egress.maxResponseBytes;
797
+ if (!cap || !response.body) return response;
798
+ let total = 0;
799
+ const capped = response.body.pipeThrough(
800
+ new TransformStream({
801
+ transform(chunk, controller) {
802
+ total += chunk.byteLength;
803
+ if (total > cap) {
804
+ controller.error(
805
+ new MerchantEgressResponseCapError(
806
+ `Merchant response from ${url} exceeded the ${cap}-byte cap while it was being read; the read was aborted.`,
807
+ cap,
808
+ url
809
+ )
810
+ );
811
+ return;
812
+ }
813
+ controller.enqueue(chunk);
814
+ }
815
+ })
816
+ );
817
+ return new Response(capped, {
818
+ status: response.status,
819
+ statusText: response.statusText,
820
+ headers: response.headers
821
+ });
822
+ }
568
823
  /**
569
824
  * Establish an MCP Streamable-HTTP session. Any handshake failure degrades
570
825
  * to `undefined`, allowing the caller to fall back to plain x402.
@@ -588,7 +843,7 @@ var McpMerchantTransport = class {
588
843
  clientInfo: MCP_CLIENT_INFO
589
844
  }
590
845
  })
591
- });
846
+ }, this.budgetFor("mcpSession"), this.capFor("mcpSession"));
592
847
  if (!response.ok) return void 0;
593
848
  const sessionId = response.headers.get("mcp-session-id");
594
849
  if (!sessionId) return void 0;
@@ -596,7 +851,8 @@ var McpMerchantTransport = class {
596
851
  if (message && "error" in message) return void 0;
597
852
  await this.notifyInitialized(url, init, sessionId, wallet);
598
853
  return sessionId;
599
- } catch {
854
+ } catch (err) {
855
+ if (err instanceof MerchantEgressRefusedError || err instanceof MerchantEgressResponseCapError) throw err;
600
856
  return void 0;
601
857
  }
602
858
  }
@@ -612,7 +868,9 @@ var McpMerchantTransport = class {
612
868
  let text;
613
869
  try {
614
870
  text = await response.clone().text();
615
- } catch {
871
+ } catch (err) {
872
+ if (err instanceof MerchantEgressResponseCapError) throw err;
873
+ if (isBodyPhaseTimeout(err)) throw new MerchantTimeoutError(`Merchant response body read timed out.`);
616
874
  return void 0;
617
875
  }
618
876
  if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
@@ -635,7 +893,9 @@ var McpMerchantTransport = class {
635
893
  let text;
636
894
  try {
637
895
  text = await response.clone().text();
638
- } catch {
896
+ } catch (err) {
897
+ if (err instanceof MerchantEgressResponseCapError) throw err;
898
+ if (isBodyPhaseTimeout(err)) throw new MerchantTimeoutError(`Merchant response body read timed out.`);
639
899
  return response;
640
900
  }
641
901
  const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
@@ -702,7 +962,7 @@ var McpMerchantTransport = class {
702
962
  if (send.includes(name)) headers.set(name, paymentHeader);
703
963
  else headers.delete(name);
704
964
  }
705
- return this.fetch(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader));
965
+ return this.fetch(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader), this.budgetFor("delivery"), this.capFor("delivery"));
706
966
  }
707
967
  /**
708
968
  * #3171: deliver the payment; if the merchant answers its unknown-session
@@ -874,8 +1134,19 @@ function sessionNotFoundRecovery(body) {
874
1134
  function isRecord(value) {
875
1135
  return typeof value === "object" && value !== null && !Array.isArray(value);
876
1136
  }
1137
+ function isRedirectStatus(status) {
1138
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
1139
+ }
1140
+ function asRefusal(err, url, beforeRequest) {
1141
+ if (!(err instanceof MerchantEgressRefusedError)) return err;
1142
+ if (err.beforeRequest === beforeRequest) return err;
1143
+ return new MerchantEgressRefusedError(err.message, url, err.refusal, beforeRequest);
1144
+ }
877
1145
  async function captureMerchantResponse(response) {
878
- const merchant_body = await response.text().catch(() => "");
1146
+ const merchant_body = await response.text().catch((err) => {
1147
+ if (err instanceof MerchantEgressResponseCapError) throw err;
1148
+ return "";
1149
+ });
879
1150
  return {
880
1151
  merchant_status: response.status,
881
1152
  merchant_status_text: response.statusText,
@@ -894,7 +1165,8 @@ async function responseHasBazaarExtension(response) {
894
1165
  try {
895
1166
  const body = await response.clone().json();
896
1167
  return body?.extensions?.bazaar != null;
897
- } catch {
1168
+ } catch (err) {
1169
+ if (err instanceof MerchantEgressResponseCapError) throw err;
898
1170
  return false;
899
1171
  }
900
1172
  }
@@ -922,95 +1194,6 @@ function parseSseJsonRpcMessages(text) {
922
1194
  function selectJsonRpcResult(messages) {
923
1195
  return messages.find((message) => "result" in message || "error" in message) ?? messages.at(-1);
924
1196
  }
925
- var RECEIPT_VERSION = "haven-receipt-1";
926
- var DELEGATION_MANAGER_CHAIN_IDS = /* @__PURE__ */ new Set([8453, 84532]);
927
- var EIP712_DOMAIN_TYPEHASH = keccak256(
928
- toBytes("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
929
- );
930
- function delegationDomainSeparator(chainId) {
931
- return keccak256(
932
- encodeAbiParameters(
933
- [{ type: "bytes32" }, { type: "bytes32" }, { type: "bytes32" }, { type: "uint256" }, { type: "address" }],
934
- [
935
- EIP712_DOMAIN_TYPEHASH,
936
- keccak256(toBytes("DelegationManager")),
937
- keccak256(toBytes("1")),
938
- BigInt(chainId),
939
- DELEGATION_MANAGER
940
- ]
941
- )
942
- );
943
- }
944
- function delegationDigest(structHash, chainId) {
945
- return keccak256(concatHex(["0x1901", delegationDomainSeparator(chainId), structHash]));
946
- }
947
- function defaultRecover(hash, signature) {
948
- return ethers.recoverAddress(hash, signature);
949
- }
950
- var NOT_A_SIGNED_RECEIPT = { verified: false, reason: "not_a_signed_receipt" };
951
- function match(delegate, hash, signature, recover) {
952
- let recovered;
953
- try {
954
- recovered = recover(hash, signature);
955
- } catch {
956
- return { ok: false, reason: "bad_signature" };
957
- }
958
- if (recovered.toLowerCase() !== delegate.toLowerCase()) {
959
- return { ok: false, reason: "signer_mismatch", recoveredSigner: recovered };
960
- }
961
- return { ok: true, recoveredSigner: recovered };
962
- }
963
- function finish(result, verifiedOver) {
964
- if (result.ok) {
965
- return { verified: true, recoveredSigner: result.recoveredSigner, verifiedOver };
966
- }
967
- return result.recoveredSigner !== void 0 ? { verified: false, reason: result.reason, recoveredSigner: result.recoveredSigner } : { verified: false, reason: result.reason };
968
- }
969
- function verifyPaymentReceipt(receipt, recover = defaultRecover) {
970
- if (typeof receipt !== "object" || receipt === null) return NOT_A_SIGNED_RECEIPT;
971
- const source = receipt;
972
- if (typeof source.authorization !== "object" || source.authorization === null) {
973
- return NOT_A_SIGNED_RECEIPT;
974
- }
975
- const auth = source.authorization;
976
- const { delegate, signHash: signHash2, signature, signatureScheme } = auth;
977
- if (typeof delegate !== "string" || delegate === "" || typeof signHash2 !== "string" || signHash2 === "") {
978
- return NOT_A_SIGNED_RECEIPT;
979
- }
980
- if (typeof signature !== "string" || signature === "") {
981
- return { verified: false, reason: "missing_signature" };
982
- }
983
- const chainId = typeof source.onChain?.chainId === "number" ? source.onChain.chainId : void 0;
984
- const digestVerifiable = chainId !== void 0 && DELEGATION_MANAGER_CHAIN_IDS.has(chainId);
985
- if (signatureScheme === "eip712_userop") {
986
- return { verified: false, reason: "not_verifiable_offline" };
987
- }
988
- if (signatureScheme === "eip712_delegation") {
989
- if (!digestVerifiable) {
990
- return { verified: false, reason: "not_verifiable_offline" };
991
- }
992
- let digest;
993
- try {
994
- digest = delegationDigest(signHash2, chainId);
995
- } catch {
996
- return { verified: false, reason: "bad_signature" };
997
- }
998
- return finish(match(delegate, digest, signature, recover), "delegation_digest");
999
- }
1000
- const raw = match(delegate, signHash2, signature, recover);
1001
- if (raw.ok) return finish(raw, "sign_hash");
1002
- if (digestVerifiable) {
1003
- let digest;
1004
- try {
1005
- digest = delegationDigest(signHash2, chainId);
1006
- } catch {
1007
- return finish(raw, "sign_hash");
1008
- }
1009
- const delegated = match(delegate, digest, signature, recover);
1010
- if (delegated.ok) return finish(delegated, "delegation_digest");
1011
- }
1012
- return finish(raw, "sign_hash");
1013
- }
1014
1197
 
1015
1198
  // src/account-reads.ts
1016
1199
  function safeBigInt(value) {
@@ -1043,6 +1226,19 @@ function formatTokenAmount(atomic, decimals) {
1043
1226
  const fracPart = digits.slice(digits.length - decimals).replace(/0+$/, "");
1044
1227
  return fracPart ? `${intPart}.${fracPart}` : intPart;
1045
1228
  }
1229
+ function mapSupersession(raw) {
1230
+ if (typeof raw !== "object" || raw === null) return null;
1231
+ const { agent_id: replacementAgentId, superseded_at: supersededAt } = raw;
1232
+ if (typeof replacementAgentId !== "string" || replacementAgentId === "") return null;
1233
+ if (typeof supersededAt !== "string" || supersededAt === "") return null;
1234
+ return {
1235
+ replacementAgentId,
1236
+ supersededAt,
1237
+ warning: `This agent was replaced on its machine by agent ${replacementAgentId} at ${supersededAt}. This session is still running as the old agent: restart the agent runtime so it loads the new agent, and ask the user to remove this agent on its Haven agent page if it is no longer used \u2014 it keeps any budget it still holds until then.`,
1238
+ nextAction: AgentPaymentNextAction.StopAndTellUser,
1239
+ nextToolOmittedReason: "The next step is the user's: restart this session, and remove the superseded agent in the Haven dashboard. No Haven tool does either."
1240
+ };
1241
+ }
1046
1242
  function mapTaskBudget(raw) {
1047
1243
  return {
1048
1244
  id: raw.id,
@@ -1182,7 +1378,10 @@ var AccountReads = class {
1182
1378
  remainingDisplay: allowance.remainingDisplay,
1183
1379
  configuredAmount: allowance.configuredAmount,
1184
1380
  resetPeriodMin: allowance.resetPeriodMin,
1185
- isResetPending: allowance.onchain.isResetPending
1381
+ isResetPending: allowance.onchain.isResetPending,
1382
+ // #3731: same provenance as the detailed read's field — the compact
1383
+ // row is a projection of the same allowance, never a second source.
1384
+ fundsCoverRemaining: allowance.fundsCoverRemaining
1186
1385
  };
1187
1386
  });
1188
1387
  const readiness = deriveReadiness(agent.status, allowances);
@@ -1275,6 +1474,7 @@ var AccountReads = class {
1275
1474
  accountAddress: raw.account_address,
1276
1475
  delegateAddress: raw.delegate_address,
1277
1476
  chainId: raw.chain_id,
1477
+ supersededBy: mapSupersession(raw.superseded_by),
1278
1478
  allowances: raw.allowances.map((allowance) => ({
1279
1479
  id: allowance.id,
1280
1480
  tokenAddress: allowance.token_address,
@@ -1287,6 +1487,11 @@ var AccountReads = class {
1287
1487
  ...allowance.recipient_address !== void 0 ? { recipientAddress: allowance.recipient_address } : {},
1288
1488
  ...allowance.merchant_id !== void 0 ? { merchantId: allowance.merchant_id } : {},
1289
1489
  ...allowance.reserved_haven_atomic !== void 0 ? { reservedHavenAtomic: allowance.reserved_haven_atomic } : {},
1490
+ // #3731: boolean → itself; ANY other value (absent, string, null) →
1491
+ // null, never undefined — a malformed or empty body answers
1492
+ // "unverifiable", not a missing key. The key is absent on the wire
1493
+ // when the remaining is 0; that maps to null here too.
1494
+ fundsCoverRemaining: allowance.funds_cover_remaining === true || allowance.funds_cover_remaining === false ? allowance.funds_cover_remaining : null,
1290
1495
  remainingDisplay: formatRemainingDisplay(allowance.token_address, allowance.token_symbol, allowance.onchain.remaining),
1291
1496
  onchain: {
1292
1497
  amount: allowance.onchain.amount,
@@ -1361,12 +1566,12 @@ var AccountReads = class {
1361
1566
  (allowance) => allowance.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
1362
1567
  );
1363
1568
  const payingHash = payment.budgetDelegationHash ?? null;
1364
- const match2 = payingHash ? tokenRows.find((allowance) => allowance.delegationHash === payingHash) ?? null : tokenRows[0] ?? null;
1365
- if (!match2 && payingHash) {
1569
+ const match = payingHash ? tokenRows.find((allowance) => allowance.delegationHash === payingHash) ?? null : tokenRows[0] ?? null;
1570
+ if (!match && payingHash) {
1366
1571
  return unavailable("the budget that paid is not one of this agent's own budgets", payment);
1367
1572
  }
1368
- if (!match2) return unavailable("no allowance/budget row matches the settled token", payment);
1369
- const remainingDisplay = formatRemainingDisplay(match2.tokenAddress, match2.tokenSymbol, match2.onchain.remaining);
1573
+ if (!match) return unavailable("no allowance/budget row matches the settled token", payment);
1574
+ const remainingDisplay = formatRemainingDisplay(match.tokenAddress, match.tokenSymbol, match.onchain.remaining);
1370
1575
  const rail = agentResult.value.executionRail;
1371
1576
  if (rail !== "delegation") {
1372
1577
  return unavailable(`the account's rail ('${rail}') cannot have a settled x402 payment`, payment);
@@ -1375,21 +1580,21 @@ var AccountReads = class {
1375
1580
  payment,
1376
1581
  allowance: {
1377
1582
  rail,
1378
- remaining_atomic: match2.onchain.remaining,
1583
+ remaining_atomic: match.onchain.remaining,
1379
1584
  // Deprecated spellings (#3464): kept for the deprecation window;
1380
1585
  // removal condition on the type's JSDoc.
1381
1586
  remaining_display: remainingDisplay,
1382
- token_symbol: match2.tokenSymbol,
1383
- token_address: match2.tokenAddress,
1384
- reset_period: match2.resetPeriodMin,
1587
+ token_symbol: match.tokenSymbol,
1588
+ token_address: match.tokenAddress,
1589
+ reset_period: match.resetPeriodMin,
1385
1590
  source: "active_delegations",
1386
1591
  // The canonical spellings — the SAME names and values
1387
1592
  // `haven_get_agent`'s allowances[] rows report.
1388
- remainingAtomic: match2.onchain.remaining,
1593
+ remainingAtomic: match.onchain.remaining,
1389
1594
  remainingDisplay,
1390
- resetPeriodMin: match2.resetPeriodMin,
1391
- tokenSymbol: match2.tokenSymbol,
1392
- tokenAddress: match2.tokenAddress
1595
+ resetPeriodMin: match.resetPeriodMin,
1596
+ tokenSymbol: match.tokenSymbol,
1597
+ tokenAddress: match.tokenAddress
1393
1598
  },
1394
1599
  warnings: []
1395
1600
  };
@@ -1420,6 +1625,12 @@ var AccountReads = class {
1420
1625
  nextCursor: typeof raw.next_cursor === "string" ? raw.next_cursor : null
1421
1626
  };
1422
1627
  }
1628
+ /**
1629
+ * #3723: the re-wrap is deliberate — this is the endpoint's own
1630
+ * `{ receipt, verification }` shape, and `verifyPaymentReceipt` accepts the
1631
+ * whole object or `.receipt` alone. The local `verification` is Haven's
1632
+ * self-check, not offline evidence.
1633
+ */
1423
1634
  async getReceipt(paymentId) {
1424
1635
  const { receipt } = await this.transport.get(`/payments/${paymentId}/receipt`);
1425
1636
  return { receipt, verification: verifyPaymentReceipt(receipt) };
@@ -1436,7 +1647,8 @@ var AccountReads = class {
1436
1647
  accountAddress: raw.account_address,
1437
1648
  delegateAddress: raw.delegate_address,
1438
1649
  chainId: raw.chain_id,
1439
- executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
1650
+ executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy",
1651
+ supersededBy: mapSupersession(raw.superseded_by)
1440
1652
  };
1441
1653
  }
1442
1654
  };
@@ -1593,11 +1805,25 @@ function noCompatiblePaymentOptionError(accepts) {
1593
1805
  const unsupportedOnly = !erc7710Only && accepts.some(
1594
1806
  (opt) => opt !== null && typeof opt === "object" && !isErc7710Option(opt) && (x402AssetTransferMethod(opt) !== null || opt.extra?.paymentFlow !== void 0)
1595
1807
  );
1808
+ const offeredSchemes = nonExactSchemesOnly(accepts);
1596
1809
  return new HavenApiError(
1597
- "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC." + (erc7710Only ? " The only Haven-compatible option this merchant advertises is tagged extra.assetTransferMethod: 'erc7710' (direct settlement), which this EIP-3009 payment path cannot settle \u2014 the limitation is the settlement scheme, not the asset. Paying this merchant requires a delegation-rail erc7710 flow (settleX402Erc7710, or the hosted MCP purchase tools)." : unsupportedOnly ? " Every option this merchant advertises asks for a transfer method or payment flow Haven cannot construct (for example extra.assetTransferMethod: 'permit2', or an unrecognized extra.paymentFlow). No payment intent was created and no funds moved." : ""),
1810
+ "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC." + (offeredSchemes ? ` The merchant offered only ${offeredSchemes}; Haven pays only the 'exact' scheme. For a request with a body, this often means the body or its Content-Type was missing or invalid: send the full request again (method, body, and Content-Type: application/json for a JSON API).` : erc7710Only ? " The only Haven-compatible option this merchant advertises is tagged extra.assetTransferMethod: 'erc7710' (direct settlement), which this EIP-3009 payment path cannot settle \u2014 the limitation is the settlement scheme, not the asset. Paying this merchant requires a delegation-rail erc7710 flow (settleX402Erc7710, or the hosted MCP purchase tools)." : unsupportedOnly ? " Every option this merchant advertises asks for a transfer method or payment flow Haven cannot construct (for example extra.assetTransferMethod: 'permit2', or an unrecognized extra.paymentFlow). No payment intent was created and no funds moved." : ""),
1598
1811
  400
1599
1812
  );
1600
1813
  }
1814
+ var SCHEME_ECHO_RE = /^[A-Za-z0-9_-]{1,32}$/;
1815
+ function nonExactSchemesOnly(accepts) {
1816
+ if (!Array.isArray(accepts) || accepts.length === 0) return null;
1817
+ const schemes = [];
1818
+ for (const opt of accepts) {
1819
+ if (opt === null || typeof opt !== "object") return null;
1820
+ const scheme = opt.scheme;
1821
+ if (typeof scheme !== "string" || scheme === "exact") return null;
1822
+ if (!SCHEME_ECHO_RE.test(scheme)) return null;
1823
+ if (!schemes.includes(scheme)) schemes.push(scheme);
1824
+ }
1825
+ return schemes.map((s) => `'${s}'`).join(", ");
1826
+ }
1601
1827
  function buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport) {
1602
1828
  const standard = selectStandardPaymentOption(paymentRequired.accepts);
1603
1829
  const option = standard ?? selectErc7710PaymentOption(paymentRequired.accepts);
@@ -1878,7 +2104,7 @@ var X402FundingLeg = class {
1878
2104
  );
1879
2105
  }
1880
2106
  }
1881
- const paymentHeader = await this.createPaymentHeader(paymentRequired, option);
2107
+ const paymentHeader = await this.createPaymentHeader(paymentRequired, option, raw.payment_id ?? state?.paymentId);
1882
2108
  if (raw.success && raw.tx_hash) {
1883
2109
  const receipt2 = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
1884
2110
  this.cacheReceipt(idempotencyKey, paymentHeader, receipt2, taskBudgetId, subBudgetId);
@@ -1913,17 +2139,44 @@ var X402FundingLeg = class {
1913
2139
  return receipt;
1914
2140
  }
1915
2141
  // ── Header minting ───────────────────────────────────────────────
1916
- async createPaymentHeader(paymentRequired, option) {
2142
+ /**
2143
+ * Mint the merchant-facing X-PAYMENT header with the delegate key.
2144
+ *
2145
+ * #3888: when `paymentId` is given, the EIP-3009 nonce is the shared
2146
+ * derivation from that Haven payment id instead of the x402 library's
2147
+ * random draw — that is what lets the backend attribute the merchant's
2148
+ * `AuthorizationUsed(delegate, nonce)` settlement to this payment when the
2149
+ * agent never reports it, and what makes a resume/retry re-sign of a
2150
+ * settled payment carry the SAME nonce (the merchant refuses it as already
2151
+ * used, closing the #3475 re-pay hazard). The signed typed data stays the
2152
+ * library's own: the nonce only overwrites the field on the unsigned
2153
+ * payload `preparePaymentHeader` built. Without a payment id the library's
2154
+ * random nonce is kept — an install too old to name its payment behaves
2155
+ * exactly as before.
2156
+ */
2157
+ async createPaymentHeader(paymentRequired, option, paymentId) {
1917
2158
  if (!this.delegateKey) {
1918
2159
  throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
1919
2160
  }
1920
2161
  const account = privateKeyToAccount(this.delegateKey);
1921
2162
  const requirements = toStandardPaymentRequirements(paymentRequired, option);
1922
- const header = await exact.evm.createPaymentHeader(
1923
- account,
1924
- paymentRequired.x402Version,
1925
- requirements
1926
- );
2163
+ let header;
2164
+ if (paymentId) {
2165
+ const unsigned = exact.evm.preparePaymentHeader(
2166
+ account.address,
2167
+ paymentRequired.x402Version,
2168
+ requirements
2169
+ );
2170
+ unsigned.payload.authorization.nonce = deriveX402PaymentNonce(paymentId);
2171
+ const signed = await exact.evm.signPaymentHeader(account, requirements, unsigned);
2172
+ header = exact.evm.encodePayment(signed);
2173
+ } else {
2174
+ header = await exact.evm.createPaymentHeader(
2175
+ account,
2176
+ paymentRequired.x402Version,
2177
+ requirements
2178
+ );
2179
+ }
1927
2180
  if (paymentRequired.x402Version < 2) return header;
1928
2181
  const payment = decodeBase64Json(header);
1929
2182
  return encodeBase64Json(x402V2PaymentEnvelope(paymentRequired, option, payment.payload));
@@ -2366,6 +2619,9 @@ function toolError(err) {
2366
2619
  var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
2367
2620
  var EVIDENCE_RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
2368
2621
  var EVIDENCE_RETRYABLE_STATUS = 503;
2622
+ function isWellFormedSettlementTxHash(hash) {
2623
+ return typeof hash === "string" && /^0x[0-9a-fA-F]{64}$/.test(hash);
2624
+ }
2369
2625
  var MerchantCompletion = class {
2370
2626
  post;
2371
2627
  merchantTransport;
@@ -2466,6 +2722,10 @@ var MerchantCompletion = class {
2466
2722
  protocolReceiptHeaderName,
2467
2723
  protocolReceiptHeader
2468
2724
  });
2725
+ const settlementCandidate = merchantSettlement.settlementTxHash;
2726
+ if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && settlementCandidate.toLowerCase() !== receipt.txHash.toLowerCase()) {
2727
+ await this.reportSettlementEvidenceOnce(receipt.paymentId, settlementCandidate);
2728
+ }
2469
2729
  await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
2470
2730
  return retryResponse;
2471
2731
  }
@@ -2499,6 +2759,20 @@ var MerchantCompletion = class {
2499
2759
  } catch {
2500
2760
  }
2501
2761
  }
2762
+ /**
2763
+ * #3770: record the agent's evidence-only delivery verdict on a settled
2764
+ * payment — `ok`, `unusable` or `partial`, with a bounded note. Moves no
2765
+ * money and never changes `settled` or any money field; the receipt reads
2766
+ * (`haven_list_receipts` / `haven_get_receipt`) then carry it beside the
2767
+ * payment. Refuses another agent's payment (404) and an unsettled one
2768
+ * (409) — a report requires a delivery that already happened.
2769
+ */
2770
+ async reportDeliveryQuality(input) {
2771
+ return await this.post(`/machine-payments/${input.paymentId}/delivery-quality`, {
2772
+ quality: input.quality,
2773
+ ...input.note !== void 0 ? { note: input.note } : {}
2774
+ });
2775
+ }
2502
2776
  async resolveCompletionContext(input) {
2503
2777
  const status = await this.getPaymentStatus(input.paymentId);
2504
2778
  if (status.rail !== "x402") {
@@ -2670,7 +2944,10 @@ var MerchantCompletion = class {
2670
2944
  rail: "x402",
2671
2945
  txHash,
2672
2946
  resourceUrl,
2673
- merchantStatus: input.merchantStatus
2947
+ merchantStatus: input.merchantStatus,
2948
+ // #3778: accepted outcomes only — a rejection delivered nothing, so
2949
+ // there is no deliverable to point at.
2950
+ ...input.deliveryReference ? { deliveryReference: input.deliveryReference } : {}
2674
2951
  });
2675
2952
  return { paymentId: status.paymentId, outcome: "accepted", txHash, resourceUrl, recorded: "evidence" };
2676
2953
  }
@@ -2687,28 +2964,38 @@ var MerchantCompletion = class {
2687
2964
  paymentProofHeader: input.paymentProofHeader,
2688
2965
  protocolReceiptHeaderName: input.protocolReceiptHeaderName,
2689
2966
  protocolReceiptHeader: input.protocolReceiptHeader,
2690
- protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
2967
+ protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0,
2968
+ ...input.deliveryReference ? { deliveryReference: input.deliveryReference } : {}
2691
2969
  };
2692
2970
  for (let attempt = 0; ; attempt += 1) {
2693
- try {
2694
- await this.post("/machine-payments/evidence", body);
2695
- return { outcome: "confirmed" };
2696
- } catch (err) {
2697
- const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
2698
- const retryable = statusCode === EVIDENCE_RETRYABLE_STATUS;
2699
- if (!retryable) {
2700
- const reason = refusalReasonFromBody(err);
2701
- return {
2702
- outcome: "refused",
2703
- statusCode: statusCode ?? 0,
2704
- ...reason ? { reason } : {}
2705
- };
2706
- }
2707
- if (attempt >= EVIDENCE_RETRY_DELAYS_MS.length) {
2708
- return { outcome: "retryable", statusCode };
2709
- }
2710
- await this.sleep(EVIDENCE_RETRY_DELAYS_MS[attempt]);
2971
+ const result = await this.postEvidenceOnce(body);
2972
+ if (result.outcome !== "retryable" || attempt >= EVIDENCE_RETRY_DELAYS_MS.length) {
2973
+ return result;
2711
2974
  }
2975
+ await this.sleep(EVIDENCE_RETRY_DELAYS_MS[attempt]);
2976
+ }
2977
+ }
2978
+ /**
2979
+ * ONE evidence attempt — the body of `reportEvidence`'s loop above, and the
2980
+ * whole of the #3764 settlement report below. A 503 answers `retryable`
2981
+ * immediately; the caller decides whether to sleep and re-attempt
2982
+ * (`reportEvidence`) or hand the outcome to its consumer (#3764 D2).
2983
+ */
2984
+ async postEvidenceOnce(body) {
2985
+ try {
2986
+ await this.post("/machine-payments/evidence", body);
2987
+ return { outcome: "confirmed" };
2988
+ } catch (err) {
2989
+ const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
2990
+ if (statusCode !== EVIDENCE_RETRYABLE_STATUS) {
2991
+ const reason = refusalReasonFromBody(err);
2992
+ return {
2993
+ outcome: "refused",
2994
+ statusCode: statusCode ?? 0,
2995
+ ...reason ? { reason } : {}
2996
+ };
2997
+ }
2998
+ return { outcome: "retryable", statusCode };
2712
2999
  }
2713
3000
  }
2714
3001
  /**
@@ -2753,6 +3040,27 @@ var MerchantCompletion = class {
2753
3040
  txHash: settlementTxHash
2754
3041
  });
2755
3042
  }
3043
+ /**
3044
+ * #3764: report the merchant's OWN EIP-3009 settlement transaction right
3045
+ * after a completed purchase — the second leg of a payment that already has
3046
+ * a funding-evidence row. Same endpoint and payload shape as
3047
+ * `reportSettlementEvidence` above, but with #3764's D2 budget: exactly ONE
3048
+ * attempt, no `EVIDENCE_RETRY_DELAYS_MS` backoff. The resource is already
3049
+ * paid for and delivered, so a retryable answer must not stretch the
3050
+ * completed purchase — the HOSTED tools hand the outcome to the agent
3051
+ * (`settlementEvidenceOutcome` → `haven_report_settlement_evidence`), and
3052
+ * the local caller swallows it by design (D1). Callers validate the hash
3053
+ * before this call (well-formed, non-zero, different from the funding hash
3054
+ * case-insensitively), so there is no client-side refusal here — the
3055
+ * backend verifies on-chain and is the only judge.
3056
+ */
3057
+ async reportSettlementEvidenceOnce(paymentId, settlementTxHash) {
3058
+ return this.postEvidenceOnce({
3059
+ paymentId,
3060
+ rail: "x402",
3061
+ txHash: settlementTxHash
3062
+ });
3063
+ }
2756
3064
  };
2757
3065
  function refusalReasonFromBody(err) {
2758
3066
  if (!(err instanceof HavenApiError)) return void 0;
@@ -2805,10 +3113,23 @@ function mapCatalogEntry(entry) {
2805
3113
  protocol: entry.protocol,
2806
3114
  toolName: entry.tool_name,
2807
3115
  toolArguments: entry.tool_arguments ?? null,
3116
+ // #3769: call semantics. Conditional spread like `merchant` — a backend
3117
+ // predating #3769 leaves the fields absent, never null-but-present, so a
3118
+ // fixture's exact object shape does not grow keys. Present on the wire
3119
+ // means the row was read post-#3769 and null means "not declared".
3120
+ ...entry.tool_arguments_schema !== void 0 ? {
3121
+ toolArgumentsSchema: entry.tool_arguments_schema ?? null,
3122
+ httpMethod: entry.http_method ?? null,
3123
+ bodyType: entry.body_type ?? null,
3124
+ bodyExample: entry.body_example ?? null
3125
+ } : {},
2808
3126
  priceDisplay: entry.price_display,
2809
3127
  priceAtomic: entry.price_atomic,
2810
3128
  asset: entry.asset,
2811
3129
  network: entry.network,
3130
+ // #3839: conditional like `merchant` — absent (not null) when the backend
3131
+ // does not send the field, so an older backend's exact shape is unchanged.
3132
+ ...entry.asset_transfer_methods !== void 0 ? { assetTransferMethods: entry.asset_transfer_methods } : {},
2812
3133
  status: entry.status,
2813
3134
  verifiedAt: entry.verified_at,
2814
3135
  source: entry.source,
@@ -2833,6 +3154,7 @@ var HavenClient = class {
2833
3154
  accountReads;
2834
3155
  delegateSweep;
2835
3156
  x402Wallet;
3157
+ egressPolicy;
2836
3158
  merchantTransport;
2837
3159
  confirmationTimeout;
2838
3160
  pollingInterval;
@@ -2864,6 +3186,15 @@ var HavenClient = class {
2864
3186
  merchantCompletion;
2865
3187
  /** Delegate address derived from the private key (if provided) */
2866
3188
  delegateAddress;
3189
+ /**
3190
+ * #3747: the merchant-egress policy this client enforces on every merchant
3191
+ * request, or `undefined` when none was configured (default SDK behaviour).
3192
+ * Hosted tool seams read this to run the SAME policy at quote/prepare time,
3193
+ * so a bad target is refused before funding, not only at the wire.
3194
+ */
3195
+ get merchantEgress() {
3196
+ return this.egressPolicy;
3197
+ }
2867
3198
  constructor(config) {
2868
3199
  this.delegateKey = config.delegateKey;
2869
3200
  this.havenApi = new HavenApiTransport(config);
@@ -2879,7 +3210,8 @@ var HavenClient = class {
2879
3210
  buildExplorerUrl: (chainId, hash) => buildExplorerUrl(chainId, hash)
2880
3211
  });
2881
3212
  this.x402Wallet = config.x402Wallet;
2882
- this.merchantTransport = new McpMerchantTransport({ merchantTimeout: config.merchantTimeout });
3213
+ this.egressPolicy = config.merchantEgress;
3214
+ this.merchantTransport = new McpMerchantTransport({ merchantTimeout: config.merchantTimeout, egress: config.merchantEgress });
2883
3215
  this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
2884
3216
  this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
2885
3217
  this.chainRpcs = config.chainRpcs ?? {};
@@ -3455,6 +3787,9 @@ var HavenClient = class {
3455
3787
  * locally. The server's own verification is ignored — the receipt is verified
3456
3788
  * here (independently of Haven) by recovering the signer from the
3457
3789
  * authorisation, so the result is trustworthy even if the backend lied.
3790
+ * The returned `{ receipt, verification }` object is the same shape the
3791
+ * `GET /payments/{id}/receipt` endpoint returns, and `verifyPaymentReceipt`
3792
+ * (#3723) accepts either layer: pass this whole object or `.receipt` alone.
3458
3793
  */
3459
3794
  async getReceipt(paymentId) {
3460
3795
  return this.accountReads.getReceipt(paymentId);
@@ -3687,7 +4022,7 @@ var HavenClient = class {
3687
4022
  async quoteX402(url, init, options = {}) {
3688
4023
  const initialInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
3689
4024
  const request = snapshotX402Request(url, initialInit);
3690
- const response = await this.merchantTransport.fetch(url, initialInit);
4025
+ const response = await this.merchantTransport.fetch(url, initialInit, this.merchantTransport.budgetFor("quote"), this.merchantTransport.capFor("quote"));
3691
4026
  if (response.status !== 402) {
3692
4027
  const toolResultChallenge = await this.merchantTransport.extractToolResultChallenge(response, initialInit);
3693
4028
  if (toolResultChallenge) {
@@ -3697,7 +4032,8 @@ var HavenClient = class {
3697
4032
  let body;
3698
4033
  try {
3699
4034
  body = await response.clone().json();
3700
- } catch {
4035
+ } catch (err) {
4036
+ if (err instanceof MerchantEgressResponseCapError) throw err;
3701
4037
  body = void 0;
3702
4038
  }
3703
4039
  throw new X402UnexpectedStatusError(
@@ -3829,7 +4165,7 @@ var HavenClient = class {
3829
4165
  "settled"
3830
4166
  );
3831
4167
  }
3832
- const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option);
4168
+ const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option, status.paymentId);
3833
4169
  const receipt = this.fundingLeg.receiptFromStatus(input.paymentRequired, option, paymentHeader, status);
3834
4170
  this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt, input.taskBudgetId);
3835
4171
  return receipt;
@@ -3846,7 +4182,7 @@ var HavenClient = class {
3846
4182
  if (!url) {
3847
4183
  throw new HavenApiError("x402 resume requires the original URL or a captured request snapshot.", 400);
3848
4184
  }
3849
- const response = await this.merchantTransport.fetch(url, initialInit);
4185
+ const response = await this.merchantTransport.fetch(url, initialInit, this.merchantTransport.budgetFor("quote"), this.merchantTransport.capFor("quote"));
3850
4186
  if (response.status !== 402) {
3851
4187
  throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
3852
4188
  }
@@ -3891,7 +4227,7 @@ var HavenClient = class {
3891
4227
  }
3892
4228
  let requestInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
3893
4229
  if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3894
- const response = await this.merchantTransport.fetch(url, requestInit);
4230
+ const response = await this.merchantTransport.fetch(url, requestInit, this.merchantTransport.budgetFor("quote"), this.merchantTransport.capFor("quote"));
3895
4231
  let paymentRequired;
3896
4232
  let nativeChallenge = false;
3897
4233
  if (response.status !== 402) {
@@ -3904,7 +4240,8 @@ var HavenClient = class {
3904
4240
  } else {
3905
4241
  try {
3906
4242
  paymentRequired = await parsePaymentRequiredResponse(response);
3907
- } catch {
4243
+ } catch (err) {
4244
+ if (err instanceof MerchantEgressResponseCapError) throw err;
3908
4245
  return response;
3909
4246
  }
3910
4247
  }
@@ -4013,6 +4350,7 @@ var HavenClient = class {
4013
4350
  const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
4014
4351
  const merchantAccepted = surfaced.ok && inBandChallenge === void 0 && metaSettlement?.success !== false;
4015
4352
  let evidenceOutcome;
4353
+ let settlementEvidenceOutcome;
4016
4354
  if (!merchantAccepted) {
4017
4355
  if (!input.noFundingLeg && fundingTxHash) {
4018
4356
  await this.merchantCompletion.recordRetryRejected({
@@ -4044,9 +4382,21 @@ var HavenClient = class {
4044
4382
  paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
4045
4383
  paymentProofHeader: input.paymentHeader,
4046
4384
  protocolReceiptHeaderName,
4047
- protocolReceiptHeader
4385
+ protocolReceiptHeader,
4386
+ // #3778: the delivery pointer rides the accepted arm's evidence
4387
+ // report — a rejection delivered nothing to point at.
4388
+ ...input.deliveryReference ? { deliveryReference: input.deliveryReference } : {}
4048
4389
  });
4049
4390
  }
4391
+ if (!input.noFundingLeg) {
4392
+ const settlementCandidate = settlement.settlementTxHash;
4393
+ if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && fundingTxHash && settlementCandidate.toLowerCase() !== fundingTxHash.toLowerCase()) {
4394
+ settlementEvidenceOutcome = await this.merchantCompletion.reportSettlementEvidenceOnce(
4395
+ evidenceContext.paymentId,
4396
+ settlementCandidate
4397
+ );
4398
+ }
4399
+ }
4050
4400
  await this.merchantCompletion.reportMerchantReceipt(evidenceContext.paymentId, surfaced);
4051
4401
  }
4052
4402
  return {
@@ -4055,7 +4405,8 @@ var HavenClient = class {
4055
4405
  body,
4056
4406
  // #3155 review S3: a transaction beside a rejection is not a settlement.
4057
4407
  settlementTxHash: merchantAccepted ? settlement.settlementTxHash ?? void 0 : void 0,
4058
- evidenceOutcome
4408
+ evidenceOutcome,
4409
+ settlementEvidenceOutcome
4059
4410
  };
4060
4411
  }
4061
4412
  /**
@@ -4090,13 +4441,26 @@ var HavenClient = class {
4090
4441
  async reportSettlementEvidence(paymentId, settlementTxHash) {
4091
4442
  return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash);
4092
4443
  }
4444
+ /**
4445
+ * #3770: record the agent's evidence-only delivery verdict on a settled
4446
+ * payment — `ok`, `unusable` or `partial`, with a bounded note. The
4447
+ * receipt reads (`haven_list_receipts` / `haven_get_receipt`) then carry
4448
+ * it beside the payment. Moves no money and never changes `settled`;
4449
+ * another agent's payment is refused (404), an unsettled one refused
4450
+ * (409).
4451
+ */
4452
+ async reportDeliveryQuality(input) {
4453
+ return await this.merchantCompletion.reportDeliveryQuality(input);
4454
+ }
4093
4455
  /**
4094
4456
  * GET /x402/:id/merchant-call-context — the settle-leg twin of #1263's
4095
4457
  * sign-context fetch (#1307). Re-serves the stored merchant MCP-tool call
4096
- * context (merchant_url, tool_name, arguments, mcp_transport) recorded at
4097
- * quote time, so `haven_settle_mcp_tool` / `haven_complete_mcp_tool` can
4098
- * omit those fields and let Haven rehydrate them by payment_id instead of
4099
- * the caller re-threading them. Throws `HavenApiError` (404 unknown/foreign
4458
+ * context (merchant_url, tool_name, arguments, mcp_transport — and the
4459
+ * catalog row name #3781 added, when the purchase came from a catalog
4460
+ * entry) recorded at quote time, so `haven_settle_mcp_tool` /
4461
+ * `haven_complete_mcp_tool` can omit those fields and let Haven rehydrate
4462
+ * them by payment_id instead of the caller re-threading them. Throws
4463
+ * `HavenApiError` (404 unknown/foreign
4100
4464
  * payment_id, 409 no stored context, 410 expired) — the caller decides the
4101
4465
  * fallback (re-send the full context explicitly).
4102
4466
  */
@@ -4109,6 +4473,9 @@ var HavenClient = class {
4109
4473
  merchantUrl: raw.merchant_url,
4110
4474
  toolName: raw.tool_name,
4111
4475
  arguments: raw.arguments ?? {},
4476
+ // #3781: the catalog row name rides the same blob; absent on a direct
4477
+ // haven_pay_mcp_tool purchase.
4478
+ ...raw.catalog_name ? { catalogName: raw.catalog_name } : {},
4112
4479
  ...raw.mcp_transport ? {
4113
4480
  mcpTransport: {
4114
4481
  handshakeRequired: raw.mcp_transport.handshake_required,
@@ -4117,6 +4484,36 @@ var HavenClient = class {
4117
4484
  } : {}
4118
4485
  };
4119
4486
  }
4487
+ /**
4488
+ * GET /x402/by-idempotency-key/:key — read-only, agent-scoped lookup of the
4489
+ * caller's own x402 intent for an idempotency key (#3739). A hosted pay tool
4490
+ * calls it BEFORE re-probing a merchant, so a replay never depends on the
4491
+ * merchant still answering. Writes nothing (a stale pending intent is
4492
+ * reported with `windowOpen: false`, never expired here). Returns `null`
4493
+ * when there is no such intent (a 404: unknown and not-yours are the same
4494
+ * answer); any other failure throws `HavenApiError`.
4495
+ */
4496
+ async findX402IntentByIdempotencyKey(key) {
4497
+ let raw;
4498
+ try {
4499
+ raw = await this.get(`/x402/by-idempotency-key/${encodeURIComponent(key)}`);
4500
+ } catch (err) {
4501
+ if (err instanceof HavenApiError && err.statusCode === 404) return null;
4502
+ throw err;
4503
+ }
4504
+ return {
4505
+ paymentId: raw.payment_id,
4506
+ status: raw.status,
4507
+ settlementScheme: raw.settlement_scheme ?? null,
4508
+ resourceUrl: raw.resource_url ?? null,
4509
+ expiresAt: raw.expires_at ?? null,
4510
+ windowOpen: raw.window_open === true,
4511
+ taskBudgetId: raw.task_budget_id ?? null,
4512
+ amountAtomic: raw.amount_atomic,
4513
+ asset: raw.asset ?? null,
4514
+ network: raw.network
4515
+ };
4516
+ }
4120
4517
  /**
4121
4518
  * Wait for a funding tx to be mined with ≥1 confirmation before the
4122
4519
  * merchant retry, eliminating the race where the merchant's
@@ -4236,6 +4633,7 @@ function sleep(ms) {
4236
4633
  function composeDescription(d) {
4237
4634
  return [d.summary, d.selectionGuidance, d.behavior, d.nextActionGuidance].filter(Boolean).join(" ");
4238
4635
  }
4636
+ var FUNDING_LEG_DISCOVERY_GUIDANCE = `funding_leg_expected is a catalog hint: among equivalent entries prefer false (erc7710, no funding leg); "unknown" is no preference; never hide true entries. A recipient-pinned budget can pay erc7710 merchants only. The quote's expected_funding_leg is authoritative.`;
4239
4637
  var toolDescriptions = {
4240
4638
  quoteX402: {
4241
4639
  summary: "Inspect an HTTP 402 x402 paid resource without creating a Haven payment, signature, approval, or on-chain transaction.",
@@ -4275,14 +4673,14 @@ var toolDescriptions = {
4275
4673
  },
4276
4674
  getAgent: {
4277
4675
  summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, per-token remaining allowance (atomic + human-readable). The recommended first call in a new session.",
4278
- selectionGuidance: "Use this as the session bootstrap, or to confirm identity together with whether the agent can spend right now. For per-token detail (configured vs spent vs reset window) use haven_get_allowances.",
4279
- behavior: 'Reads identity plus the live spend-authority snapshot \u2014 the active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. It covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify the signer with a signer tool call or connect --doctor. An over-budget payment is declined before any money moves; there is no approval queue \u2014 ask the owner to grant or raise the budget in Haven. allowances[] carries id, tokenAddress, remainingAtomic, remainingDisplay per token. taskBudgets[] rows carry maxAtomic/maxDisplay plus spentAtomic/remainingAtomic/remainingDisplay \u2014 what the chain still allows that budget \u2014 and remainingIsFromChain: false marks a failed read (figures null, never the full cap); only live rows are listed (closed and expired omitted), each with its status (pending | open | closing) and isExpired, so a closing budget stays visible. pendingSubBudgetSignatures[] lists sub-budget rows awaiting YOUR signature, each with its haven_sign next step. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.',
4676
+ selectionGuidance: "Use this as the session bootstrap, or to confirm identity together with whether the agent has spend authority right now. For per-token detail (configured vs spent vs reset window) use haven_get_allowances.",
4677
+ behavior: `Reads identity plus the live spend-authority snapshot \u2014 the active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. Readiness is AUTHORITY: it says a budget is live, not that the account holds funds. Each allowances[] row carries fundsCoverRemaining: false when the account's balance cannot back that row's whole remaining budget \u2014 a heads-up to mention to the user, not a refusal (budgets above the balance are a normal setup; an owner may top up weekly) \u2014 and null when the coverage read failed or the remaining figure was not read live; the key is absent when the remaining is 0, and rows for one token are compared alone. It covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify the signer with a signer tool call or connect --doctor. An over-budget payment is declined before any money moves; there is no approval queue \u2014 ask the owner to grant or raise the budget in Haven. allowances[] carries id, tokenAddress, remainingAtomic, remainingDisplay per token. taskBudgets[] rows carry maxAtomic/maxDisplay plus spentAtomic/remainingAtomic/remainingDisplay \u2014 what the chain still allows that budget \u2014 and remainingIsFromChain: false marks a failed read (figures null, never the full cap); only live rows are listed (closed and expired omitted), each with its status (pending | open | closing) and isExpired, so a closing budget stays visible. pendingSubBudgetSignatures[] lists sub-budget rows awaiting YOUR signature, each with its haven_sign next step. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.`,
4280
4678
  nextActionGuidance: ""
4281
4679
  },
4282
4680
  getAllowances: {
4283
4681
  summary: "Return configured and on-chain allowance state for the authenticated agent. On-chain allowance is the real spend gate.",
4284
- selectionGuidance: "Use this when the user asks about allowance, budget, spend limit, remaining amount, remaining allowance, remaining budget, daily limit, reset period, what can I spend, or what the agent can still spend. For whether the account actually HOLDS funds behind the budget use haven_check_funds.",
4285
- behavior: "Returns the per-token spend authority for the account: the active budget delegation (remaining = the period budget, which re-arms natively at the period boundary), each with id, onchain.remaining, remainingDisplay, delegationHash, recipientAddress (null = open; a pinned budget pays only that recipient), merchantId (set only on a merchant-locked budget), and reservedHavenAtomic \u2014 Haven-side task- and sub-budget reservations summed beside, never folded into, the on-chain figure. An over-budget payment is declined before any money moves; nothing queues. Configured amounts from Haven are returned alongside. Open task budgets are not listed here: haven_get_agent's taskBudgets[] carries each one's spentAtomic/remainingAtomic and lifecycle status.",
4682
+ selectionGuidance: "Use this when the user asks about allowance, budget, spend limit, remaining amount, remaining allowance, remaining budget, daily limit, reset period, what can I spend, or what the agent can still spend.",
4683
+ behavior: "Returns the per-token spend authority for the account: the active budget delegation (remaining = the period budget, which re-arms natively at the period boundary), each with id, onchain.remaining, remainingDisplay, delegationHash, recipientAddress (null = open; a pinned budget pays only that recipient), merchantId (set only on a merchant-locked budget), reservedHavenAtomic \u2014 Haven-side task- and sub-budget reservations summed beside, never folded into, the on-chain figure \u2014 and fundsCoverRemaining: false when the account's balance cannot back that row's whole remaining budget (a heads-up to mention to the user, not a refusal \u2014 budgets above the balance are a normal setup), null when the coverage read failed or the remaining figure was not read live, absent when the remaining is 0, each row compared alone. An over-budget payment is declined before any money moves; nothing queues. Configured amounts from Haven are returned alongside. Open task budgets are not listed here: haven_get_agent's taskBudgets[] carries each one's spentAtomic/remainingAtomic and lifecycle status.",
4286
4684
  nextActionGuidance: ""
4287
4685
  },
4288
4686
  // #3126 — the sufficiency signal, deliberately NOT a balance tool. The
@@ -4301,8 +4699,14 @@ var toolDescriptions = {
4301
4699
  },
4302
4700
  verifyReceipt: {
4303
4701
  summary: "Verify a signed payment receipt offline \u2014 confirm the agent delegate signed its authorisation.",
4304
- selectionGuidance: "Takes the signed bundle from GET /payments/{id}/receipt, not a haven_list_receipts row \u2014 a row carries no signature and returns not_a_signed_receipt. No network.",
4305
- behavior: "Never throws. An erc7710 bundle verifies over the delegation digest the delegate signed; a direct or eip3009 bundle returns not_verifiable_offline. verified true means only that the delegate signed the hash named by verifiedOver \u2014 payment facts are Haven-asserted and settlement is not proven; check settlementTxHash on an explorer.",
4702
+ selectionGuidance: "The signed bundle comes from haven_get_receipt or GET /payments/{id}/receipt \u2014 that response is accepted as-is (whole, or its .receipt) \u2014 not a haven_list_receipts row: a row carries no signature and returns not_a_signed_receipt. No network.",
4703
+ behavior: "Never throws. An erc7710 bundle verifies over the delegation digest the delegate signed; a direct or eip3009 bundle returns not_verifiable_offline. verified true means only that the delegate signed the hash named by verifiedOver \u2014 payment facts are Haven-asserted and settlement is not proven; check settlementTxHash on an explorer. The endpoint wrapper's own verification field, when one rides along, is ignored \u2014 offline recovery is the only evidence.",
4704
+ nextActionGuidance: ""
4705
+ },
4706
+ getReceipt: {
4707
+ summary: "Fetch one settled payment's signed receipt bundle \u2014 the self-contained evidence an offline verifier checks.",
4708
+ selectionGuidance: "This agent's own settled payments only. Use when the signed bundle itself is needed \u2014 receipt verification (haven_verify_receipt reads it unchanged), attaching payment evidence to a report \u2014 not for status questions: haven_get_payment_status answers status, and haven_list_receipts rows are history metadata without the signature.",
4709
+ behavior: "Returns { receipt } \u2014 the signed haven-receipt-1 bundle. An unknown id, another agent's id, or the agent's own unsettled payment is a structured 404. Settlement is not proven by fetching: check settlementTxHash on an explorer.",
4306
4710
  nextActionGuidance: ""
4307
4711
  },
4308
4712
  payMcpTool: {
@@ -4314,7 +4718,7 @@ var toolDescriptions = {
4314
4718
  discoverTools: {
4315
4719
  summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, the next call.",
4316
4720
  selectionGuidance: "Use this when the user asks what the agent can buy, pay for, or which paid services exist \u2014 or when you need a resource URL for a service the user described. Use verified=verified for entries Haven watched pass a live quote probe (operator-curated or self-submitted) \u2014 domain_verified is the only ownership claim; never treat these badges as proof of merchant honesty, quality, or reliability. Do NOT use for balance, budget, or spend-limit questions \u2014 use haven_get_allowances. Do NOT use to pay \u2014 each returned entry names the next tool to call.",
4317
- behavior: "Use each entry's suggested_tool + suggested_arguments first; an entry without them says why. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Use category for a case-insensitive category filter (for example, VPN or vpn), or search for a product name, category, or description term. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, the hint, and the provenance badges source/domain_verified/verified_payable. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
4721
+ behavior: "Use each entry's suggested_tool + suggested_arguments first; an entry without them says why. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Use category for a case-insensitive category filter (for example, VPN or vpn), or search for a product name, category, or description term. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, the hint, and the provenance badges source/domain_verified/verified_payable. " + FUNDING_LEG_DISCOVERY_GUIDANCE + " The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
4318
4722
  nextActionGuidance: `Call suggested_tool with suggested_arguments VERBATIM (no hint: read suggested_tool_omitted_reason). Confirm the price from the live quote or pay result, not the catalog; if the next tool takes a cap, pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1"), never atomic units by hand.`
4319
4723
  },
4320
4724
  submitCatalogEntry: {
@@ -4556,9 +4960,17 @@ var havenTools = {
4556
4960
  openai: openaiTools
4557
4961
  };
4558
4962
 
4963
+ // src/catalog-funding-leg.ts
4964
+ function catalogFundingLegExpected(assetTransferMethods) {
4965
+ if (typeof assetTransferMethods !== "string") return "unknown";
4966
+ const methods = assetTransferMethods.split(",").map((method) => method.trim()).filter((method) => method.length > 0);
4967
+ if (methods.length === 0) return "unknown";
4968
+ return !methods.includes("erc7710");
4969
+ }
4970
+
4559
4971
  // src/agent-guidance.ts
4560
4972
  var AGENT_SECRET_HYGIENE_SENTENCE = "Do not print private keys, API keys, credential file contents, or config secrets in chat or logs.";
4561
- var AGENT_CLIENT_UPDATE_SENTENCE = "If a Haven result carries `client_update`, that client is out of date: run its `upgrade_command` as given, then any repair line it prints, then retry. `required: true` means payments are refused until you do.";
4973
+ var AGENT_CLIENT_UPDATE_SENTENCE = "If a Haven result carries `client_update`, that client is out of date: run its `upgrade_command` exactly as given \u2014 a connector command may name your pair with `--name <slug>` \u2014 then any repair line it prints, then retry. `required: true` means payments are refused until you do.";
4562
4974
  var AGENT_LOCAL_KEY_SENTENCE = "The Haven connector generates the signing key locally and sends Haven only the public signing address plus proof.";
4563
4975
  var AGENT_NETWORK_ACCESS_SENTENCE = "Network access is expected: this command downloads the npm package and contacts the Haven API, so if your environment is sandboxed, run it with network access enabled or request network access escalation; that changes the execution environment, not the command, and is not a command modification.";
4564
4976
  var AGENT_JSON_MODE_SENTENCE = "If you are an AI agent running this command yourself rather than a human pasting it, you should append --json: the connector then emits one machine-readable, secret-free result object on stdout with progress on stderr, and returns promptly instead of blocking while it waits for the budget approval.";
@@ -4606,7 +5018,7 @@ haven budget grant <agentId> --amount <n> --token USDC --period <minutes> [--rec
4606
5018
  haven budget revoke <agentId> <delegationHash> [--wait]
4607
5019
  \`\`\`
4608
5020
 
4609
- The CLI never signs: it prints a dashboard link, your user opens it and signs with their passkey or wallet. \`--wait\` polls until the human's signature lands. The hash for \`revoke\` is in \`haven agents show <agentId>\`.
5021
+ The CLI never signs: it prints a dashboard link, your user opens it and signs with their passkey or wallet. \`--wait\` polls until the human's signature lands. The hash for \`revoke\` is in \`haven budget show <agentId> --hashes\`.
4610
5022
 
4611
5023
  ## Hand-off scripts
4612
5024
 
@@ -4656,7 +5068,7 @@ If the approval wait times out, nothing is lost \u2014 your agent is registered
4656
5068
 
4657
5069
  Call \`haven_get_agent\`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus \`spend_authority_readiness\`:
4658
5070
 
4659
- - \`ready\` \u2014 a budget is live; you can pay.
5071
+ - \`ready\` \u2014 a budget is live: you have the authority to pay. It does not say the account holds funds \u2014 the \`allowances[]\` rows carry \`funds_cover_remaining\`, and \`false\` there is a heads-up to mention to your user, not a refusal.
4660
5072
  - \`needs_approval\` \u2014 the connector finished, nobody approved yet. Ask your user again, in their Haven tab; there is no queue to wait in.
4661
5073
  - \`revoked\` \u2014 the credential is not active; ask your user to create a new agent.
4662
5074
 
@@ -4693,7 +5105,7 @@ Then:
4693
5105
  1. Run \`<packages.cli.one_liner> login\`. That command is the manifest's \`packages.cli.one_liner\` (\`/.well-known/haven.json\`) \u2014 run it as given: \`packages.cli.channel\` serves the full spec. It prints a link and a code and does not need my password \u2014 it must never ask for it. Give me the link straight away and wait for me to approve it in my browser.
4694
5106
  2. Once I have approved, run \`haven agents connect --name <a name you choose> --budget <amount> --token USDC --period <minutes>\` with the budget I tell you. If I have not given you one, ask me before running it. Add \`--run\` to complete the connection in the same step.
4695
5107
  3. ${AGENT_APPROVAL_RELAY_JSON_SENTENCE}
4696
- 4. Once I have approved the budget, verify with the \`haven_get_agent\` tool: \`ready\` means you can pay, \`needs_approval\` means my approval has not landed yet.
5108
+ 4. Once I have approved the budget, verify with the \`haven_get_agent\` tool: \`ready\` means I have given you the authority to pay \u2014 it does not confirm the account holds funds, so if its allowances rows carry \`funds_cover_remaining: false\`, tell me and still try the payment; \`needs_approval\` means my approval has not landed yet.
4697
5109
 
4698
5110
  Two things only I can do: approving that budget with my passkey, and funding the account with USDC \u2014 no ETH, Haven sponsors the gas. Tell me if either is missing rather than working around it.
4699
5111
 
@@ -4702,7 +5114,7 @@ ${AGENT_SECRET_HYGIENE_SENTENCE}`;
4702
5114
  // src/skill-content.ts
4703
5115
  var HAVEN_SKILL_MD = `---
4704
5116
  name: haven-pay
4705
- description: Pay for things from the user's Haven wallet within their agent rules, and set Haven up when it is not yet connected. Use when the user asks to send, pay, tip, or transfer crypto; when a request hits an HTTP 402 (x402) paywall; or when they ask to create a Haven account, create an agent, or connect one.
5117
+ description: Pay for things from the user's Haven wallet within their agent rules, and set Haven up when it is not yet connected. Use when the user asks to send, pay, tip, or transfer crypto; when a request hits an HTTP 402 (x402) paywall, or another tool returns an x402 payment URL; or when they ask to create a Haven account, create an agent, or connect one.
4706
5118
  ---
4707
5119
 
4708
5120
  # Haven: pay from a Haven wallet
@@ -4714,8 +5126,16 @@ nothing is paid past the rules the user set.
4714
5126
 
4715
5127
  Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
4716
5128
  the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
5129
+ Those are an UNNAMED pair's names: the default setup now names the pair from
5130
+ the agent's display name, \`haven-<slug>\` / \`haven-signer-<slug>\`, and its
5131
+ tool names follow that pair \u2014 \`mcp__haven-<slug>__\u2026\` /
5132
+ \`mcp__haven-signer-<slug>__\u2026\`. Read the server names off the agent's own
5133
+ configuration (or the \`next_tool_server\` field) rather than assuming the bare
5134
+ ones; bare \`haven\` / \`haven-signer\` remain on installs wired before the
5135
+ change, and \`--bare\` still opts into them.
4717
5136
  That namespacing is Claude-family; other runtimes name the servers by their
4718
- own config keys (Codex: \`haven\`, \`haven_signer\`). Tool results carry the
5137
+ own config keys (Codex: \`haven\`, \`haven_signer\` \u2014 or the pair's two
5138
+ suffixed names). Tool results carry the
4719
5139
  exact next step (\`next_action\`, \`next_tool\`, \`next_arguments\`, plus the
4720
5140
  runtime-neutral \`next_tool_server\` + \`next_tool_name\` + \`next_tool_server_role\`
4721
5141
  \u2014 the bare tool name on that logical server, whatever your runtime calls it).
@@ -4724,11 +5144,27 @@ says why; that is a complete answer.
4724
5144
  Follow those fields first; the prose below is fallback and orientation, not
4725
5145
  the source of truth.
4726
5146
 
5147
+ **When more than one Haven pair is configured** (a named pair is
5148
+ \`haven-<slug>\` + \`haven-signer-<slug>\`), you act as ONE agent per task. If
5149
+ the user has not said which \u2014 in the request, or a project-level choice they
5150
+ stated \u2014 ask before any payment tool. Keep every call inside that pair: a
5151
+ signer call goes to the signer of the hosted server you called \u2014
5152
+ \`haven-<slug>\` with \`haven-signer-<slug>\`, bare \`haven\` with
5153
+ \`haven-signer\`, Codex \`haven\` with \`haven_signer\`. Confirm by identity,
5154
+ not name: \`haven_get_agent\` returns \`id\` and \`delegateAddress\`, and each
5155
+ signer states the agent id and delegate address it is bound to in its own
5156
+ instructions (compare the delegate address alone when a signer has no recorded
5157
+ agent id). If they differ, stop and sign nothing \u2014 switch to the signer whose
5158
+ identity matches.
5159
+
4727
5160
  ## When to use this skill
4728
5161
 
4729
5162
  - The user asks to send money, pay someone, tip, donate, or transfer tokens.
4730
5163
  - A request returns HTTP 402 (x402): use the Haven pay tools to settle it,
4731
5164
  then retry the original request.
5165
+ - Another tool or API hands back an x402 payment URL (a merchant's own
5166
+ checkout, for example): pay that URL with the Haven pay tools rather than a
5167
+ raw deposit address \u2014 see *Paying* below.
4732
5168
 
4733
5169
  ## Onboarding and setup
4734
5170
 
@@ -4804,7 +5240,14 @@ spending:
4804
5240
  (wallet, network) plus \`spend_authority_readiness\` (\`ready\` / \`needs_approval\` /
4805
5241
  \`revoked\`) and live remaining per-token allowance, in one shot. That signal
4806
5242
  covers hosted identity and on-chain spend authority only \u2014 it cannot see the
4807
- local signer; the signer is verified by calling any signer tool.
5243
+ local signer; the signer is verified by calling any signer tool. Readiness is
5244
+ authority: \`ready\` says a budget is live, not that money is there. Each
5245
+ \`allowances[]\` row carries \`funds_cover_remaining\`: \`false\` (the account
5246
+ cannot back that row's whole remaining budget) is a heads-up to mention to
5247
+ the user, not a refusal \u2014 a budget above the balance is a normal setup, so
5248
+ still try the payment; \`null\` means the coverage read failed or the remaining
5249
+ figure was not read live; the key is absent when the remaining is 0. Rows for
5250
+ one token are compared alone.
4808
5251
  - \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
4809
5252
  (configured, spent, reset window) when you need more than the summary.
4810
5253
  - \`mcp__haven__haven_check_funds\` \u2014 whether the account actually HOLDS at
@@ -4905,9 +5348,40 @@ merchant leg for you.
4905
5348
  **Direct transfer / non-MCP paywall:** \`mcp__haven__haven_pay\` with
4906
5349
  \`to\`, \`amount\`, and \`token\` for a plain transfer. For an arbitrary,
4907
5350
  non-MCP x402 paywall: \`mcp__haven__haven_quote_x402\` to get a quote, then
4908
- \`mcp__haven__haven_pay_x402_quote\` \u2014 follow the result's guidance fields
4909
- first and sign in the local Haven signer. On THIS path Haven does not talk to
4910
- the merchant: \`mcp__haven-signer__haven_sign_x402\` returns both
5351
+ \`mcp__haven__haven_pay_x402_quote\` with the quote's \`next_arguments\`
5352
+ (\`url\`, \`method\`, \`headers\`, \`body\`, a cap and an
5353
+ \`idempotency_key\`) and no
5354
+ \`payment_required\`: Haven fetches the payment challenge itself, so there is
5355
+ nothing to copy. Follow the result's guidance fields first and sign in the
5356
+ local Haven signer. If the 402 carries a
5357
+ \`sign-in-with-x\` extension (x402 Sign-In-With-X), call
5358
+ \`mcp__haven-signer__haven_sign_siwx\` with \`{ url, challenge }\` \u2014 \`url\` is
5359
+ the FINAL URL after redirects \u2014 and retry the merchant with the
5360
+ \`SIGN-IN-WITH-X\` header it returns: the delegate
5361
+ wallet signs in as the wallet that paid, moving no funds. NEVER follow a
5362
+ redirect with \`SIGN-IN-WITH-X\` (or a resulting session token) attached; if
5363
+ the final origin differs, re-sign there.
5364
+
5365
+ Sign in BEFORE a multi-call flow: if a merchant documents or offers a
5366
+ sign-in session (its docs, or a 402 whose \`sign-in-with-x\` extension names
5367
+ a free sign-in route), do not pay call by call. Send the unsigned sign-in
5368
+ request yourself, take the \`sign-in-with-x\` extension from its 402, and
5369
+ call \`mcp__haven-signer__haven_sign_siwx\` with it. The session token (often
5370
+ about 2 h) waives the merchant's micro-fees, so make every now-free call
5371
+ YOURSELF with the \`SIGN-IN-WITH-X\` header set \u2014 never pass a merchant
5372
+ session token to \`mcp__haven__haven_quote_x402\` or
5373
+ \`mcp__haven__haven_pay_x402_quote\` (their unpaid probe refuses a 200 with
5374
+ X402_PROBE_NOT_PAYMENT_REQUIRED) \u2014 and pay only where the merchant still
5375
+ charges (on Bitrefill, \`invoice/pay\`; keep sending the session token
5376
+ there). Sign at the FINAL URL after redirects, and never forward
5377
+ \`SIGN-IN-WITH-X\` or a session token across a redirect. If the signer's
5378
+ tools/list has no \`haven_sign_siwx\` (a signer older than 0.9.0-alpha.0),
5379
+ or the sign-in is refused, pay each call normally instead.
5380
+
5381
+ On THIS path Haven never sends the
5382
+ paid request (it sends only unpaid probes):
5383
+ on the **EIP-3009** scheme (the pay result names
5384
+ \`mcp__haven-signer__haven_sign_x402\`), that tool returns both
4911
5385
  \`signature\` and \`payment_header\`; relay \`signature\` with
4912
5386
  \`mcp__haven__haven_submit\`, then retry the paywalled URL yourself with
4913
5387
  \`payment_header\`. Do not pass that call's \`x402_binding\` to
@@ -4915,13 +5389,27 @@ the merchant: \`mcp__haven-signer__haven_sign_x402\` returns both
4915
5389
  building the header, so the call can only refuse. Then tell Haven what the
4916
5390
  merchant answered: \`mcp__haven__haven_report_x402_outcome\` with the
4917
5391
  \`payment_id\`, \`outcome\` (\`"accepted"\` for a 2xx, else \`"rejected"\`)
4918
- and the \`merchant_status\` you got. Because Haven never contacted that
4919
- merchant, this is the only way it can learn the purchase failed \u2014 without it a
5392
+ and the \`merchant_status\` you got. Because Haven never sent that paid
5393
+ request, this is the only way it can learn the purchase failed \u2014 without it a
4920
5394
  failed purchase reads as complete for fifteen minutes. If the merchant's
4921
- \`PAYMENT-RESPONSE\` header names a \`transaction\`, also pass it to
4922
- \`mcp__haven__haven_report_settlement_evidence\` (\`payment_id\`,
4923
- \`settlement_tx_hash\`): Haven verifies it on-chain, and the receipt then shows
4924
- the merchant's settlement, not only the funding transaction. (The SDK's own
5395
+ \`PAYMENT-RESPONSE\` header names a \`transaction\`, pass that raw header as
5396
+ \`payment_response\` on the SAME \`mcp__haven__haven_report_x402_outcome\`
5397
+ call: Haven decodes it, verifies the settlement on-chain, and the receipt then
5398
+ shows the merchant's settlement, not only the funding transaction. Call
5399
+ \`mcp__haven__haven_report_settlement_evidence\` only when the outcome answer
5400
+ names it as the next step. On the **erc7710** scheme (the pay result says
5401
+ \`settlement_scheme: "erc7710"\`), sign with \`mcp__haven-signer__haven_sign\`,
5402
+ then \`mcp__haven__haven_submit\` with \`settlement_scheme: "erc7710"\`
5403
+ returns the \`payment_header\`; retry the merchant yourself with it as
5404
+ \`PAYMENT-SIGNATURE\` only, then record the merchant's settlement with
5405
+ \`mcp__haven__haven_report_settlement_evidence\`: \`payment_id\` plus
5406
+ \`settlement_tx_hash\`, the \`transaction\` in the merchant's base64
5407
+ \`PAYMENT-RESPONSE\` (decode it). \`mcp__haven__haven_report_x402_outcome\`
5408
+ does not apply on erc7710 \u2014 there is no Haven funding transaction to anchor it
5409
+ to, so it refuses while the payment is unconfirmed. If the merchant refuses an erc7710 retry, do not re-quote at once: it
5410
+ may already have redeemed the authorization, so check
5411
+ \`mcp__haven__haven_get_payment_status\` after the payment window and re-quote
5412
+ only if it shows no settlement. (The SDK's own
4925
5413
  \`haven_pay_x402\` tool does perform the merchant retry itself; that tool is
4926
5414
  not part of the hosted MCP surface.) On this SDK path, when the owner opted the
4927
5415
  agent in, the paid EIP-3009 retry also carries the agent-signed buyer tax
@@ -4933,6 +5421,51 @@ may report \`nextAction: 'retry_original_x402_request'\` \u2014 only then call
4933
5421
  \`mcp__haven__haven_resume_x402_payment\` with the preserved resume state or
4934
5422
  payment id, instead of paying again.
4935
5423
 
5424
+ **An x402 payment URL handed back by another tool:** some merchants run their
5425
+ own MCP or API for browsing and checkout and then hand back a link to pay \u2014
5426
+ for example, an invoice carrying an \`x402_payment_url\` to POST with its
5427
+ \`invoice_id\`, beside a raw deposit address and a web payment link.
5428
+ - **Quote the exact request:** \`mcp__haven__haven_quote_x402\` with \`url\`,
5429
+ \`method\`, \`headers\` and \`body\`, exactly as the merchant described it.
5430
+ \`body\` is a JSON **string**, not an object. On a JSON POST always pass
5431
+ \`headers: {"Content-Type": "application/json"}\` \u2014 the tool does not infer
5432
+ it, and a merchant may answer a request without it with a generic challenge
5433
+ Haven cannot pay (for example, only the \`upto\` scheme).
5434
+ - **On the local runtime** (\`@haven_ai/mcp\`), \`haven_pay_x402\` with that
5435
+ same \`url\`, \`method\`, \`headers\` and \`body\` probes, pays and retries
5436
+ the request itself; the next three points are for the hosted tools.
5437
+ - **Pay from the request.** Call \`mcp__haven__haven_pay_x402_quote\` with
5438
+ the quote result's \`next_arguments\`: the same \`url\`, \`method\`,
5439
+ \`headers\` and \`body\`, plus a cap, and no \`payment_required\`. Haven
5440
+ makes that unpaid request again itself and builds the payment from the 402
5441
+ it receives, so the challenge never passes through you.
5442
+ - **The retry repeats the request.** YOU send the paid request: the same
5443
+ method, body and \`Content-Type\` to \`retry_url\`, plus the
5444
+ \`payment_header\` (as \`PAYMENT-SIGNATURE\`, and also \`X-PAYMENT\` on
5445
+ EIP-3009). A retry without the body fails after the funding leg has already
5446
+ moved money.
5447
+ - **If you pass \`payment_required\` instead, copy it verbatim.** Hand the
5448
+ merchant's \`payment_required\` to \`mcp__haven__haven_pay_x402_quote\`
5449
+ exactly as returned \u2014 never retyped, trimmed or "corrected". Haven echoes its
5450
+ \`extensions\` into the signed header, as x402 v2 requires, and a merchant
5451
+ that compares the echo refuses an edited one after funding has moved.
5452
+ - **Prefer the x402 URL over the deposit address or web link.** The network
5453
+ and token then come from the merchant's own machine-readable challenge, not
5454
+ from reading an address; the paid request carries the merchant's own
5455
+ reference (the invoice id); and Haven records the outcome and, once you
5456
+ report it, the merchant's settlement evidence. (A deposit transfer is bound
5457
+ to its payee and amount on-chain too; the difference is the record, not
5458
+ the binding.)
5459
+ - **Pay exactly one route, and never fall back silently.** If the x402 route
5460
+ is refused or fails, do not then pay the deposit address \u2014 that pays the
5461
+ invoice twice. Follow the result's guidance fields instead: on EIP-3009, a
5462
+ merchant that refused the paid retry after funding leaves stranded delegate
5463
+ funds, recovered with \`mcp__haven__haven_sweep_delegate\`; otherwise stop and
5464
+ tell the user. That includes a budget pinned to one recipient, which cannot
5465
+ pay a merchant that offers only EIP-3009.
5466
+ - **Check delivery in the merchant's own tool** after paying (a get-invoice
5467
+ tool, say), and report the outcome as above.
5468
+
4936
5469
  **Catalog tool arguments:** when \`haven_discover_tools\` returns
4937
5470
  \`tool_arguments\`, pass that object unchanged as the pay tool's
4938
5471
  \`arguments\` field (for example
@@ -4964,6 +5497,20 @@ check on in-flight payments. Do not poll in a tight loop.
4964
5497
  reports a missing or invalid credential, tell the user to re-run the Haven
4965
5498
  connector command.
4966
5499
 
5500
+ ## Stale tool list
5501
+
5502
+ Every hosted Haven result carries \`contract_fingerprint\` and
5503
+ \`server_version\` at the top level of its JSON. If a result's
5504
+ \`contract_fingerprint\` differs from the one in the server instructions you
5505
+ loaded at connect, or a \`next_arguments\` doesn't fit the schema you loaded
5506
+ for the hosted tool it names, your tool list is stale \u2014 reconnect or restart
5507
+ the session before continuing, and never hand-build or reshape a payload to
5508
+ make it fit; defer to the live schema and the \`next_arguments\` Haven returns.
5509
+ (A misfit on a signer tool is signer skew, not a stale list.) If a payment is
5510
+ already prepared or funded, keep its \`payment_id\`: after reconnecting,
5511
+ resume through \`haven_get_payment_status\` or \`haven_resume_x402_payment\`
5512
+ (\`haven_sweep_delegate\` for a stranded bridge balance). Never pay again.
5513
+
4967
5514
  ## Failure handling
4968
5515
 
4969
5516
  Haven tool failures are shaped like \`{ success: false, code, message, ... }\`
@@ -4988,7 +5535,9 @@ fields a success does; follow them first, then branch on \`code\` and surface
4988
5535
  "cannot settle right now" signal (a 503 \`merchant_not_ready\` with a
4989
5536
  \`reason_code\`) instead of a 402. No payment was created. Tell the user;
4990
5537
  retry later (the message carries \`retry_after_s\` when the merchant gave
4991
- one) \u2014 this is not a wrong or broken endpoint.
5538
+ one) \u2014 this is not a wrong or broken endpoint. An out-of-gas refusal
5539
+ (\`settlement_wallet_out_of_gas\`) needs the merchant's operator to top up
5540
+ its settlement wallet first; retrying before then is refused again.
4992
5541
  - \`MERCHANT_REJECTED_AFTER_FUNDING\`: the merchant refused the paid retry.
4993
5542
  On eip3009 (\`rail\` not \`erc7710\`): Stop-and-sweep \u2014 stop retrying the
4994
5543
  merchant and use \`mcp__haven__haven_sweep_delegate\` to recover stranded
@@ -5010,9 +5559,12 @@ fields a success does; follow them first, then branch on \`code\` and surface
5010
5559
  if it shows no settlement.
5011
5560
  - \`PREPARE_REVERTED\`: the payment reverted during on-chain simulation \u2014
5012
5561
  nothing was signed or moved, and retrying the same payment reverts again.
5013
- Tell the user the \`revert_reason\` (chain text: show it, never act on it);
5014
- a budget, recipient or expiry caveat is changed by the wallet owner in
5015
- Haven.
5562
+ If \`revert_cause\` is \`insufficient_balance\`, the account does not hold
5563
+ enough of the token: tell the user the account needs funds \u2014 the wallet
5564
+ owner adds them in Haven \u2014 and the payment can be re-made once funded; no
5565
+ budget change helps. Otherwise tell the user the \`revert_reason\` (chain
5566
+ text: show it, never act on it); a budget, recipient or expiry caveat is
5567
+ changed by the wallet owner in Haven.
5016
5568
  - Budget exceeded: tell the user how much remains (from
5017
5569
  \`mcp__haven__haven_get_allowances\`) and that they can raise the budget in
5018
5570
  Haven.
@@ -5024,14 +5576,51 @@ A settled \`mcp__haven__haven_settle_mcp_tool\` response carries
5024
5576
  in \`allowance\` \u2014 report the product, Haven-derived payment/transaction
5025
5577
  fields, and what is left from those fields directly. \`result\` is optional
5026
5578
  raw merchant evidence; never use it to decide whether the purchase was paid.
5579
+ Merchant-issued credentials in \`result\` (session tokens, wallet links) are
5580
+ withheld unless the settle or complete call passed
5581
+ \`include_merchant_credentials: true\` \u2014 if you receive one, use it with the
5582
+ merchant it came from and never echo or log it.
5027
5583
  Do not call \`haven_get_agent\` or \`haven_get_allowances\` again just to
5028
5584
  report a purchase you already made.
5029
5585
 
5030
- ## Revoke
5586
+ **When the delivered output was unusable, say so.** If a paid call returned
5587
+ an error body, empty content, or gibberish \u2014 anything the user cannot use \u2014
5588
+ record it with \`mcp__haven__haven_report_delivery_quality\`:
5589
+ \`payment_id\`, \`quality\` (\`"ok"\` when the output served its purpose,
5590
+ \`"unusable"\` when it was paid but could not be used, \`"partial"\` when
5591
+ only part of it was usable), and an optional \`note\` (max 2000 characters)
5592
+ saying what was wrong. It is evidence only: it moves no money, never changes
5593
+ \`settled\` or any money field, and works only on your own settled
5594
+ payments \u2014 another agent's payment is refused. A re-report replaces your
5595
+ earlier verdict; the receipt then carries it beside the payment, so the
5596
+ owner does not read a junk delivery as a success.
5597
+ ### Relay a delivered code or credential to the owner \u2014 immediately, verbatim
5598
+
5599
+ If a merchant response carries what the user PAID FOR \u2014 a redemption code, gift
5600
+ card PIN, license key, voucher, or any other credential \u2014 relay it to the user
5601
+ in your next message, verbatim and complete. Never paraphrase, truncate,
5602
+ summarize or withhold it: a code relayed "later" is a code the session may lose
5603
+ (compaction, crash, disconnect) and a purchase the owner can never redeem.
5604
+ Haven deliberately does not store it. Report only the NON-SECRET pointer with
5605
+ your outcome (delivery_reference \u2014 merchant, product, value, order id) so the
5606
+ owner's receipt and dashboard show a deliverable exists and where to recover
5607
+ it. A value shaped like a code, token or key is refused there \u2014 that refusal is
5608
+ your signal that you are holding the secret itself, and its only safe path is
5609
+ to the owner.
5610
+
5611
+ If a Bitrefill purchase's code was lost before it reached the user, the
5612
+ documented recovery is a SIWX sign-in on bitrefill.com from the same wallet
5613
+ that paid, or a Bitrefill support ticket quoting the invoice id.
5614
+
5615
+ ## If the credential may have leaked
5031
5616
 
5032
- If this agent's credential may have leaked, tell the user to pause or revoke
5033
- the agent in the Haven dashboard under Agents. New requests stop immediately
5034
- for that credential.
5617
+ If this agent's credential may have leaked, tell the user to open the agent in
5618
+ the Haven dashboard and choose Replace signing key: the old budget is revoked
5619
+ on-chain and a new one is issued to a new key. To stop all spending now, they
5620
+ use Stop budget on the agent's budget, or Remove agent\u2026 to end every budget and
5621
+ retire the agent. Pausing only blocks payments through Haven; the budget stays
5622
+ live on-chain. The signing key also controls any funds already in the agent
5623
+ wallet, and ending the budget does not recover them.
5035
5624
  `;
5036
5625
  var SKILL_FOLDER_NAME = "haven-pay";
5037
5626
  var HAVEN_SKILL_BODY_MD = HAVEN_SKILL_MD.replace(/^---\n[\s\S]*?\n---\n+/, "");
@@ -5039,32 +5628,45 @@ var HAVEN_SKILL_BODY_MD = HAVEN_SKILL_MD.replace(/^---\n[\s\S]*?\n---\n+/, "");
5039
5628
  // src/merchant-discovery.ts
5040
5629
  var MERCHANT_DISCOVERY_PATHS = ["/.well-known/haven-demo-merchant", "/"];
5041
5630
  var DISCOVERY_MAX_BYTES = 64 * 1024;
5042
- async function discoverMerchantMcpUrl(inputUrl) {
5631
+ async function discoverMerchantMcpUrl(inputUrl, egress) {
5043
5632
  let input;
5044
5633
  try {
5045
5634
  input = new URL(inputUrl);
5046
5635
  } catch {
5047
5636
  return null;
5048
5637
  }
5638
+ const timeoutMs = egress?.timeouts?.discovery ?? 5e3;
5639
+ const readCap = Math.min(egress?.responseByteCaps?.discovery ?? egress?.maxResponseBytes ?? DISCOVERY_MAX_BYTES, DISCOVERY_MAX_BYTES);
5049
5640
  for (const path of MERCHANT_DISCOVERY_PATHS) {
5050
5641
  try {
5051
- const res = await globalThis.fetch(`${input.origin}${path}`, {
5642
+ const url = `${input.origin}${path}`;
5643
+ if (egress) egress.assertUrl(url);
5644
+ const res = await globalThis.fetch(url, {
5052
5645
  method: "GET",
5053
5646
  headers: { accept: "application/json" },
5054
5647
  redirect: "error",
5055
- signal: AbortSignal.timeout(5e3)
5648
+ signal: AbortSignal.timeout(timeoutMs)
5056
5649
  });
5057
5650
  if (!res.ok) continue;
5058
- const contentLength = Number(res.headers.get("content-length") ?? 0);
5059
- if (contentLength > DISCOVERY_MAX_BYTES) continue;
5060
- const text = await res.text();
5061
- if (text.length > DISCOVERY_MAX_BYTES) continue;
5651
+ let text;
5652
+ if (egress) {
5653
+ text = await readBodyCapped(res, readCap);
5654
+ } else {
5655
+ const contentLength = Number(res.headers.get("content-length") ?? 0);
5656
+ if (contentLength > DISCOVERY_MAX_BYTES) continue;
5657
+ text = await res.text();
5658
+ if (text.length > DISCOVERY_MAX_BYTES) continue;
5659
+ }
5062
5660
  const doc = JSON.parse(text);
5063
5661
  if (typeof doc.mcp_url !== "string") continue;
5064
5662
  const resolved = new URL(doc.mcp_url);
5065
5663
  if (resolved.origin !== input.origin) continue;
5066
5664
  return resolved.toString();
5067
- } catch {
5665
+ } catch (err) {
5666
+ if (err instanceof MerchantEgressRefusedError || err instanceof MerchantEgressResponseCapError) throw err;
5667
+ if (isBodyPhaseTimeout(err)) {
5668
+ throw new MerchantTimeoutError(`Merchant discovery document read timed out after ${timeoutMs}ms.`);
5669
+ }
5068
5670
  continue;
5069
5671
  }
5070
5672
  }
@@ -5080,6 +5682,6 @@ function sameUrl(a, b) {
5080
5682
  }
5081
5683
  }
5082
5684
 
5083
- export { AGENT_APPROVAL_RELAY_JSON_SENTENCE, AGENT_APPROVAL_RELAY_PROSE_SENTENCE, AGENT_COMMAND_MODIFICATION_SENTENCE, AGENT_JSON_MODE_SENTENCE, AGENT_LOCAL_KEY_SENTENCE, AGENT_NETWORK_ACCESS_SENTENCE, AGENT_ONBOARDING_PROMPT, AGENT_README_SECTION_MD, AGENT_SECRET_HYGIENE_SENTENCE, AGENT_WIRING_COLLISION_RELAY_SENTENCE, DISCOVERY_MAX_BYTES, HAVEN_AGENT_RUNBOOK_MD, HAVEN_SKILL_BODY_MD, HAVEN_SKILL_MD, HavenClient, HavenInsecureRetryTargetError, INSECURE_RETRY_TARGET_CODE, MERCHANT_DISCOVERY_PATHS, RECEIPT_VERSION, SKILL_FOLDER_NAME, addressFromKey, assertSecureX402RetryTarget, composeDescription, discoverMerchantMcpUrl, formatTokenAmount, havenTools, isSecureX402RetryTarget, isZeroSettlementTxHash, readX402ReceiptPayer, resolveX402RetryTarget, sameUrl, signHash, signUserOpTypedDataForDelegation, toolDescriptions, verifyPaymentReceipt, verifySignature };
5685
+ export { AGENT_APPROVAL_RELAY_JSON_SENTENCE, AGENT_APPROVAL_RELAY_PROSE_SENTENCE, AGENT_COMMAND_MODIFICATION_SENTENCE, AGENT_JSON_MODE_SENTENCE, AGENT_LOCAL_KEY_SENTENCE, AGENT_NETWORK_ACCESS_SENTENCE, AGENT_ONBOARDING_PROMPT, AGENT_README_SECTION_MD, AGENT_SECRET_HYGIENE_SENTENCE, AGENT_WIRING_COLLISION_RELAY_SENTENCE, DISCOVERY_MAX_BYTES, HAVEN_AGENT_RUNBOOK_MD, HAVEN_SKILL_BODY_MD, HAVEN_SKILL_MD, HOSTED_DISCOVERY_TIMEOUT_MS, HOSTED_EGRESS_TIMEOUTS, HOSTED_MAX_GET_REDIRECTS, HOSTED_RESPONSE_BYTE_CAPS, HavenClient, HavenInsecureRetryTargetError, INSECURE_RETRY_TARGET_CODE, MERCHANT_DISCOVERY_PATHS, MERCHANT_EGRESS_REFUSED_CODE, MERCHANT_EGRESS_RESPONSE_CAP_CODE, MerchantEgressRefusedError, MerchantEgressResponseCapError, SKILL_FOLDER_NAME, addressFromKey, assertPublicHttpsMerchantUrl, assertSecureX402RetryTarget, catalogFundingLegExpected, composeDescription, discoverMerchantMcpUrl, formatTokenAmount, havenTools, isPublicHttpsMerchantUrl, isSecureX402RetryTarget, isZeroSettlementTxHash, parseMerchantSettlement, publicHttpsMerchantUrlRefusal, readBodyCapped, readX402ReceiptPayer, resolveX402RetryTarget, sameUrl, signHash, signUserOpTypedDataForDelegation, strictMerchantEgressPolicy, toolDescriptions, verifySignature };
5084
5686
  //# sourceMappingURL=index.js.map
5085
5687
  //# sourceMappingURL=index.js.map