@haven_ai/sdk 0.10.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.
- package/README.md +8 -5
- package/dist/{base64-CiR5BJhk.d.cts → base64-D4VZhgUg.d.ts} +2 -2
- package/dist/{base64-CxkinHPQ.d.ts → base64-DR4Gl8cD.d.cts} +2 -2
- package/dist/{chunk-EWTHDEIW.js → chunk-34YJQFCM.js} +3 -3
- package/dist/{chunk-EWTHDEIW.js.map → chunk-34YJQFCM.js.map} +1 -1
- package/dist/{chunk-6LEXYQ7J.cjs → chunk-DXBAN4SN.cjs} +4 -4
- package/dist/{chunk-6LEXYQ7J.cjs.map → chunk-DXBAN4SN.cjs.map} +1 -1
- package/dist/{chunk-TD2PHUYP.js → chunk-I7SL4A6X.js} +19 -3
- package/dist/chunk-I7SL4A6X.js.map +1 -0
- package/dist/{chunk-22FJ4JQO.cjs → chunk-IKQIIMM6.cjs} +35 -35
- package/dist/{chunk-22FJ4JQO.cjs.map → chunk-IKQIIMM6.cjs.map} +1 -1
- package/dist/{chunk-H37YVYZA.cjs → chunk-MZKLAHBK.cjs} +19 -3
- package/dist/chunk-MZKLAHBK.cjs.map +1 -0
- package/dist/{chunk-MLUWABVT.js → chunk-ZDJ55LR7.js} +4 -4
- package/dist/{chunk-MLUWABVT.js.map → chunk-ZDJ55LR7.js.map} +1 -1
- package/dist/edge.cjs +93 -93
- package/dist/edge.d.cts +4 -4
- package/dist/edge.d.ts +4 -4
- package/dist/edge.js +4 -4
- package/dist/index.cjs +755 -450
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -26
- package/dist/index.d.ts +55 -26
- package/dist/index.js +439 -134
- package/dist/index.js.map +1 -1
- package/dist/{receipt-Cg9a5Seq.d.ts → receipt-CbD8L0WL.d.ts} +1 -1
- package/dist/{receipt-q7ez8Zb2.d.cts → receipt-_bWCELEH.d.cts} +1 -1
- package/dist/test-support.cjs +21 -21
- package/dist/test-support.d.cts +2 -2
- package/dist/test-support.d.ts +2 -2
- package/dist/test-support.js +2 -2
- package/dist/{types-BKgqzvnk.d.cts → types-DKmvjXK6.d.cts} +65 -10
- package/dist/{types-BKgqzvnk.d.ts → types-DKmvjXK6.d.ts} +65 -10
- package/package.json +1 -1
- package/dist/chunk-H37YVYZA.cjs.map +0 -1
- package/dist/chunk-TD2PHUYP.js.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, deriveX402PaymentNonce, encodeBase64Json, x402V2PaymentEnvelope, isErc7710Option, buildSignedTaxDeclaration, encodeTaxDeclarationHeader, selectX402SettlementScheme, selectErc7710PaymentOption, x402AssetTransferMethod, resolveTokenFromAddress, normalizePaymentRequired, X_TAX_DECLARATION_HEADER } from './chunk-
|
|
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-
|
|
3
|
-
import { verifyPaymentReceipt } from './chunk-
|
|
4
|
-
export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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);
|
|
714
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
759
|
-
} catch {
|
|
760
|
-
throw
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
"
|
|
772
|
-
false
|
|
889
|
+
"redirect_cross_origin_body",
|
|
890
|
+
false,
|
|
891
|
+
{ redirectStatus: response.status, redirectTarget: next }
|
|
773
892
|
);
|
|
774
893
|
}
|
|
775
|
-
|
|
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
|
-
|
|
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.
|
|
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) => {
|
|
@@ -1260,6 +1392,7 @@ function mapTaskBudget(raw) {
|
|
|
1260
1392
|
status: raw.status,
|
|
1261
1393
|
expiresAt: raw.expires_at,
|
|
1262
1394
|
isExpired: raw.is_expired,
|
|
1395
|
+
...raw.is_live === void 0 ? {} : { isLive: raw.is_live },
|
|
1263
1396
|
createdAt: raw.created_at,
|
|
1264
1397
|
openedAt: raw.opened_at,
|
|
1265
1398
|
closedAt: raw.closed_at,
|
|
@@ -1413,7 +1546,8 @@ var AccountReads = class {
|
|
|
1413
1546
|
* an agent can still act on, and read-by-id (`haven_get_task_budget`) is
|
|
1414
1547
|
* the any-status surface for everything else:
|
|
1415
1548
|
*
|
|
1416
|
-
* - `closing`
|
|
1549
|
+
* - `closing` stays visible until its expiry (#3773; an expired `closing`
|
|
1550
|
+
* row drops, #3923) — the close submit is
|
|
1417
1551
|
* in flight and the signer's close refusal tells the caller to re-check
|
|
1418
1552
|
* exactly this row's status;
|
|
1419
1553
|
* - `closed` rows drop (terminal — `closed_at` names what happened, and
|
|
@@ -1422,10 +1556,14 @@ var AccountReads = class {
|
|
|
1422
1556
|
* budget reserves nothing and pays nothing);
|
|
1423
1557
|
* - unexpired `pending` and `open` rows stay.
|
|
1424
1558
|
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
* `
|
|
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
|
|
1429
1567
|
* no `is_expired` is treated as unexpired — the degraded read errs toward
|
|
1430
1568
|
* KEEPING rows, never toward hiding one the agent could still act on.
|
|
1431
1569
|
*/
|
|
@@ -1439,7 +1577,9 @@ var AccountReads = class {
|
|
|
1439
1577
|
});
|
|
1440
1578
|
const rows = raw?.task_budgets;
|
|
1441
1579
|
if (!Array.isArray(rows)) return [];
|
|
1442
|
-
return rows.map((row) => mapTaskBudget(row)).filter(
|
|
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));
|
|
1443
1583
|
} catch {
|
|
1444
1584
|
return [];
|
|
1445
1585
|
}
|
|
@@ -2615,6 +2755,26 @@ function toolError(err) {
|
|
|
2615
2755
|
};
|
|
2616
2756
|
}
|
|
2617
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
|
+
|
|
2618
2778
|
// src/merchant-completion.ts
|
|
2619
2779
|
var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
|
|
2620
2780
|
var EVIDENCE_RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
|
|
@@ -2664,16 +2824,31 @@ var MerchantCompletion = class {
|
|
|
2664
2824
|
retryInit = withTaxDeclarationHeader(initialInit, void 0);
|
|
2665
2825
|
}
|
|
2666
2826
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
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
|
+
}
|
|
2673
2841
|
const toolResult = retryResponse.ok ? await this.merchantTransport.readToolResult(retryResponse) : void 0;
|
|
2674
|
-
const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
|
|
2675
2842
|
const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
|
|
2676
|
-
const
|
|
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;
|
|
2677
2852
|
if (!retryResponse.ok || inBandRejection) {
|
|
2678
2853
|
const merchant = await captureMerchantResponse(retryResponse);
|
|
2679
2854
|
await this.recordRetryRejected({
|
|
@@ -2688,7 +2863,7 @@ var MerchantCompletion = class {
|
|
|
2688
2863
|
}
|
|
2689
2864
|
});
|
|
2690
2865
|
throw new HavenApiError(
|
|
2691
|
-
"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.",
|
|
2692
2867
|
inBandRejection ? X402_RETRY_REJECTED_STATUS : merchant.merchant_status,
|
|
2693
2868
|
{
|
|
2694
2869
|
marker: "x402_retry_rejected_after_funding",
|
|
@@ -2701,10 +2876,10 @@ var MerchantCompletion = class {
|
|
|
2701
2876
|
}
|
|
2702
2877
|
);
|
|
2703
2878
|
}
|
|
2704
|
-
const
|
|
2705
|
-
const
|
|
2706
|
-
|
|
2707
|
-
|
|
2879
|
+
const { protocolReceiptHeader, protocolReceiptHeaderName } = answer;
|
|
2880
|
+
const merchantSettlement = {
|
|
2881
|
+
settlementTxHash: reportedSettlementTxHash ?? null
|
|
2882
|
+
};
|
|
2708
2883
|
if (receipt.merchant && merchantSettlement.settlementTxHash) {
|
|
2709
2884
|
receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
|
|
2710
2885
|
receipt.merchant.settlementExplorerUrl = buildExplorerUrl(receipt.chainId, merchantSettlement.settlementTxHash) || null;
|
|
@@ -2724,7 +2899,12 @@ var MerchantCompletion = class {
|
|
|
2724
2899
|
});
|
|
2725
2900
|
const settlementCandidate = merchantSettlement.settlementTxHash;
|
|
2726
2901
|
if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && settlementCandidate.toLowerCase() !== receipt.txHash.toLowerCase()) {
|
|
2727
|
-
await this.
|
|
2902
|
+
await this.reportEvidence({
|
|
2903
|
+
paymentId: receipt.paymentId,
|
|
2904
|
+
rail: "x402",
|
|
2905
|
+
txHash: settlementCandidate,
|
|
2906
|
+
...settlementProofFields(receipt.paymentHeader)
|
|
2907
|
+
});
|
|
2728
2908
|
}
|
|
2729
2909
|
await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
|
|
2730
2910
|
return retryResponse;
|
|
@@ -2827,6 +3007,51 @@ var MerchantCompletion = class {
|
|
|
2827
3007
|
// and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
|
|
2828
3008
|
// surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
|
|
2829
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
|
+
}
|
|
2830
3055
|
async recordRetryRejected(input) {
|
|
2831
3056
|
try {
|
|
2832
3057
|
await this.post("/machine-payments/reconciliation-events", {
|
|
@@ -2976,10 +3201,11 @@ var MerchantCompletion = class {
|
|
|
2976
3201
|
}
|
|
2977
3202
|
}
|
|
2978
3203
|
/**
|
|
2979
|
-
* ONE evidence attempt — the body of `reportEvidence`'s loop above
|
|
2980
|
-
*
|
|
2981
|
-
*
|
|
2982
|
-
*
|
|
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).
|
|
2983
3209
|
*/
|
|
2984
3210
|
async postEvidenceOnce(body) {
|
|
2985
3211
|
try {
|
|
@@ -2989,10 +3215,12 @@ var MerchantCompletion = class {
|
|
|
2989
3215
|
const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
|
|
2990
3216
|
if (statusCode !== EVIDENCE_RETRYABLE_STATUS) {
|
|
2991
3217
|
const reason = refusalReasonFromBody(err);
|
|
3218
|
+
const errorCode = refusalErrorCodeFromBody(err);
|
|
2992
3219
|
return {
|
|
2993
3220
|
outcome: "refused",
|
|
2994
3221
|
statusCode: statusCode ?? 0,
|
|
2995
|
-
...reason ? { reason } : {}
|
|
3222
|
+
...reason ? { reason } : {},
|
|
3223
|
+
...errorCode ? { errorCode } : {}
|
|
2996
3224
|
};
|
|
2997
3225
|
}
|
|
2998
3226
|
return { outcome: "retryable", statusCode };
|
|
@@ -3029,39 +3257,64 @@ var MerchantCompletion = class {
|
|
|
3029
3257
|
* gate uses, so a caller cannot "fix" a missing hash by reporting the demo
|
|
3030
3258
|
* merchant's own marker and getting a different verdict than the settle
|
|
3031
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.
|
|
3032
3269
|
*/
|
|
3033
|
-
async reportSettlementEvidence(paymentId, settlementTxHash) {
|
|
3270
|
+
async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
|
|
3034
3271
|
if (isZeroSettlementTxHash(settlementTxHash)) {
|
|
3035
3272
|
throw new HavenZeroSettlementHashError(paymentId);
|
|
3036
3273
|
}
|
|
3037
3274
|
return this.reportEvidence({
|
|
3038
3275
|
paymentId,
|
|
3039
3276
|
rail: "x402",
|
|
3040
|
-
txHash: settlementTxHash
|
|
3277
|
+
txHash: settlementTxHash,
|
|
3278
|
+
...settlementProofFields(options.paymentHeader)
|
|
3041
3279
|
});
|
|
3042
3280
|
}
|
|
3043
3281
|
/**
|
|
3044
3282
|
* #3764: report the merchant's OWN EIP-3009 settlement transaction right
|
|
3045
|
-
* after a completed purchase — the second leg of a payment that
|
|
3046
|
-
* 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
|
|
3047
3285
|
* `reportSettlementEvidence` above, but with #3764's D2 budget: exactly ONE
|
|
3048
3286
|
* attempt, no `EVIDENCE_RETRY_DELAYS_MS` backoff. The resource is already
|
|
3049
3287
|
* paid for and delivered, so a retryable answer must not stretch the
|
|
3050
3288
|
* completed purchase — the HOSTED tools hand the outcome to the agent
|
|
3051
|
-
* (`settlementEvidenceOutcome` → `haven_report_settlement_evidence`)
|
|
3052
|
-
*
|
|
3053
|
-
*
|
|
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
|
|
3054
3295
|
* case-insensitively), so there is no client-side refusal here — the
|
|
3055
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.
|
|
3056
3301
|
*/
|
|
3057
|
-
async reportSettlementEvidenceOnce(paymentId, settlementTxHash) {
|
|
3302
|
+
async reportSettlementEvidenceOnce(paymentId, settlementTxHash, paymentHeader) {
|
|
3058
3303
|
return this.postEvidenceOnce({
|
|
3059
3304
|
paymentId,
|
|
3060
3305
|
rail: "x402",
|
|
3061
|
-
txHash: settlementTxHash
|
|
3306
|
+
txHash: settlementTxHash,
|
|
3307
|
+
...settlementProofFields(paymentHeader)
|
|
3062
3308
|
});
|
|
3063
3309
|
}
|
|
3064
3310
|
};
|
|
3311
|
+
function settlementProofFields(paymentHeader) {
|
|
3312
|
+
if (!paymentHeader || !paymentHeader.trim()) return {};
|
|
3313
|
+
return {
|
|
3314
|
+
paymentProofHeaderName: x402PaymentHeaderNamesSent(paymentHeader),
|
|
3315
|
+
paymentProofHeader: paymentHeader
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3065
3318
|
function refusalReasonFromBody(err) {
|
|
3066
3319
|
if (!(err instanceof HavenApiError)) return void 0;
|
|
3067
3320
|
const body = err.body;
|
|
@@ -3069,6 +3322,13 @@ function refusalReasonFromBody(err) {
|
|
|
3069
3322
|
const reason = body.reason;
|
|
3070
3323
|
return typeof reason === "string" && reason.length > 0 ? reason : void 0;
|
|
3071
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
|
+
}
|
|
3072
3332
|
function isZeroSettlementTxHash(hash) {
|
|
3073
3333
|
return typeof hash === "string" && /^0x0+$/i.test(hash);
|
|
3074
3334
|
}
|
|
@@ -3089,6 +3349,25 @@ function parseProtocolReceiptHeader(value) {
|
|
|
3089
3349
|
}
|
|
3090
3350
|
}
|
|
3091
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
|
+
}
|
|
3092
3371
|
|
|
3093
3372
|
// src/client.ts
|
|
3094
3373
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
@@ -3648,7 +3927,8 @@ var HavenClient = class {
|
|
|
3648
3927
|
amountAtomic: input.amountAtomic,
|
|
3649
3928
|
...input.merchantTo !== void 0 ? { merchantTo: input.merchantTo } : {},
|
|
3650
3929
|
...input.resourceUrl !== void 0 ? { resourceUrl: input.resourceUrl } : {},
|
|
3651
|
-
...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {}
|
|
3930
|
+
...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {},
|
|
3931
|
+
...input.recordRefusal !== void 0 ? { recordRefusal: input.recordRefusal } : {}
|
|
3652
3932
|
});
|
|
3653
3933
|
}
|
|
3654
3934
|
/**
|
|
@@ -3755,10 +4035,10 @@ var HavenClient = class {
|
|
|
3755
4035
|
return this.get(`/catalog/submit/${encodeURIComponent(id)}`);
|
|
3756
4036
|
}
|
|
3757
4037
|
/**
|
|
3758
|
-
* Fetch one
|
|
4038
|
+
* Fetch one catalog entry by id (#1306).
|
|
3759
4039
|
*
|
|
3760
4040
|
* Chain-scoped for free by the backend's SQL when the client is
|
|
3761
|
-
* agent-authenticated (#1299): an unknown id and an id
|
|
4041
|
+
* agent-authenticated (#1299): an unknown id and an id listed for a
|
|
3762
4042
|
* DIFFERENT chain than this agent's both 404 identically — this method does
|
|
3763
4043
|
* not (and must not) re-filter by chain in JS. Read-only, like
|
|
3764
4044
|
* {@link discoverTools}.
|
|
@@ -3914,9 +4194,10 @@ var HavenClient = class {
|
|
|
3914
4194
|
return this.accountReads.listPendingSubBudgetSignatures();
|
|
3915
4195
|
}
|
|
3916
4196
|
/**
|
|
3917
|
-
* Close a sub-budget row (delegating agent only). A `pending` or
|
|
3918
|
-
* closes immediately; a live child
|
|
3919
|
-
*
|
|
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
|
|
3920
4201
|
* `submitSubBudget`.
|
|
3921
4202
|
*/
|
|
3922
4203
|
async closeSubBudget(id) {
|
|
@@ -4342,13 +4623,15 @@ var HavenClient = class {
|
|
|
4342
4623
|
body = text;
|
|
4343
4624
|
}
|
|
4344
4625
|
const toolResult = mcpToolResultOf(body);
|
|
4345
|
-
const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
|
|
4346
4626
|
const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
|
|
4347
4627
|
const headerReceipt = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
|
|
4348
|
-
const
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4628
|
+
const answer = decidePostFundingMerchantAnswer({
|
|
4629
|
+
toolResult,
|
|
4630
|
+
httpOk: surfaced.ok,
|
|
4631
|
+
paymentResponseHeader: headerReceipt,
|
|
4632
|
+
metaSettlement
|
|
4633
|
+
});
|
|
4634
|
+
const merchantAccepted = answer.accepted;
|
|
4352
4635
|
let evidenceOutcome;
|
|
4353
4636
|
let settlementEvidenceOutcome;
|
|
4354
4637
|
if (!merchantAccepted) {
|
|
@@ -4370,7 +4653,7 @@ var HavenClient = class {
|
|
|
4370
4653
|
});
|
|
4371
4654
|
}
|
|
4372
4655
|
} else {
|
|
4373
|
-
const rawEvidenceTxHash = input.noFundingLeg ?
|
|
4656
|
+
const rawEvidenceTxHash = input.noFundingLeg ? answer.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
|
|
4374
4657
|
const evidenceTxHash = rawEvidenceTxHash && !isZeroSettlementTxHash(rawEvidenceTxHash) ? rawEvidenceTxHash : void 0;
|
|
4375
4658
|
if (evidenceTxHash) {
|
|
4376
4659
|
evidenceOutcome = await this.merchantCompletion.reportEvidence({
|
|
@@ -4381,19 +4664,20 @@ var HavenClient = class {
|
|
|
4381
4664
|
merchantStatus: surfaced.status,
|
|
4382
4665
|
paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
|
|
4383
4666
|
paymentProofHeader: input.paymentHeader,
|
|
4384
|
-
protocolReceiptHeaderName,
|
|
4385
|
-
protocolReceiptHeader,
|
|
4667
|
+
protocolReceiptHeaderName: answer.protocolReceiptHeaderName,
|
|
4668
|
+
protocolReceiptHeader: answer.protocolReceiptHeader,
|
|
4386
4669
|
// #3778: the delivery pointer rides the accepted arm's evidence
|
|
4387
4670
|
// report — a rejection delivered nothing to point at.
|
|
4388
4671
|
...input.deliveryReference ? { deliveryReference: input.deliveryReference } : {}
|
|
4389
4672
|
});
|
|
4390
4673
|
}
|
|
4391
4674
|
if (!input.noFundingLeg) {
|
|
4392
|
-
const settlementCandidate =
|
|
4675
|
+
const settlementCandidate = answer.settlementTxHash;
|
|
4393
4676
|
if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && fundingTxHash && settlementCandidate.toLowerCase() !== fundingTxHash.toLowerCase()) {
|
|
4394
4677
|
settlementEvidenceOutcome = await this.merchantCompletion.reportSettlementEvidenceOnce(
|
|
4395
4678
|
evidenceContext.paymentId,
|
|
4396
|
-
settlementCandidate
|
|
4679
|
+
settlementCandidate,
|
|
4680
|
+
input.paymentHeader
|
|
4397
4681
|
);
|
|
4398
4682
|
}
|
|
4399
4683
|
}
|
|
@@ -4404,7 +4688,7 @@ var HavenClient = class {
|
|
|
4404
4688
|
ok: merchantAccepted,
|
|
4405
4689
|
body,
|
|
4406
4690
|
// #3155 review S3: a transaction beside a rejection is not a settlement.
|
|
4407
|
-
settlementTxHash: merchantAccepted ?
|
|
4691
|
+
settlementTxHash: merchantAccepted ? answer.settlementTxHash ?? void 0 : void 0,
|
|
4408
4692
|
evidenceOutcome,
|
|
4409
4693
|
settlementEvidenceOutcome
|
|
4410
4694
|
};
|
|
@@ -4437,9 +4721,15 @@ var HavenClient = class {
|
|
|
4437
4721
|
* Haven verifies the delegate → merchant transfer on-chain
|
|
4438
4722
|
* (`observeEip3009MerchantSettlement`) and records it beside the funding
|
|
4439
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.
|
|
4440
4730
|
*/
|
|
4441
|
-
async reportSettlementEvidence(paymentId, settlementTxHash) {
|
|
4442
|
-
return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash);
|
|
4731
|
+
async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
|
|
4732
|
+
return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash, options);
|
|
4443
4733
|
}
|
|
4444
4734
|
/**
|
|
4445
4735
|
* #3770: record the agent's evidence-only delivery verdict on a settled
|
|
@@ -4688,7 +4978,7 @@ var toolDescriptions = {
|
|
|
4688
4978
|
checkFunds: {
|
|
4689
4979
|
summary: "Check whether the agent's account actually holds at least the given amount of a token \u2014 funds held, not spend permitted.",
|
|
4690
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.",
|
|
4691
|
-
behavior: "Returns covered: true (the account holds at least the checked amount), false (a live chain read reports
|
|
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.",
|
|
4692
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."
|
|
4693
4983
|
},
|
|
4694
4984
|
listReceipts: {
|
|
@@ -4716,9 +5006,9 @@ var toolDescriptions = {
|
|
|
4716
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."
|
|
4717
5007
|
},
|
|
4718
5008
|
discoverTools: {
|
|
4719
|
-
summary: "Step 1 of a purchase: discover payable services from Haven's
|
|
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 (
|
|
4721
|
-
behavior: "Use each entry's suggested_tool + suggested_arguments first; an entry without them says why. Read-only lookup against Haven's
|
|
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.",
|
|
4722
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.`
|
|
4723
5013
|
},
|
|
4724
5014
|
submitCatalogEntry: {
|
|
@@ -4741,8 +5031,8 @@ var toolDescriptions = {
|
|
|
4741
5031
|
},
|
|
4742
5032
|
reportSettlementEvidence: {
|
|
4743
5033
|
summary: "Report a payment's real merchant settlement transaction hash so Haven can verify it on-chain and record it.",
|
|
4744
|
-
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.",
|
|
4745
|
-
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."
|
|
4746
5036
|
}
|
|
4747
5037
|
};
|
|
4748
5038
|
|
|
@@ -5002,7 +5292,7 @@ Your user gives you a **budget on their own account** \u2014 not their wallet, a
|
|
|
5002
5292
|
|
|
5003
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.
|
|
5004
5294
|
|
|
5005
|
-
1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey
|
|
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.
|
|
5006
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.
|
|
5007
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.
|
|
5008
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.
|
|
@@ -5026,7 +5316,7 @@ Send these as your own message, \`<host>\` replaced by the host you fetched this
|
|
|
5026
5316
|
|
|
5027
5317
|
**Before signup** (step 1):
|
|
5028
5318
|
|
|
5029
|
-
> 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
|
|
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\`.
|
|
5030
5320
|
|
|
5031
5321
|
**At the passkey step:**
|
|
5032
5322
|
|
|
@@ -5394,10 +5684,13 @@ request, this is the only way it can learn the purchase failed \u2014 without it
|
|
|
5394
5684
|
failed purchase reads as complete for fifteen minutes. If the merchant's
|
|
5395
5685
|
\`PAYMENT-RESPONSE\` header names a \`transaction\`, pass that raw header as
|
|
5396
5686
|
\`payment_response\` on the SAME \`mcp__haven__haven_report_x402_outcome\`
|
|
5397
|
-
call
|
|
5398
|
-
|
|
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
|
|
5399
5692
|
\`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
|
|
5693
|
+
names it as the next step, and pass the same \`payment_header\` there too. On the **erc7710** scheme (the pay result says
|
|
5401
5694
|
\`settlement_scheme: "erc7710"\`), sign with \`mcp__haven-signer__haven_sign\`,
|
|
5402
5695
|
then \`mcp__haven__haven_submit\` with \`settlement_scheme: "erc7710"\`
|
|
5403
5696
|
returns the \`payment_header\`; retry the merchant yourself with it as
|
|
@@ -5427,7 +5720,9 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
|
|
|
5427
5720
|
\`invoice_id\`, beside a raw deposit address and a web payment link.
|
|
5428
5721
|
- **Quote the exact request:** \`mcp__haven__haven_quote_x402\` with \`url\`,
|
|
5429
5722
|
\`method\`, \`headers\` and \`body\`, exactly as the merchant described it.
|
|
5430
|
-
\`body\` is a JSON **string**, not an object.
|
|
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
|
|
5431
5726
|
\`headers: {"Content-Type": "application/json"}\` \u2014 the tool does not infer
|
|
5432
5727
|
it, and a merchant may answer a request without it with a generic challenge
|
|
5433
5728
|
Haven cannot pay (for example, only the \`upto\` scheme).
|
|
@@ -5456,6 +5751,16 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
|
|
|
5456
5751
|
report it, the merchant's settlement evidence. (A deposit transfer is bound
|
|
5457
5752
|
to its payee and amount on-chain too; the difference is the record, not
|
|
5458
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.
|
|
5459
5764
|
- **Pay exactly one route, and never fall back silently.** If the x402 route
|
|
5460
5765
|
is refused or fails, do not then pay the deposit address \u2014 that pays the
|
|
5461
5766
|
invoice twice. Follow the result's guidance fields instead: on EIP-3009, a
|