@haven_ai/sdk 0.9.0-alpha.0 → 0.11.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 (38) hide show
  1. package/README.md +9 -6
  2. package/dist/{base64-BZDPJ8M1.d.ts → base64-D4VZhgUg.d.ts} +11 -3
  3. package/dist/{base64-Cyxhsvqd.d.cts → base64-DR4Gl8cD.d.cts} +11 -3
  4. package/dist/{chunk-VRKWNJ7H.js → chunk-34YJQFCM.js} +3 -3
  5. package/dist/{chunk-VRKWNJ7H.js.map → chunk-34YJQFCM.js.map} +1 -1
  6. package/dist/{chunk-7UTWSJC5.cjs → chunk-DXBAN4SN.cjs} +4 -4
  7. package/dist/{chunk-7UTWSJC5.cjs.map → chunk-DXBAN4SN.cjs.map} +1 -1
  8. package/dist/{chunk-J7N4LUZZ.js → chunk-I7SL4A6X.js} +23 -5
  9. package/dist/chunk-I7SL4A6X.js.map +1 -0
  10. package/dist/{chunk-ZDKNPNI3.cjs → chunk-IKQIIMM6.cjs} +41 -35
  11. package/dist/chunk-IKQIIMM6.cjs.map +1 -0
  12. package/dist/{chunk-4Z6ATE44.cjs → chunk-MZKLAHBK.cjs} +23 -5
  13. package/dist/chunk-MZKLAHBK.cjs.map +1 -0
  14. package/dist/{chunk-424S4QMG.js → chunk-ZDJ55LR7.js} +10 -6
  15. package/dist/chunk-ZDJ55LR7.js.map +1 -0
  16. package/dist/edge.cjs +99 -91
  17. package/dist/edge.d.cts +4 -4
  18. package/dist/edge.d.ts +4 -4
  19. package/dist/edge.js +4 -4
  20. package/dist/index.cjs +861 -459
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +88 -27
  23. package/dist/index.d.ts +88 -27
  24. package/dist/index.js +541 -148
  25. package/dist/index.js.map +1 -1
  26. package/dist/{receipt-WkXHZGbu.d.ts → receipt-CbD8L0WL.d.ts} +1 -1
  27. package/dist/{receipt-D4Roey3D.d.cts → receipt-_bWCELEH.d.cts} +1 -1
  28. package/dist/test-support.cjs +21 -21
  29. package/dist/test-support.d.cts +2 -2
  30. package/dist/test-support.d.ts +2 -2
  31. package/dist/test-support.js +2 -2
  32. package/dist/{types-CrszUJ7Z.d.cts → types-DKmvjXK6.d.cts} +96 -12
  33. package/dist/{types-CrszUJ7Z.d.ts → types-DKmvjXK6.d.ts} +96 -12
  34. package/package.json +1 -1
  35. package/dist/chunk-424S4QMG.js.map +0 -1
  36. package/dist/chunk-4Z6ATE44.cjs.map +0 -1
  37. package/dist/chunk-J7N4LUZZ.js.map +0 -1
  38. package/dist/chunk-ZDKNPNI3.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
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, encodeBase64Json, x402V2PaymentEnvelope, isErc7710Option, buildSignedTaxDeclaration, encodeTaxDeclarationHeader, selectX402SettlementScheme, selectErc7710PaymentOption, x402AssetTransferMethod, resolveTokenFromAddress, normalizePaymentRequired, X_TAX_DECLARATION_HEADER } from './chunk-424S4QMG.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, x402PaymentHeaderNamesFor, x402RetryHeadersFor, x402V2PaymentEnvelope } from './chunk-424S4QMG.js';
3
- import { verifyPaymentReceipt } from './chunk-VRKWNJ7H.js';
4
- export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-VRKWNJ7H.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-J7N4LUZZ.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-J7N4LUZZ.js';
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-ZDJ55LR7.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-ZDJ55LR7.js';
3
+ import { verifyPaymentReceipt } from './chunk-34YJQFCM.js';
4
+ export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-34YJQFCM.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-I7SL4A6X.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-I7SL4A6X.js';
7
7
  import { hashTypedData } from 'viem';
8
8
  import { ethers } from 'ethers';
9
9
  import { AsyncLocalStorage } from 'async_hooks';
@@ -542,12 +542,30 @@ var MerchantEgressRefusedError = class extends HavenError {
542
542
  url;
543
543
  refusal;
544
544
  beforeRequest;
545
- constructor(message, url, refusal, beforeRequest) {
546
- super(message, MERCHANT_EGRESS_REFUSED_CODE, 400);
545
+ /** #4257: the redirect status that was not followed, when a redirect was refused. */
546
+ redirectStatus;
547
+ /**
548
+ * #4257: set only when `url` IS the redirect target (a cross-origin body
549
+ * re-send, a spent hop budget, a hop `assertUrl` refused). A refusal that
550
+ * stopped at the answering URL itself (a non-GET redirect, a missing or
551
+ * unresolvable Location) leaves it unset, and `url` is that answering URL.
552
+ */
553
+ redirectTarget;
554
+ /**
555
+ * #4257: set when the refusal interrupted a PAID delivery and the local
556
+ * completion has re-thrown it with the payment it belongs to — the funded,
557
+ * undelivered state, never "nothing happened".
558
+ */
559
+ settlementScheme;
560
+ constructor(message, url, refusal, beforeRequest, extra = {}) {
561
+ super(message, MERCHANT_EGRESS_REFUSED_CODE, 400, extra.paymentId);
547
562
  this.name = "MerchantEgressRefusedError";
548
563
  this.url = url;
549
564
  this.refusal = refusal;
550
565
  this.beforeRequest = beforeRequest;
566
+ this.redirectStatus = extra.redirectStatus;
567
+ this.redirectTarget = extra.redirectTarget;
568
+ this.settlementScheme = extra.settlementScheme;
551
569
  }
552
570
  };
553
571
  var MERCHANT_EGRESS_RESPONSE_CAP_CODE = "MERCHANT_EGRESS_RESPONSE_CAP";
@@ -662,6 +680,29 @@ var MCP_X402_PAYMENT_META_KEY = "x402/payment";
662
680
  var MCP_X402_PAYMENT_RESPONSE_META_KEY = "x402/payment-response";
663
681
  var X402_RETRY_REJECTED_STATUS = 402;
664
682
  var SESSION_NOT_FOUND_NEXT_ACTION = "reinitialize_then_retry_same_payment_header";
683
+ var CROSS_ORIGIN_REDIRECT_HEADERS = /* @__PURE__ */ new Set([
684
+ "accept",
685
+ "accept-language",
686
+ "user-agent",
687
+ "x402-wallet"
688
+ ]);
689
+ function crossOriginRedirectHeaders(headers) {
690
+ const kept = new Headers();
691
+ for (const [name, value] of new Headers(headers)) {
692
+ if (CROSS_ORIGIN_REDIRECT_HEADERS.has(name)) kept.set(name, value);
693
+ }
694
+ return kept;
695
+ }
696
+ var NO_POLICY_MAX_REDIRECTS = 20;
697
+ var REQUEST_BODY_HEADERS = ["content-encoding", "content-language", "content-location", "content-type", "content-length"];
698
+ function redirectedInit(init, status) {
699
+ const method = (init.method ?? "GET").toUpperCase();
700
+ const toGet = (status === 301 || status === 302) && method === "POST" || status === 303 && method !== "GET" && method !== "HEAD";
701
+ if (!toGet) return init;
702
+ const headers = new Headers(init.headers);
703
+ for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
704
+ return { ...init, method: "GET", body: null, headers };
705
+ }
665
706
  var McpMerchantTransport = class {
666
707
  merchantTimeout;
667
708
  fetchImpl;
@@ -692,11 +733,24 @@ var McpMerchantTransport = class {
692
733
  }
693
734
  /** Fetch a merchant with a settlement-sized timeout and caller cancellation. */
694
735
  async fetch(url, init = {}, timeoutMs = this.merchantTimeout, maxBytes) {
736
+ return this.send(url, init, timeoutMs, maxBytes, false);
737
+ }
738
+ /**
739
+ * `fetch`, plus whether this request is the paid delivery (#4257). A paid
740
+ * delivery, or any body carrying an x402 payment in `_meta`, is never
741
+ * re-sent to another origin by a redirect that keeps the body; without a policy every other
742
+ * request follows that redirect as the platform would, headers reduced.
743
+ */
744
+ async send(url, init, timeoutMs, maxBytes, paidDelivery) {
745
+ const carriesPayment = paidDelivery || bodyCarriesX402Payment(init.body);
695
746
  if (!this.egress) {
696
747
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
697
748
  const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
698
749
  try {
699
- return await this.fetchImpl(url, { ...init, signal });
750
+ if (init.redirect === "manual" || init.redirect === "error") {
751
+ return await this.fetchImpl(url, { ...init, signal });
752
+ }
753
+ return await this.followRedirects(url, init, signal, void 0, carriesPayment);
700
754
  } catch (err) {
701
755
  if (timeoutSignal.aborted) {
702
756
  throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
@@ -704,87 +758,144 @@ var McpMerchantTransport = class {
704
758
  throw err;
705
759
  }
706
760
  }
707
- return this.fetchUnderPolicy(url, init, timeoutMs, maxBytes);
761
+ return this.fetchUnderPolicy(url, init, timeoutMs, maxBytes, carriesPayment);
708
762
  }
709
763
  /**
710
764
  * #3747: the policy path. Every URL — the initial request and each redirect
711
765
  * hop — is `assertUrl`-checked BEFORE it connects; redirects run `manual`
712
766
  * 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
767
+ * `maxGetRedirects` (a redirect on any other method is refused); a hop to
768
+ * another origin carries only `CROSS_ORIGIN_REDIRECT_HEADERS` (#4244);
769
+ * response bodies are capped WHILE being read, not after buffering. The timeout
715
770
  * signal spans the WHOLE redirect chain, and `MerchantTimeoutError`
716
771
  * classification is preserved.
717
772
  */
718
- async fetchUnderPolicy(url, init, timeoutMs, maxBytes) {
773
+ async fetchUnderPolicy(url, init, timeoutMs, maxBytes, carriesPayment) {
719
774
  const egress = this.egress;
720
775
  try {
721
776
  egress.assertUrl(url);
722
777
  } catch (err) {
723
778
  throw asRefusal(err, url, true);
724
779
  }
725
- const method = (init.method ?? "GET").toUpperCase();
726
780
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
727
781
  const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
728
- const maxRedirects = egress.maxGetRedirects ?? 0;
782
+ let finalUrl = url;
783
+ try {
784
+ const response = await this.followRedirects(url, init, signal, egress, carriesPayment, (hopUrl) => {
785
+ finalUrl = hopUrl;
786
+ });
787
+ return this.capResponse(response, finalUrl, maxBytes);
788
+ } catch (err) {
789
+ if (timeoutSignal.aborted) {
790
+ throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${finalUrl}`);
791
+ }
792
+ throw err;
793
+ }
794
+ }
795
+ /**
796
+ * The redirect follower both paths share (#3747, #4244, #4257). Redirects
797
+ * run `manual` and every hop is decided here:
798
+ *
799
+ * - Under a `policy`, every hop is `assertUrl`-checked before it connects,
800
+ * only GET redirects are followed (any other method is refused), up to
801
+ * `maxGetRedirects`; a missing or unresolvable Location is refused.
802
+ * - Without one, the platform's rules are kept: up to
803
+ * `NO_POLICY_MAX_REDIRECTS` hops, the Fetch spec's method/body rewrite,
804
+ * and a 3xx with no Location returned as the answer.
805
+ * - On both, a hop to another origin carries only
806
+ * `CROSS_ORIGIN_REDIRECT_HEADERS`, never restored on a later hop.
807
+ * - A hop that would re-send a request body to another origin is refused
808
+ * under a policy (which follows GET alone, so this is unreachable there),
809
+ * and without one only when `carriesPayment`: the paid delivery, or a body
810
+ * carrying an x402 payment in `_meta` (#4257). Any other body-keeping redirect is
811
+ * followed with its body, as the platform did, headers reduced.
812
+ *
813
+ * `onHop` sees every URL a request goes to, so the caller can name the hop
814
+ * it was on when the chain stops.
815
+ */
816
+ async followRedirects(url, init, signal, policy, carriesPayment, onHop) {
817
+ const maxRedirects = policy ? policy.maxGetRedirects ?? 0 : NO_POLICY_MAX_REDIRECTS;
729
818
  let currentUrl = url;
819
+ let hopInit = init;
730
820
  let hops = 0;
731
- try {
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;
821
+ while (true) {
822
+ onHop?.(currentUrl);
823
+ const response = await this.fetchImpl(currentUrl, {
824
+ ...hopInit,
825
+ redirect: init.redirect === "error" ? "error" : "manual",
826
+ signal
827
+ });
828
+ if (!isRedirectStatus(response.status)) return response;
829
+ const method = (hopInit.method ?? "GET").toUpperCase();
830
+ if (policy && method !== "GET") {
831
+ throw new MerchantEgressRefusedError(
832
+ `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.`,
833
+ currentUrl,
834
+ "redirect_on_non_get",
835
+ false,
836
+ { redirectStatus: response.status }
837
+ );
838
+ }
839
+ const location = response.headers.get("location");
840
+ if (!location) {
841
+ if (!policy) return response;
842
+ throw new MerchantEgressRefusedError(
843
+ `Hosted egress policy refuses the ${response.status} redirect from ${currentUrl}: it carries no Location header, so there is no checked target to follow.`,
844
+ currentUrl,
845
+ "redirect_missing_location",
846
+ false,
847
+ { redirectStatus: response.status }
848
+ );
849
+ }
850
+ let next;
851
+ try {
852
+ next = new URL(location, currentUrl).toString();
853
+ } catch {
854
+ if (!policy) throw new TypeError(`fetch failed: the redirect from ${currentUrl} has an unresolvable Location header`);
855
+ throw new MerchantEgressRefusedError(
856
+ `Hosted egress policy refuses the redirect from ${currentUrl}: its Location header is not a resolvable URL.`,
857
+ currentUrl,
858
+ "redirect_invalid_location",
859
+ false,
860
+ { redirectStatus: response.status }
861
+ );
862
+ }
863
+ if (!policy && !/^https?:$/.test(new URL(next).protocol)) {
864
+ throw new TypeError(`fetch failed: the redirect from ${currentUrl} names a non-HTTP(S) URL`);
865
+ }
866
+ if (hops >= maxRedirects) {
867
+ if (!policy) throw new TypeError(`fetch failed: redirect count exceeded (${maxRedirects}) at ${currentUrl}`);
868
+ throw new MerchantEgressRefusedError(
869
+ `Hosted egress policy refuses the redirect from ${currentUrl} to ${next}: the ${maxRedirects}-redirect budget for this request is spent.`,
870
+ next,
871
+ "redirect_budget_exceeded",
872
+ false,
873
+ { redirectStatus: response.status, redirectTarget: next }
874
+ );
875
+ }
876
+ if (policy) {
757
877
  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
- );
878
+ policy.assertUrl(next);
879
+ } catch (err) {
880
+ throw asRefusal(err, next, false, { redirectStatus: response.status, redirectTarget: next });
766
881
  }
767
- if (hops >= maxRedirects) {
882
+ }
883
+ hopInit = redirectedInit(hopInit, response.status);
884
+ if (new URL(next).origin !== new URL(currentUrl).origin) {
885
+ if (hopInit.body != null && (policy || carriesPayment)) {
768
886
  throw new MerchantEgressRefusedError(
769
- `Hosted egress policy refuses the redirect from ${currentUrl} to ${next}: the ${maxRedirects}-redirect budget for this request is spent.`,
887
+ carriesPayment ? `The ${response.status} redirect of a paid request from ${currentUrl} to ${next} was not followed: following it would re-send the paid request to a different origin. The paid request, payment header and body included, was already delivered to ${currentUrl}; nothing was sent to ${next}.` : `Refusing the ${response.status} redirect from ${currentUrl} to ${next}: it would re-send the request body to a different origin. The request was already sent to the first URL; nothing was sent to the redirect target.`,
770
888
  next,
771
- "redirect_budget_exceeded",
772
- false
889
+ "redirect_cross_origin_body",
890
+ false,
891
+ { redirectStatus: response.status, redirectTarget: next }
773
892
  );
774
893
  }
775
- try {
776
- egress.assertUrl(next);
777
- } catch (err) {
778
- throw asRefusal(err, next, false);
779
- }
780
- hops++;
781
- currentUrl = next;
782
- }
783
- } catch (err) {
784
- if (timeoutSignal.aborted) {
785
- throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${currentUrl}`);
894
+ hopInit = { ...hopInit, headers: crossOriginRedirectHeaders(hopInit.headers) };
786
895
  }
787
- throw err;
896
+ await response.body?.cancel().catch(() => void 0);
897
+ hops++;
898
+ currentUrl = next;
788
899
  }
789
900
  }
790
901
  /**
@@ -962,7 +1073,7 @@ var McpMerchantTransport = class {
962
1073
  if (send.includes(name)) headers.set(name, paymentHeader);
963
1074
  else headers.delete(name);
964
1075
  }
965
- return this.fetch(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader), this.budgetFor("delivery"), this.capFor("delivery"));
1076
+ return this.send(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader), this.budgetFor("delivery"), this.capFor("delivery"), true);
966
1077
  }
967
1078
  /**
968
1079
  * #3171: deliver the payment; if the merchant answers its unknown-session
@@ -1099,6 +1210,27 @@ function withMcpPaymentMeta(init, paymentHeader) {
1099
1210
  function encodeMcpSettlementReceipt(settlement) {
1100
1211
  return encodeBase64Json(settlement);
1101
1212
  }
1213
+ function bodyCarriesX402Payment(body) {
1214
+ let text;
1215
+ if (typeof body === "string") text = body;
1216
+ else if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
1217
+ try {
1218
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
1219
+ } catch {
1220
+ return false;
1221
+ }
1222
+ } else return false;
1223
+ let request;
1224
+ try {
1225
+ request = JSON.parse(text);
1226
+ } catch {
1227
+ return false;
1228
+ }
1229
+ return Array.isArray(request) ? request.some(messageCarriesX402Payment) : messageCarriesX402Payment(request);
1230
+ }
1231
+ function messageCarriesX402Payment(message) {
1232
+ return isRecord(message) && isRecord(message.params) && isRecord(message.params._meta) && message.params._meta[MCP_X402_PAYMENT_META_KEY] !== void 0;
1233
+ }
1102
1234
  function parseJsonRpcToolsCall(body) {
1103
1235
  if (typeof body !== "string") return void 0;
1104
1236
  let request;
@@ -1137,10 +1269,10 @@ function isRecord(value) {
1137
1269
  function isRedirectStatus(status) {
1138
1270
  return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
1139
1271
  }
1140
- function asRefusal(err, url, beforeRequest) {
1272
+ function asRefusal(err, url, beforeRequest, redirect) {
1141
1273
  if (!(err instanceof MerchantEgressRefusedError)) return err;
1142
- if (err.beforeRequest === beforeRequest) return err;
1143
- return new MerchantEgressRefusedError(err.message, url, err.refusal, beforeRequest);
1274
+ if (err.beforeRequest === beforeRequest && !redirect) return err;
1275
+ return new MerchantEgressRefusedError(err.message, url, err.refusal, beforeRequest, redirect);
1144
1276
  }
1145
1277
  async function captureMerchantResponse(response) {
1146
1278
  const merchant_body = await response.text().catch((err) => {
@@ -1226,6 +1358,19 @@ function formatTokenAmount(atomic, decimals) {
1226
1358
  const fracPart = digits.slice(digits.length - decimals).replace(/0+$/, "");
1227
1359
  return fracPart ? `${intPart}.${fracPart}` : intPart;
1228
1360
  }
1361
+ function mapSupersession(raw) {
1362
+ if (typeof raw !== "object" || raw === null) return null;
1363
+ const { agent_id: replacementAgentId, superseded_at: supersededAt } = raw;
1364
+ if (typeof replacementAgentId !== "string" || replacementAgentId === "") return null;
1365
+ if (typeof supersededAt !== "string" || supersededAt === "") return null;
1366
+ return {
1367
+ replacementAgentId,
1368
+ supersededAt,
1369
+ 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.`,
1370
+ nextAction: AgentPaymentNextAction.StopAndTellUser,
1371
+ 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."
1372
+ };
1373
+ }
1229
1374
  function mapTaskBudget(raw) {
1230
1375
  return {
1231
1376
  id: raw.id,
@@ -1247,6 +1392,7 @@ function mapTaskBudget(raw) {
1247
1392
  status: raw.status,
1248
1393
  expiresAt: raw.expires_at,
1249
1394
  isExpired: raw.is_expired,
1395
+ ...raw.is_live === void 0 ? {} : { isLive: raw.is_live },
1250
1396
  createdAt: raw.created_at,
1251
1397
  openedAt: raw.opened_at,
1252
1398
  closedAt: raw.closed_at,
@@ -1400,7 +1546,8 @@ var AccountReads = class {
1400
1546
  * an agent can still act on, and read-by-id (`haven_get_task_budget`) is
1401
1547
  * the any-status surface for everything else:
1402
1548
  *
1403
- * - `closing` ALWAYS stays visible, expired or not — the close submit is
1549
+ * - `closing` stays visible until its expiry (#3773; an expired `closing`
1550
+ * row drops, #3923) — the close submit is
1404
1551
  * in flight and the signer's close refusal tells the caller to re-check
1405
1552
  * exactly this row's status;
1406
1553
  * - `closed` rows drop (terminal — `closed_at` names what happened, and
@@ -1409,10 +1556,14 @@ var AccountReads = class {
1409
1556
  * budget reserves nothing and pays nothing);
1410
1557
  * - unexpired `pending` and `open` rows stay.
1411
1558
  *
1412
- * The same filter also runs here, client-side: it is what keeps the list
1413
- * bounded against a backend that predates `status=live`, which refuses the
1414
- * value with a 400 — that refusal (and only that) falls back to
1415
- * `status=all`. A row whose wire carries
1559
+ * A filter also runs here, client-side. A row carrying the backend's
1560
+ * `is_live` (#3922) is kept exactly when it is true — the server's own
1561
+ * `status=live` verdict. A row without it (an older backend) is derived by
1562
+ * the same rule: `closing` stays until its expiry, then drops (#3923), as
1563
+ * the backend does since #3773. That is
1564
+ * what keeps the list bounded against a backend that predates `status=live`,
1565
+ * which refuses the value with a 400 — that refusal (and only that) falls
1566
+ * back to `status=all`. A row whose wire carries
1416
1567
  * no `is_expired` is treated as unexpired — the degraded read errs toward
1417
1568
  * KEEPING rows, never toward hiding one the agent could still act on.
1418
1569
  */
@@ -1426,7 +1577,9 @@ var AccountReads = class {
1426
1577
  });
1427
1578
  const rows = raw?.task_budgets;
1428
1579
  if (!Array.isArray(rows)) return [];
1429
- return rows.map((row) => mapTaskBudget(row)).filter((tb) => tb.status === "closing" || !tb.isExpired && tb.status !== "closed").map((tb) => summarizeTaskBudget(tb));
1580
+ return rows.map((row) => mapTaskBudget(row)).filter(
1581
+ (tb) => tb.isLive !== void 0 ? tb.isLive : !tb.isExpired && tb.status !== "closed"
1582
+ ).map((tb) => summarizeTaskBudget(tb));
1430
1583
  } catch {
1431
1584
  return [];
1432
1585
  }
@@ -1461,6 +1614,7 @@ var AccountReads = class {
1461
1614
  accountAddress: raw.account_address,
1462
1615
  delegateAddress: raw.delegate_address,
1463
1616
  chainId: raw.chain_id,
1617
+ supersededBy: mapSupersession(raw.superseded_by),
1464
1618
  allowances: raw.allowances.map((allowance) => ({
1465
1619
  id: allowance.id,
1466
1620
  tokenAddress: allowance.token_address,
@@ -1633,7 +1787,8 @@ var AccountReads = class {
1633
1787
  accountAddress: raw.account_address,
1634
1788
  delegateAddress: raw.delegate_address,
1635
1789
  chainId: raw.chain_id,
1636
- executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
1790
+ executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy",
1791
+ supersededBy: mapSupersession(raw.superseded_by)
1637
1792
  };
1638
1793
  }
1639
1794
  };
@@ -2089,7 +2244,7 @@ var X402FundingLeg = class {
2089
2244
  );
2090
2245
  }
2091
2246
  }
2092
- const paymentHeader = await this.createPaymentHeader(paymentRequired, option);
2247
+ const paymentHeader = await this.createPaymentHeader(paymentRequired, option, raw.payment_id ?? state?.paymentId);
2093
2248
  if (raw.success && raw.tx_hash) {
2094
2249
  const receipt2 = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
2095
2250
  this.cacheReceipt(idempotencyKey, paymentHeader, receipt2, taskBudgetId, subBudgetId);
@@ -2124,17 +2279,44 @@ var X402FundingLeg = class {
2124
2279
  return receipt;
2125
2280
  }
2126
2281
  // ── Header minting ───────────────────────────────────────────────
2127
- async createPaymentHeader(paymentRequired, option) {
2282
+ /**
2283
+ * Mint the merchant-facing X-PAYMENT header with the delegate key.
2284
+ *
2285
+ * #3888: when `paymentId` is given, the EIP-3009 nonce is the shared
2286
+ * derivation from that Haven payment id instead of the x402 library's
2287
+ * random draw — that is what lets the backend attribute the merchant's
2288
+ * `AuthorizationUsed(delegate, nonce)` settlement to this payment when the
2289
+ * agent never reports it, and what makes a resume/retry re-sign of a
2290
+ * settled payment carry the SAME nonce (the merchant refuses it as already
2291
+ * used, closing the #3475 re-pay hazard). The signed typed data stays the
2292
+ * library's own: the nonce only overwrites the field on the unsigned
2293
+ * payload `preparePaymentHeader` built. Without a payment id the library's
2294
+ * random nonce is kept — an install too old to name its payment behaves
2295
+ * exactly as before.
2296
+ */
2297
+ async createPaymentHeader(paymentRequired, option, paymentId) {
2128
2298
  if (!this.delegateKey) {
2129
2299
  throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
2130
2300
  }
2131
2301
  const account = privateKeyToAccount(this.delegateKey);
2132
2302
  const requirements = toStandardPaymentRequirements(paymentRequired, option);
2133
- const header = await exact.evm.createPaymentHeader(
2134
- account,
2135
- paymentRequired.x402Version,
2136
- requirements
2137
- );
2303
+ let header;
2304
+ if (paymentId) {
2305
+ const unsigned = exact.evm.preparePaymentHeader(
2306
+ account.address,
2307
+ paymentRequired.x402Version,
2308
+ requirements
2309
+ );
2310
+ unsigned.payload.authorization.nonce = deriveX402PaymentNonce(paymentId);
2311
+ const signed = await exact.evm.signPaymentHeader(account, requirements, unsigned);
2312
+ header = exact.evm.encodePayment(signed);
2313
+ } else {
2314
+ header = await exact.evm.createPaymentHeader(
2315
+ account,
2316
+ paymentRequired.x402Version,
2317
+ requirements
2318
+ );
2319
+ }
2138
2320
  if (paymentRequired.x402Version < 2) return header;
2139
2321
  const payment = decodeBase64Json(header);
2140
2322
  return encodeBase64Json(x402V2PaymentEnvelope(paymentRequired, option, payment.payload));
@@ -2573,6 +2755,26 @@ function toolError(err) {
2573
2755
  };
2574
2756
  }
2575
2757
 
2758
+ // src/merchant-answer.ts
2759
+ function decidePostFundingMerchantAnswer(input) {
2760
+ const inBandChallenge = input.toolResult ? extractMcpPaymentRequired(input.toolResult) : void 0;
2761
+ const headerReceipt = input.paymentResponseHeader;
2762
+ const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : input.metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
2763
+ const protocolReceiptHeader = headerReceipt ?? (input.metaSettlement ? encodeMcpSettlementReceipt(input.metaSettlement) : void 0);
2764
+ const settlement = parseMerchantSettlement(headerReceipt ?? null);
2765
+ const settlementTxHash = settlement.settlementTxHash ?? (typeof input.metaSettlement?.transaction === "string" ? input.metaSettlement.transaction : void 0);
2766
+ const plainToolErrorAfterFunding = input.toolResult?.isError === true && inBandChallenge === void 0 && !settlementTxHash;
2767
+ const inBandRejection = inBandChallenge !== void 0 || input.metaSettlement?.success === false || plainToolErrorAfterFunding;
2768
+ return {
2769
+ accepted: input.httpOk && !inBandRejection,
2770
+ inBandRejection,
2771
+ plainToolErrorAfterFunding,
2772
+ settlementTxHash,
2773
+ protocolReceiptHeader,
2774
+ protocolReceiptHeaderName
2775
+ };
2776
+ }
2777
+
2576
2778
  // src/merchant-completion.ts
2577
2779
  var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
2578
2780
  var EVIDENCE_RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
@@ -2622,16 +2824,31 @@ var MerchantCompletion = class {
2622
2824
  retryInit = withTaxDeclarationHeader(initialInit, void 0);
2623
2825
  }
2624
2826
  }
2625
- const retryResponse = await this.merchantTransport.deliverPaymentRecoveringSession(
2626
- url,
2627
- retryInit,
2628
- receipt.paymentHeader,
2629
- () => this.merchantTransport.initialize(url, initialInit)
2630
- );
2827
+ let retryResponse;
2828
+ try {
2829
+ retryResponse = await this.merchantTransport.deliverPaymentRecoveringSession(
2830
+ url,
2831
+ retryInit,
2832
+ receipt.paymentHeader,
2833
+ () => this.merchantTransport.initialize(url, initialInit)
2834
+ );
2835
+ } catch (err) {
2836
+ if (err instanceof MerchantEgressRefusedError && !err.beforeRequest) {
2837
+ throw await this.paidDeliveryInterrupted(err, url, receipt, receipt.paymentHeader, retryInit?.body != null);
2838
+ }
2839
+ throw err;
2840
+ }
2631
2841
  const toolResult = retryResponse.ok ? await this.merchantTransport.readToolResult(retryResponse) : void 0;
2632
- const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
2633
2842
  const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
2634
- const inBandRejection = inBandChallenge !== void 0 || metaSettlement?.success === false;
2843
+ const headerReceipt = retryResponse.headers.get("PAYMENT-RESPONSE");
2844
+ const answer = decidePostFundingMerchantAnswer({
2845
+ toolResult,
2846
+ httpOk: retryResponse.ok,
2847
+ paymentResponseHeader: headerReceipt ?? void 0,
2848
+ metaSettlement
2849
+ });
2850
+ const reportedSettlementTxHash = answer.settlementTxHash;
2851
+ const inBandRejection = answer.inBandRejection;
2635
2852
  if (!retryResponse.ok || inBandRejection) {
2636
2853
  const merchant = await captureMerchantResponse(retryResponse);
2637
2854
  await this.recordRetryRejected({
@@ -2646,7 +2863,7 @@ var MerchantCompletion = class {
2646
2863
  }
2647
2864
  });
2648
2865
  throw new HavenApiError(
2649
- "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
2866
+ "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required. The merchant may still settle late: check the payment status before sweeping the delegate.",
2650
2867
  inBandRejection ? X402_RETRY_REJECTED_STATUS : merchant.merchant_status,
2651
2868
  {
2652
2869
  marker: "x402_retry_rejected_after_funding",
@@ -2659,10 +2876,10 @@ var MerchantCompletion = class {
2659
2876
  }
2660
2877
  );
2661
2878
  }
2662
- const headerReceipt = retryResponse.headers.get("PAYMENT-RESPONSE");
2663
- const protocolReceiptHeader = headerReceipt ?? (metaSettlement ? encodeMcpSettlementReceipt(metaSettlement) : void 0);
2664
- const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
2665
- const merchantSettlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
2879
+ const { protocolReceiptHeader, protocolReceiptHeaderName } = answer;
2880
+ const merchantSettlement = {
2881
+ settlementTxHash: reportedSettlementTxHash ?? null
2882
+ };
2666
2883
  if (receipt.merchant && merchantSettlement.settlementTxHash) {
2667
2884
  receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
2668
2885
  receipt.merchant.settlementExplorerUrl = buildExplorerUrl(receipt.chainId, merchantSettlement.settlementTxHash) || null;
@@ -2682,7 +2899,12 @@ var MerchantCompletion = class {
2682
2899
  });
2683
2900
  const settlementCandidate = merchantSettlement.settlementTxHash;
2684
2901
  if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && settlementCandidate.toLowerCase() !== receipt.txHash.toLowerCase()) {
2685
- await this.reportSettlementEvidenceOnce(receipt.paymentId, settlementCandidate);
2902
+ await this.reportEvidence({
2903
+ paymentId: receipt.paymentId,
2904
+ rail: "x402",
2905
+ txHash: settlementCandidate,
2906
+ ...settlementProofFields(receipt.paymentHeader)
2907
+ });
2686
2908
  }
2687
2909
  await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
2688
2910
  return retryResponse;
@@ -2785,6 +3007,51 @@ var MerchantCompletion = class {
2785
3007
  // and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
2786
3008
  // surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
2787
3009
  // for agent-to-merchant payments.
3010
+ /**
3011
+ * #4257: record a paid delivery that a refused redirect interrupted, and
3012
+ * return the refusal re-thrown with its payment and an accurate account:
3013
+ * the payment header (and the body, when there is one) reached `url`, the
3014
+ * merchant answered with a redirect Haven did not follow, and the merchant
3015
+ * may still settle. The record is the same
3016
+ * `merchant_retry_rejected_after_payment` event a non-2xx answer writes: its
3017
+ * `retry_status` is the redirect status, `retry_body` is null, and `details`
3018
+ * carry `refusal` plus `redirect_location` when the refusal names a redirect
3019
+ * target, or `refused_url` (the URL that answered) otherwise. The payment
3020
+ * reads as funded and undelivered.
3021
+ */
3022
+ async paidDeliveryInterrupted(err, url, receipt, paymentHeader, hasBody) {
3023
+ const scheme = x402PaymentHeaderNamesFor(paymentHeader).length > 1 ? "eip3009" : "erc7710";
3024
+ const status = err.redirectStatus ?? 0;
3025
+ await this.recordRetryRejected({
3026
+ rail: "x402",
3027
+ paymentId: receipt.paymentId,
3028
+ txHash: receipt.txHash,
3029
+ resourceUrl: receipt.resourceUrl,
3030
+ merchant: {
3031
+ merchant_status: status,
3032
+ merchant_status_text: "redirect not followed",
3033
+ merchant_headers: {},
3034
+ merchant_body: ""
3035
+ },
3036
+ details: {
3037
+ merchant_to: receipt.merchantTo,
3038
+ delegate_to: receipt.to,
3039
+ refusal: err.refusal,
3040
+ // The refusal's url is the redirect target only when it carries one;
3041
+ // otherwise it is the URL that answered (a non-GET redirect, a missing
3042
+ // or unresolvable Location), recorded under a neutral key.
3043
+ ...err.redirectTarget ? { redirect_location: err.redirectTarget } : { refused_url: err.url }
3044
+ }
3045
+ });
3046
+ const what = `The paid request was delivered to ${url}, ${hasBody ? "payment header and body" : "payment header"} included, and the merchant answered ${status ? `HTTP ${status} ` : ""}with a redirect${unfollowedRedirect(err)} Do not pay again: `;
3047
+ const next = scheme === "erc7710" ? "erc7710 has no funding leg, so there is no delegate balance to strand or sweep. The merchant may still redeem the settlement authorization within its window: check haven_get_payment_status after that window and re-quote only if it shows no settlement." : "the funding leg is confirmed on-chain and the merchant may still settle late. Check haven_get_payment_status before anything else, and sweep the delegate only if no settlement appears.";
3048
+ return new MerchantEgressRefusedError(what + next, err.url, err.refusal, false, {
3049
+ redirectStatus: err.redirectStatus,
3050
+ redirectTarget: err.redirectTarget,
3051
+ paymentId: receipt.paymentId,
3052
+ settlementScheme: scheme
3053
+ });
3054
+ }
2788
3055
  async recordRetryRejected(input) {
2789
3056
  try {
2790
3057
  await this.post("/machine-payments/reconciliation-events", {
@@ -2934,10 +3201,11 @@ var MerchantCompletion = class {
2934
3201
  }
2935
3202
  }
2936
3203
  /**
2937
- * ONE evidence attempt — the body of `reportEvidence`'s loop above, and the
2938
- * whole of the #3764 settlement report below. A 503 answers `retryable`
2939
- * immediately; the caller decides whether to sleep and re-attempt
2940
- * (`reportEvidence`) or hand the outcome to its consumer (#3764 D2).
3204
+ * ONE evidence attempt — the body of `reportEvidence`'s loop above. A 503
3205
+ * answers `retryable` immediately; the caller decides whether to sleep and
3206
+ * re-attempt (`reportEvidence`, and — #4215 — the local settlement report
3207
+ * in `retryRequest`) or hand the outcome to its consumer (#3764 D2, the
3208
+ * hosted settlement report).
2941
3209
  */
2942
3210
  async postEvidenceOnce(body) {
2943
3211
  try {
@@ -2947,10 +3215,12 @@ var MerchantCompletion = class {
2947
3215
  const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
2948
3216
  if (statusCode !== EVIDENCE_RETRYABLE_STATUS) {
2949
3217
  const reason = refusalReasonFromBody(err);
3218
+ const errorCode = refusalErrorCodeFromBody(err);
2950
3219
  return {
2951
3220
  outcome: "refused",
2952
3221
  statusCode: statusCode ?? 0,
2953
- ...reason ? { reason } : {}
3222
+ ...reason ? { reason } : {},
3223
+ ...errorCode ? { errorCode } : {}
2954
3224
  };
2955
3225
  }
2956
3226
  return { outcome: "retryable", statusCode };
@@ -2987,39 +3257,64 @@ var MerchantCompletion = class {
2987
3257
  * gate uses, so a caller cannot "fix" a missing hash by reporting the demo
2988
3258
  * merchant's own marker and getting a different verdict than the settle
2989
3259
  * path already gave it.
3260
+ *
3261
+ * #4061: `options.paymentHeader` is the payment header the caller sent to
3262
+ * the merchant on its paid retry. On eip3009 it is REQUIRED for the record
3263
+ * (#3925): its signed authorization proves the nonce the settlement burned
3264
+ * is this payment's, and a report without it is refused with
3265
+ * `errorCode: 'settlement_proof_header_missing'`. It is forwarded exactly
3266
+ * as the funding report forwards it (`paymentProofHeader` plus the names
3267
+ * `x402PaymentHeaderNamesSent` derives from the same payload). erc7710
3268
+ * needs none — Haven assembled that header itself.
2990
3269
  */
2991
- async reportSettlementEvidence(paymentId, settlementTxHash) {
3270
+ async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
2992
3271
  if (isZeroSettlementTxHash(settlementTxHash)) {
2993
3272
  throw new HavenZeroSettlementHashError(paymentId);
2994
3273
  }
2995
3274
  return this.reportEvidence({
2996
3275
  paymentId,
2997
3276
  rail: "x402",
2998
- txHash: settlementTxHash
3277
+ txHash: settlementTxHash,
3278
+ ...settlementProofFields(options.paymentHeader)
2999
3279
  });
3000
3280
  }
3001
3281
  /**
3002
3282
  * #3764: report the merchant's OWN EIP-3009 settlement transaction right
3003
- * after a completed purchase — the second leg of a payment that already has
3004
- * a funding-evidence row. Same endpoint and payload shape as
3283
+ * after a completed hosted purchase — the second leg of a payment that
3284
+ * already has a funding-evidence row. Same endpoint and payload shape as
3005
3285
  * `reportSettlementEvidence` above, but with #3764's D2 budget: exactly ONE
3006
3286
  * attempt, no `EVIDENCE_RETRY_DELAYS_MS` backoff. The resource is already
3007
3287
  * paid for and delivered, so a retryable answer must not stretch the
3008
3288
  * completed purchase — the HOSTED tools hand the outcome to the agent
3009
- * (`settlementEvidenceOutcome` → `haven_report_settlement_evidence`), and
3010
- * the local caller swallows it by design (D1). Callers validate the hash
3011
- * before this call (well-formed, non-zero, different from the funding hash
3289
+ * (`settlementEvidenceOutcome` → `haven_report_settlement_evidence`). Since
3290
+ * #4215 this ONE-attempt form serves the HOSTED path only
3291
+ * (`completeX402MerchantCall`); the local `retryRequest` posts the same
3292
+ * report through `reportEvidence`, whose bounded loop re-attempts while
3293
+ * Haven answers `retryable`. Callers validate the hash before this call
3294
+ * (well-formed, non-zero, different from the funding hash
3012
3295
  * case-insensitively), so there is no client-side refusal here — the
3013
3296
  * backend verifies on-chain and is the only judge.
3297
+ *
3298
+ * #4061: `paymentHeader` is the header the paid retry sent — the #3925
3299
+ * nonce proof, forwarded as `reportSettlementEvidence` forwards it. Both
3300
+ * callers hold it; without it the backend refuses the record.
3014
3301
  */
3015
- async reportSettlementEvidenceOnce(paymentId, settlementTxHash) {
3302
+ async reportSettlementEvidenceOnce(paymentId, settlementTxHash, paymentHeader) {
3016
3303
  return this.postEvidenceOnce({
3017
3304
  paymentId,
3018
3305
  rail: "x402",
3019
- txHash: settlementTxHash
3306
+ txHash: settlementTxHash,
3307
+ ...settlementProofFields(paymentHeader)
3020
3308
  });
3021
3309
  }
3022
3310
  };
3311
+ function settlementProofFields(paymentHeader) {
3312
+ if (!paymentHeader || !paymentHeader.trim()) return {};
3313
+ return {
3314
+ paymentProofHeaderName: x402PaymentHeaderNamesSent(paymentHeader),
3315
+ paymentProofHeader: paymentHeader
3316
+ };
3317
+ }
3023
3318
  function refusalReasonFromBody(err) {
3024
3319
  if (!(err instanceof HavenApiError)) return void 0;
3025
3320
  const body = err.body;
@@ -3027,6 +3322,13 @@ function refusalReasonFromBody(err) {
3027
3322
  const reason = body.reason;
3028
3323
  return typeof reason === "string" && reason.length > 0 ? reason : void 0;
3029
3324
  }
3325
+ function refusalErrorCodeFromBody(err) {
3326
+ if (!(err instanceof HavenApiError)) return void 0;
3327
+ const body = err.body;
3328
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return void 0;
3329
+ const code = body.error_code;
3330
+ return typeof code === "string" && code.length > 0 ? code : void 0;
3331
+ }
3030
3332
  function isZeroSettlementTxHash(hash) {
3031
3333
  return typeof hash === "string" && /^0x0+$/i.test(hash);
3032
3334
  }
@@ -3047,6 +3349,25 @@ function parseProtocolReceiptHeader(value) {
3047
3349
  }
3048
3350
  }
3049
3351
  }
3352
+ function unfollowedRedirect(err) {
3353
+ const target = err.redirectTarget ?? err.url;
3354
+ switch (err.refusal) {
3355
+ case "redirect_cross_origin_body":
3356
+ return ` to a different origin (${target}). Haven did not follow it, so the payment was not re-sent there.`;
3357
+ case "redirect_on_non_get":
3358
+ return ". Under the egress policy only GET redirects are followed, so Haven did not follow it and nothing was sent to the redirect target.";
3359
+ case "redirect_missing_location":
3360
+ return " that named no target (no Location header). Haven did not follow it.";
3361
+ case "redirect_invalid_location":
3362
+ return " whose Location header is not a resolvable URL. Haven did not follow it.";
3363
+ case "redirect_budget_exceeded":
3364
+ return ` past the redirect budget. The chain stopped before ${target}, and nothing was sent there.`;
3365
+ case "url_not_allowed":
3366
+ return ` to a URL the egress policy does not allow (${target}). Haven did not follow it, so nothing was sent there.`;
3367
+ default:
3368
+ return ". Haven did not follow it.";
3369
+ }
3370
+ }
3050
3371
 
3051
3372
  // src/client.ts
3052
3373
  var DEFAULT_POLLING_INTERVAL = 3e3;
@@ -3085,6 +3406,9 @@ function mapCatalogEntry(entry) {
3085
3406
  priceAtomic: entry.price_atomic,
3086
3407
  asset: entry.asset,
3087
3408
  network: entry.network,
3409
+ // #3839: conditional like `merchant` — absent (not null) when the backend
3410
+ // does not send the field, so an older backend's exact shape is unchanged.
3411
+ ...entry.asset_transfer_methods !== void 0 ? { assetTransferMethods: entry.asset_transfer_methods } : {},
3088
3412
  status: entry.status,
3089
3413
  verifiedAt: entry.verified_at,
3090
3414
  source: entry.source,
@@ -3603,7 +3927,8 @@ var HavenClient = class {
3603
3927
  amountAtomic: input.amountAtomic,
3604
3928
  ...input.merchantTo !== void 0 ? { merchantTo: input.merchantTo } : {},
3605
3929
  ...input.resourceUrl !== void 0 ? { resourceUrl: input.resourceUrl } : {},
3606
- ...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {}
3930
+ ...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {},
3931
+ ...input.recordRefusal !== void 0 ? { recordRefusal: input.recordRefusal } : {}
3607
3932
  });
3608
3933
  }
3609
3934
  /**
@@ -3710,10 +4035,10 @@ var HavenClient = class {
3710
4035
  return this.get(`/catalog/submit/${encodeURIComponent(id)}`);
3711
4036
  }
3712
4037
  /**
3713
- * Fetch one curated catalog entry by id (#1306).
4038
+ * Fetch one catalog entry by id (#1306).
3714
4039
  *
3715
4040
  * Chain-scoped for free by the backend's SQL when the client is
3716
- * agent-authenticated (#1299): an unknown id and an id curated for a
4041
+ * agent-authenticated (#1299): an unknown id and an id listed for a
3717
4042
  * DIFFERENT chain than this agent's both 404 identically — this method does
3718
4043
  * not (and must not) re-filter by chain in JS. Read-only, like
3719
4044
  * {@link discoverTools}.
@@ -3869,9 +4194,10 @@ var HavenClient = class {
3869
4194
  return this.accountReads.listPendingSubBudgetSignatures();
3870
4195
  }
3871
4196
  /**
3872
- * Close a sub-budget row (delegating agent only). A `pending` or expired row
3873
- * closes immediately; a live child returns `signData` for the on-chain
3874
- * revocation — sign it (`haven_sign` with `sub_budget_id`) and call
4197
+ * Close a sub-budget row (delegating agent only). A `pending` row, or an
4198
+ * `open` one already expired, closes immediately; otherwise (a live child, or
4199
+ * any `closing` row — its disable is still owed, #3923) it returns `signData`
4200
+ * for the on-chain revocation — sign it (`haven_sign` with `sub_budget_id`) and call
3875
4201
  * `submitSubBudget`.
3876
4202
  */
3877
4203
  async closeSubBudget(id) {
@@ -4120,7 +4446,7 @@ var HavenClient = class {
4120
4446
  "settled"
4121
4447
  );
4122
4448
  }
4123
- const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option);
4449
+ const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option, status.paymentId);
4124
4450
  const receipt = this.fundingLeg.receiptFromStatus(input.paymentRequired, option, paymentHeader, status);
4125
4451
  this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt, input.taskBudgetId);
4126
4452
  return receipt;
@@ -4297,13 +4623,15 @@ var HavenClient = class {
4297
4623
  body = text;
4298
4624
  }
4299
4625
  const toolResult = mcpToolResultOf(body);
4300
- const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
4301
4626
  const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
4302
4627
  const headerReceipt = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
4303
- const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
4304
- const protocolReceiptHeader = headerReceipt ?? (metaSettlement ? encodeMcpSettlementReceipt(metaSettlement) : void 0);
4305
- const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
4306
- const merchantAccepted = surfaced.ok && inBandChallenge === void 0 && metaSettlement?.success !== false;
4628
+ const answer = decidePostFundingMerchantAnswer({
4629
+ toolResult,
4630
+ httpOk: surfaced.ok,
4631
+ paymentResponseHeader: headerReceipt,
4632
+ metaSettlement
4633
+ });
4634
+ const merchantAccepted = answer.accepted;
4307
4635
  let evidenceOutcome;
4308
4636
  let settlementEvidenceOutcome;
4309
4637
  if (!merchantAccepted) {
@@ -4325,7 +4653,7 @@ var HavenClient = class {
4325
4653
  });
4326
4654
  }
4327
4655
  } else {
4328
- const rawEvidenceTxHash = input.noFundingLeg ? settlement.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
4656
+ const rawEvidenceTxHash = input.noFundingLeg ? answer.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
4329
4657
  const evidenceTxHash = rawEvidenceTxHash && !isZeroSettlementTxHash(rawEvidenceTxHash) ? rawEvidenceTxHash : void 0;
4330
4658
  if (evidenceTxHash) {
4331
4659
  evidenceOutcome = await this.merchantCompletion.reportEvidence({
@@ -4336,19 +4664,20 @@ var HavenClient = class {
4336
4664
  merchantStatus: surfaced.status,
4337
4665
  paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
4338
4666
  paymentProofHeader: input.paymentHeader,
4339
- protocolReceiptHeaderName,
4340
- protocolReceiptHeader,
4667
+ protocolReceiptHeaderName: answer.protocolReceiptHeaderName,
4668
+ protocolReceiptHeader: answer.protocolReceiptHeader,
4341
4669
  // #3778: the delivery pointer rides the accepted arm's evidence
4342
4670
  // report — a rejection delivered nothing to point at.
4343
4671
  ...input.deliveryReference ? { deliveryReference: input.deliveryReference } : {}
4344
4672
  });
4345
4673
  }
4346
4674
  if (!input.noFundingLeg) {
4347
- const settlementCandidate = settlement.settlementTxHash;
4675
+ const settlementCandidate = answer.settlementTxHash;
4348
4676
  if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && fundingTxHash && settlementCandidate.toLowerCase() !== fundingTxHash.toLowerCase()) {
4349
4677
  settlementEvidenceOutcome = await this.merchantCompletion.reportSettlementEvidenceOnce(
4350
4678
  evidenceContext.paymentId,
4351
- settlementCandidate
4679
+ settlementCandidate,
4680
+ input.paymentHeader
4352
4681
  );
4353
4682
  }
4354
4683
  }
@@ -4359,7 +4688,7 @@ var HavenClient = class {
4359
4688
  ok: merchantAccepted,
4360
4689
  body,
4361
4690
  // #3155 review S3: a transaction beside a rejection is not a settlement.
4362
- settlementTxHash: merchantAccepted ? settlement.settlementTxHash ?? void 0 : void 0,
4691
+ settlementTxHash: merchantAccepted ? answer.settlementTxHash ?? void 0 : void 0,
4363
4692
  evidenceOutcome,
4364
4693
  settlementEvidenceOutcome
4365
4694
  };
@@ -4392,9 +4721,15 @@ var HavenClient = class {
4392
4721
  * Haven verifies the delegate → merchant transfer on-chain
4393
4722
  * (`observeEip3009MerchantSettlement`) and records it beside the funding
4394
4723
  * hash, so receipts name the merchant's settlement.
4724
+ *
4725
+ * #4061: on eip3009 pass `options.paymentHeader` — the payment header you
4726
+ * sent the merchant on your paid retry. Since #3925 it is the proof the
4727
+ * settlement is recorded against; without it the report is `refused` with
4728
+ * `errorCode: 'settlement_proof_header_missing'`, and repeating it WITH the
4729
+ * header can succeed.
4395
4730
  */
4396
- async reportSettlementEvidence(paymentId, settlementTxHash) {
4397
- return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash);
4731
+ async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
4732
+ return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash, options);
4398
4733
  }
4399
4734
  /**
4400
4735
  * #3770: record the agent's evidence-only delivery verdict on a settled
@@ -4588,6 +4923,7 @@ function sleep(ms) {
4588
4923
  function composeDescription(d) {
4589
4924
  return [d.summary, d.selectionGuidance, d.behavior, d.nextActionGuidance].filter(Boolean).join(" ");
4590
4925
  }
4926
+ 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.`;
4591
4927
  var toolDescriptions = {
4592
4928
  quoteX402: {
4593
4929
  summary: "Inspect an HTTP 402 x402 paid resource without creating a Haven payment, signature, approval, or on-chain transaction.",
@@ -4642,7 +4978,7 @@ var toolDescriptions = {
4642
4978
  checkFunds: {
4643
4979
  summary: "Check whether the agent's account actually holds at least the given amount of a token \u2014 funds held, not spend permitted.",
4644
4980
  selectionGuidance: "Use this before attempting a payment when it matters whether the money is really there: allowance answers say what you are PERMITTED to spend, never whether the account HOLDS it. For allowance, budget, spend-limit, remaining-budget, reset-period, or what-can-I-spend questions use the allowance lookup tool instead.",
4645
- behavior: "Returns covered: true (the account holds at least the checked amount), false (a live chain read reports less \u2014 the budget is backed by an empty account; stop and tell the user funds are missing), or null (the chain read failed \u2014 unverifiable, never treat it as absence; coverageError says why). The account balance itself is deliberately not returned: this is a sufficiency signal, not a balance read. budget_remaining_atomic is the permitted figure from the allowance lookup (the SDK spells it budgetRemainingAtomic), named so it can never be confused with holdings.",
4981
+ behavior: "Returns covered: true (the account holds at least the checked amount), false (a live chain read reports the account holds less than the checked amount; stop and tell the user it is short), or null (the chain read failed \u2014 unverifiable, never treat it as absence; coverageError says why). The account balance itself is deliberately not returned: this is a sufficiency signal, not a balance read. budget_remaining_atomic is the permitted figure from the allowance lookup (the SDK spells it budgetRemainingAtomic), named so it can never be confused with holdings.",
4646
4982
  nextActionGuidance: "On covered=false, do not attempt the payment \u2014 tell the user the account is short and let them fund it; on covered=null, retry the check shortly or proceed knowing the payment may fail on-chain."
4647
4983
  },
4648
4984
  listReceipts: {
@@ -4670,9 +5006,9 @@ var toolDescriptions = {
4670
5006
  nextActionGuidance: "On a decline, report the reason to the user and ask them to raise the budget in Haven \u2014 there is no approval queue to wait on. This tool retries the merchant itself while it runs, so do not wait on a signal mid-call. If the process crashes after payment, a later haven_get_payment_status call may report nextAction=retry_original_x402_request \u2014 resume via haven_resume_x402_payment instead of paying again."
4671
5007
  },
4672
5008
  discoverTools: {
4673
- summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, the next call.",
4674
- 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.",
4675
- 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.",
5009
+ summary: "Step 1 of a purchase: discover payable services from Haven's merchant catalog \u2014 names, prices, the next call.",
5010
+ 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 (any source; bazaar seller-asserted) \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.",
5011
+ behavior: "Use each entry's suggested_tool + suggested_arguments first; an entry without them says why. Read-only lookup against Haven's 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.",
4676
5012
  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.`
4677
5013
  },
4678
5014
  submitCatalogEntry: {
@@ -4695,8 +5031,8 @@ var toolDescriptions = {
4695
5031
  },
4696
5032
  reportSettlementEvidence: {
4697
5033
  summary: "Report a payment's real merchant settlement transaction hash so Haven can verify it on-chain and record it.",
4698
- behavior: "Pass payment_id and, when you hold one, settlement_tx_hash (0x + 64 hex chars) \u2014 from PAYMENT-RESPONSE or a prior settlement_tx_hash. Omitting settlement_tx_hash succeeds as a no-op \u2014 nothing is checked or recorded, and no network call is made. Haven verifies on-chain before recording a hash; a zero, mismatched, or reverted hash is refused. Your own payments only.",
4699
- nextActionGuidance: "code SETTLEMENT_NOT_RECORDED (refusal_reason present): the payment's funding is confirmed and this hash was not accepted for it \u2014 do not retry it; refusal_reason carries why, verbatim. code DELIVERED_UNSETTLED: not verified, do not retry \u2014 poll haven_get_payment_status. code SETTLEMENT_PENDING (retryable:true): not mined or RPC unreachable \u2014 report the same hash again shortly."
5034
+ behavior: "Pass payment_id and, when you hold one, settlement_tx_hash (0x + 64 hex chars) \u2014 from PAYMENT-RESPONSE or a prior settlement_tx_hash. On EIP-3009 also pass payment_header (the header you retried the merchant with) or the report is refused. Omitting settlement_tx_hash succeeds as a no-op \u2014 nothing is checked or recorded, and no network call is made. Haven verifies on-chain before recording a hash; a zero, mismatched, or reverted hash is refused. Your own payments only.",
5035
+ nextActionGuidance: "code SETTLEMENT_PROOF_HEADER_MISSING: call again adding payment_header. code SETTLEMENT_NOT_RECORDED (refusal_reason present): the payment's funding is confirmed and this hash was not accepted for it \u2014 do not retry it; refusal_reason carries why, verbatim. code DELIVERED_UNSETTLED: not verified, do not retry \u2014 poll haven_get_payment_status. code SETTLEMENT_PENDING (retryable:true): not mined or RPC unreachable \u2014 report the same hash again shortly."
4700
5036
  }
4701
5037
  };
4702
5038
 
@@ -4914,9 +5250,17 @@ var havenTools = {
4914
5250
  openai: openaiTools
4915
5251
  };
4916
5252
 
5253
+ // src/catalog-funding-leg.ts
5254
+ function catalogFundingLegExpected(assetTransferMethods) {
5255
+ if (typeof assetTransferMethods !== "string") return "unknown";
5256
+ const methods = assetTransferMethods.split(",").map((method) => method.trim()).filter((method) => method.length > 0);
5257
+ if (methods.length === 0) return "unknown";
5258
+ return !methods.includes("erc7710");
5259
+ }
5260
+
4917
5261
  // src/agent-guidance.ts
4918
5262
  var AGENT_SECRET_HYGIENE_SENTENCE = "Do not print private keys, API keys, credential file contents, or config secrets in chat or logs.";
4919
- 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.";
5263
+ 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.";
4920
5264
  var AGENT_LOCAL_KEY_SENTENCE = "The Haven connector generates the signing key locally and sends Haven only the public signing address plus proof.";
4921
5265
  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.";
4922
5266
  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.";
@@ -4948,7 +5292,7 @@ Your user gives you a **budget on their own account** \u2014 not their wallet, a
4948
5292
 
4949
5293
  Four of the six steps are your user's \u2014 each needs a human signature or a human decision. The rest is yours. If they already have a funded account, start at step 3.
4950
5294
 
4951
- 1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey (Face ID / Touch ID) or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device. With a terminal, \`<packages.cli.one_liner> login --api <api-url>\` afterwards gets you a scoped session for steps 3-4 (that flag or \`HAVEN_API_URL\`, on the first command \u2014 the session then remembers the backend; **the CLI's built-in default is Haven's hosted production backend**, so on any other deployment an omitted flag connects you somewhere real and wrong rather than failing) \u2014 they approve a code in the browser, you never hold their password. 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. Do not hold the process open while you wait: under \`--json\`, pass \`--no-wait\` to get the link object back at once, then poll it with \`haven login --poll <device_code>\` \u2014 one round per invocation, exit 3 while it is still pending, 0 once approved. It can set up agents and read the account; it cannot sign, approve a budget, move funds, or rotate any agent's keys.
5295
+ 1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device. With a terminal, \`<packages.cli.one_liner> login --api <api-url>\` afterwards gets you a scoped session for steps 3-4 (that flag or \`HAVEN_API_URL\`, on the first command \u2014 the session then remembers the backend; **the CLI's built-in default is Haven's hosted production backend**, so on any other deployment an omitted flag connects you somewhere real and wrong rather than failing) \u2014 they approve a code in the browser, you never hold their password. 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. Do not hold the process open while you wait: under \`--json\`, pass \`--no-wait\` to get the link object back at once, then poll it with \`haven login --poll <device_code>\` \u2014 one round per invocation, exit 3 while it is still pending, 0 once approved. It can set up agents and read the account; it cannot sign, approve a budget, move funds, or rotate any agent's keys.
4952
5296
  2. **HUMAN \u2014 fund it.** USDC only, no ETH: Haven sponsors the gas. \`/.well-known/haven.json\` names \`chains.default\` as the deployment's expected chain, but after login \`haven wallets funding\` prints the address, the amount and which chain in one place; confirm that chain before you message your user. Without a CLI session, the dashboard's funding card shows the address and amount and its Receive-funds screen names the chain; never assume one: a testnet deployment and production both call themselves Haven. Before you write that message, read the manifest: \`environment\` says whether this deployment is \`production\`, and each \`chains.supported\` entry says whether that chain is a \`testnet\`. Real money is at stake only on a non-testnet chain of a \`production\` deployment \u2014 tell your user which case theirs is.
4953
5297
  3. **HUMAN \u2014 create the agent, set its budget**, and paste you the **setup prompt** it hands back. With a CLI session (step 1) you can do this step yourself: \`haven agents connect --name <n> --budget <amount> --token USDC --period <minutes>\` prints the same connector command and approval link; add \`--run\` to do step 4 too.
4954
5298
  4. **YOU \u2014 run the connector command** in that prompt (see What you run). It makes your signing key locally, registering only the public address.
@@ -4964,7 +5308,7 @@ haven budget grant <agentId> --amount <n> --token USDC --period <minutes> [--rec
4964
5308
  haven budget revoke <agentId> <delegationHash> [--wait]
4965
5309
  \`\`\`
4966
5310
 
4967
- 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>\`.
5311
+ 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\`.
4968
5312
 
4969
5313
  ## Hand-off scripts
4970
5314
 
@@ -4972,7 +5316,7 @@ Send these as your own message, \`<host>\` replaced by the host you fetched this
4972
5316
 
4973
5317
  **Before signup** (step 1):
4974
5318
 
4975
- > I can do everything except the parts that need your signature. Please open \`<host>/signup?next=/agents&via=agent\` \u2014 name, email, password, then a passkey (Face ID / Touch ID) or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? \`<host>/login?next=/agents\`.
5319
+ > I can do everything except the parts that need your signature. Please open \`<host>/signup?next=/agents&via=agent\` \u2014 name, email, password, then a passkey or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? \`<host>/login?next=/agents\`.
4976
5320
 
4977
5321
  **At the passkey step:**
4978
5322
 
@@ -5306,7 +5650,25 @@ the FINAL URL after redirects \u2014 and retry the merchant with the
5306
5650
  \`SIGN-IN-WITH-X\` header it returns: the delegate
5307
5651
  wallet signs in as the wallet that paid, moving no funds. NEVER follow a
5308
5652
  redirect with \`SIGN-IN-WITH-X\` (or a resulting session token) attached; if
5309
- the final origin differs, re-sign there. On THIS path Haven never sends the
5653
+ the final origin differs, re-sign there.
5654
+
5655
+ Sign in BEFORE a multi-call flow: if a merchant documents or offers a
5656
+ sign-in session (its docs, or a 402 whose \`sign-in-with-x\` extension names
5657
+ a free sign-in route), do not pay call by call. Send the unsigned sign-in
5658
+ request yourself, take the \`sign-in-with-x\` extension from its 402, and
5659
+ call \`mcp__haven-signer__haven_sign_siwx\` with it. The session token (often
5660
+ about 2 h) waives the merchant's micro-fees, so make every now-free call
5661
+ YOURSELF with the \`SIGN-IN-WITH-X\` header set \u2014 never pass a merchant
5662
+ session token to \`mcp__haven__haven_quote_x402\` or
5663
+ \`mcp__haven__haven_pay_x402_quote\` (their unpaid probe refuses a 200 with
5664
+ X402_PROBE_NOT_PAYMENT_REQUIRED) \u2014 and pay only where the merchant still
5665
+ charges (on Bitrefill, \`invoice/pay\`; keep sending the session token
5666
+ there). Sign at the FINAL URL after redirects, and never forward
5667
+ \`SIGN-IN-WITH-X\` or a session token across a redirect. If the signer's
5668
+ tools/list has no \`haven_sign_siwx\` (a signer older than 0.9.0-alpha.0),
5669
+ or the sign-in is refused, pay each call normally instead.
5670
+
5671
+ On THIS path Haven never sends the
5310
5672
  paid request (it sends only unpaid probes):
5311
5673
  on the **EIP-3009** scheme (the pay result names
5312
5674
  \`mcp__haven-signer__haven_sign_x402\`), that tool returns both
@@ -5322,10 +5684,13 @@ request, this is the only way it can learn the purchase failed \u2014 without it
5322
5684
  failed purchase reads as complete for fifteen minutes. If the merchant's
5323
5685
  \`PAYMENT-RESPONSE\` header names a \`transaction\`, pass that raw header as
5324
5686
  \`payment_response\` on the SAME \`mcp__haven__haven_report_x402_outcome\`
5325
- call: Haven decodes it, verifies the settlement on-chain, and the receipt then
5326
- shows the merchant's settlement, not only the funding transaction. Call
5687
+ call, together with \`payment_header\`: the exact header you retried the
5688
+ merchant with. Haven records a reported settlement only with that header,
5689
+ which proves the settlement is this payment's. It decodes the response,
5690
+ verifies the settlement on-chain, and the receipt then shows the merchant's
5691
+ settlement, not only the funding transaction. Call
5327
5692
  \`mcp__haven__haven_report_settlement_evidence\` only when the outcome answer
5328
- names it as the next step. On the **erc7710** scheme (the pay result says
5693
+ names it as the next step, and pass the same \`payment_header\` there too. On the **erc7710** scheme (the pay result says
5329
5694
  \`settlement_scheme: "erc7710"\`), sign with \`mcp__haven-signer__haven_sign\`,
5330
5695
  then \`mcp__haven__haven_submit\` with \`settlement_scheme: "erc7710"\`
5331
5696
  returns the \`payment_header\`; retry the merchant yourself with it as
@@ -5355,7 +5720,9 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
5355
5720
  \`invoice_id\`, beside a raw deposit address and a web payment link.
5356
5721
  - **Quote the exact request:** \`mcp__haven__haven_quote_x402\` with \`url\`,
5357
5722
  \`method\`, \`headers\` and \`body\`, exactly as the merchant described it.
5358
- \`body\` is a JSON **string**, not an object. On a JSON POST always pass
5723
+ \`body\` is a JSON **string**, not an object. When the price is stated up
5724
+ front (an invoice total, say), also pass \`expected_amount\` \u2014 the price you
5725
+ expect, in whole tokens (\`"0.10"\`). On a JSON POST always pass
5359
5726
  \`headers: {"Content-Type": "application/json"}\` \u2014 the tool does not infer
5360
5727
  it, and a merchant may answer a request without it with a generic challenge
5361
5728
  Haven cannot pay (for example, only the \`upto\` scheme).
@@ -5384,6 +5751,16 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
5384
5751
  report it, the merchant's settlement evidence. (A deposit transfer is bound
5385
5752
  to its payee and amount on-chain too; the difference is the record, not
5386
5753
  the binding.)
5754
+ - **If the quote says stop, stop.** The quote checks the agent's remaining
5755
+ budget at quote time, and it can check a price you state (\`expected_amount\`).
5756
+ When it returns \`safe_to_continue: false\` \u2014 the amount exceeds the budget, or
5757
+ the price does not match what you expected \u2014 it prefills no cap and names no
5758
+ next tool: stop and tell the user. If the x402 challenge itself is wrong (a
5759
+ price wildly off the invoice) and NOTHING has been paid via x402, the
5760
+ invoice's own deposit address MAY be an alternative, but only with the
5761
+ user's explicit go-ahead \u2014 never automatically. Once an x402 payment has
5762
+ been attempted or refused, never pay the deposit address: it can pay the
5763
+ invoice twice.
5387
5764
  - **Pay exactly one route, and never fall back silently.** If the x402 route
5388
5765
  is refused or fails, do not then pay the deposit address \u2014 that pays the
5389
5766
  invoice twice. Follow the result's guidance fields instead: on EIP-3009, a
@@ -5425,6 +5802,20 @@ check on in-flight payments. Do not poll in a tight loop.
5425
5802
  reports a missing or invalid credential, tell the user to re-run the Haven
5426
5803
  connector command.
5427
5804
 
5805
+ ## Stale tool list
5806
+
5807
+ Every hosted Haven result carries \`contract_fingerprint\` and
5808
+ \`server_version\` at the top level of its JSON. If a result's
5809
+ \`contract_fingerprint\` differs from the one in the server instructions you
5810
+ loaded at connect, or a \`next_arguments\` doesn't fit the schema you loaded
5811
+ for the hosted tool it names, your tool list is stale \u2014 reconnect or restart
5812
+ the session before continuing, and never hand-build or reshape a payload to
5813
+ make it fit; defer to the live schema and the \`next_arguments\` Haven returns.
5814
+ (A misfit on a signer tool is signer skew, not a stale list.) If a payment is
5815
+ already prepared or funded, keep its \`payment_id\`: after reconnecting,
5816
+ resume through \`haven_get_payment_status\` or \`haven_resume_x402_payment\`
5817
+ (\`haven_sweep_delegate\` for a stranded bridge balance). Never pay again.
5818
+
5428
5819
  ## Failure handling
5429
5820
 
5430
5821
  Haven tool failures are shaped like \`{ success: false, code, message, ... }\`
@@ -5449,7 +5840,9 @@ fields a success does; follow them first, then branch on \`code\` and surface
5449
5840
  "cannot settle right now" signal (a 503 \`merchant_not_ready\` with a
5450
5841
  \`reason_code\`) instead of a 402. No payment was created. Tell the user;
5451
5842
  retry later (the message carries \`retry_after_s\` when the merchant gave
5452
- one) \u2014 this is not a wrong or broken endpoint.
5843
+ one) \u2014 this is not a wrong or broken endpoint. An out-of-gas refusal
5844
+ (\`settlement_wallet_out_of_gas\`) needs the merchant's operator to top up
5845
+ its settlement wallet first; retrying before then is refused again.
5453
5846
  - \`MERCHANT_REJECTED_AFTER_FUNDING\`: the merchant refused the paid retry.
5454
5847
  On eip3009 (\`rail\` not \`erc7710\`): Stop-and-sweep \u2014 stop retrying the
5455
5848
  merchant and use \`mcp__haven__haven_sweep_delegate\` to recover stranded
@@ -5594,6 +5987,6 @@ function sameUrl(a, b) {
5594
5987
  }
5595
5988
  }
5596
5989
 
5597
- 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, composeDescription, discoverMerchantMcpUrl, formatTokenAmount, havenTools, isPublicHttpsMerchantUrl, isSecureX402RetryTarget, isZeroSettlementTxHash, parseMerchantSettlement, publicHttpsMerchantUrlRefusal, readBodyCapped, readX402ReceiptPayer, resolveX402RetryTarget, sameUrl, signHash, signUserOpTypedDataForDelegation, strictMerchantEgressPolicy, toolDescriptions, verifySignature };
5990
+ 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 };
5598
5991
  //# sourceMappingURL=index.js.map
5599
5992
  //# sourceMappingURL=index.js.map