@haven_ai/sdk 0.10.0-alpha.0 → 0.12.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 +45 -9
- package/dist/{base64-CiR5BJhk.d.cts → base64-C5OrJLij.d.ts} +10 -2
- package/dist/{base64-CxkinHPQ.d.ts → base64-COaV1Sis.d.cts} +10 -2
- package/dist/{chunk-6LEXYQ7J.cjs → chunk-4JTLJVBS.cjs} +4 -4
- package/dist/chunk-4JTLJVBS.cjs.map +1 -0
- package/dist/{chunk-MLUWABVT.js → chunk-7M3CIW7W.js} +7 -6
- package/dist/chunk-7M3CIW7W.js.map +1 -0
- package/dist/{chunk-TD2PHUYP.js → chunk-UPEDQLAL.js} +72 -6
- package/dist/chunk-UPEDQLAL.js.map +1 -0
- package/dist/{chunk-22FJ4JQO.cjs → chunk-V2ODMLDD.cjs} +38 -37
- package/dist/chunk-V2ODMLDD.cjs.map +1 -0
- package/dist/{chunk-EWTHDEIW.js → chunk-WFTGZS24.js} +3 -3
- package/dist/chunk-WFTGZS24.js.map +1 -0
- package/dist/{chunk-H37YVYZA.cjs → chunk-ZA3NOSO2.cjs} +73 -5
- package/dist/chunk-ZA3NOSO2.cjs.map +1 -0
- package/dist/edge.cjs +93 -93
- package/dist/edge.d.cts +11 -8
- package/dist/edge.d.ts +11 -8
- package/dist/edge.js +4 -4
- package/dist/index.cjs +866 -475
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -33
- package/dist/index.d.ts +69 -33
- package/dist/index.js +554 -171
- package/dist/index.js.map +1 -1
- package/dist/{receipt-q7ez8Zb2.d.cts → receipt-Dy77vCea.d.cts} +5 -4
- package/dist/{receipt-Cg9a5Seq.d.ts → receipt-DyUInpvF.d.ts} +5 -4
- 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.ts → types-CAvsjRKi.d.cts} +209 -12
- package/dist/{types-BKgqzvnk.d.cts → types-CAvsjRKi.d.ts} +209 -12
- package/package.json +1 -1
- package/dist/chunk-22FJ4JQO.cjs.map +0 -1
- package/dist/chunk-6LEXYQ7J.cjs.map +0 -1
- package/dist/chunk-EWTHDEIW.js.map +0 -1
- package/dist/chunk-H37YVYZA.cjs.map +0 -1
- package/dist/chunk-MLUWABVT.js.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-7M3CIW7W.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-7M3CIW7W.js';
|
|
3
|
+
import { verifyPaymentReceipt } from './chunk-WFTGZS24.js';
|
|
4
|
+
export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-WFTGZS24.js';
|
|
5
|
+
import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, X402AlreadySettledError, MerchantTimeoutError, AgentPaymentWarningCode, AgentPaymentNextAction, FundingConfirmationUnknownError, FundingRevertedError, HavenPaymentStateError, HavenZeroSettlementHashError, X402Erc7710AlreadySettledError, chainIdForNetwork } from './chunk-UPEDQLAL.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, FundingConfirmationUnknownError, FundingRevertedError, 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-UPEDQLAL.js';
|
|
7
7
|
import { hashTypedData } from 'viem';
|
|
8
8
|
import { ethers } from 'ethers';
|
|
9
9
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
@@ -143,12 +143,16 @@ var HavenApiTransport = class {
|
|
|
143
143
|
return data;
|
|
144
144
|
} catch (err) {
|
|
145
145
|
if (err instanceof HavenApiError) throw err;
|
|
146
|
+
const failureCause = { kind: "no_answer", method };
|
|
146
147
|
if (err instanceof Error && err.name === "AbortError") {
|
|
147
|
-
throw new HavenApiError(`Request to ${path} timed out`, 408);
|
|
148
|
+
throw new HavenApiError(`Request to ${path} timed out`, 408, void 0, void 0, failureCause);
|
|
148
149
|
}
|
|
149
150
|
throw new HavenApiError(
|
|
150
151
|
`Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
151
|
-
0
|
|
152
|
+
0,
|
|
153
|
+
void 0,
|
|
154
|
+
void 0,
|
|
155
|
+
failureCause
|
|
152
156
|
);
|
|
153
157
|
} finally {
|
|
154
158
|
clearTimeout(timeout);
|
|
@@ -354,6 +358,24 @@ function mapPaymentReceipt(raw) {
|
|
|
354
358
|
if ("approval_request_id" in raw) {
|
|
355
359
|
receipt.approvalRequestId = raw.approval_request_id ?? null;
|
|
356
360
|
}
|
|
361
|
+
if ("settlement_scheme" in raw) {
|
|
362
|
+
receipt.settlementScheme = raw.settlement_scheme ?? null;
|
|
363
|
+
}
|
|
364
|
+
if ("budget_delegation_hash" in raw) {
|
|
365
|
+
receipt.budgetDelegationHash = raw.budget_delegation_hash ?? null;
|
|
366
|
+
}
|
|
367
|
+
if ("delivery_quality" in raw) {
|
|
368
|
+
receipt.deliveryQuality = raw.delivery_quality ?? null;
|
|
369
|
+
}
|
|
370
|
+
if ("delivery_note" in raw) {
|
|
371
|
+
receipt.deliveryNote = raw.delivery_note ?? null;
|
|
372
|
+
}
|
|
373
|
+
if ("delivery_reported_at" in raw) {
|
|
374
|
+
receipt.deliveryReportedAt = raw.delivery_reported_at ?? null;
|
|
375
|
+
}
|
|
376
|
+
if ("delivery_reference" in raw) {
|
|
377
|
+
receipt.deliveryReference = raw.delivery_reference ?? null;
|
|
378
|
+
}
|
|
357
379
|
return receipt;
|
|
358
380
|
}
|
|
359
381
|
|
|
@@ -473,7 +495,7 @@ function throwPaymentStateError(label, raw) {
|
|
|
473
495
|
throw new HavenPaymentStateError(state.message, statusCode, state, raw);
|
|
474
496
|
}
|
|
475
497
|
if (raw.status === "pending_approval") {
|
|
476
|
-
throw
|
|
498
|
+
throw HavenApiError.deterministic(
|
|
477
499
|
`${label} exceeds the agent's on-chain budget and was declined; no approval is pending and none will arrive (payment_id: ${raw.payment_id}).`,
|
|
478
500
|
statusCode,
|
|
479
501
|
raw
|
|
@@ -488,6 +510,7 @@ function throwPaymentStateError(label, raw) {
|
|
|
488
510
|
}
|
|
489
511
|
const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
|
|
490
512
|
const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
|
|
513
|
+
if (statusCode >= 500) throw HavenApiError.deterministic(message, statusCode, raw);
|
|
491
514
|
throw new HavenApiError(message, statusCode, raw);
|
|
492
515
|
}
|
|
493
516
|
|
|
@@ -542,12 +565,30 @@ var MerchantEgressRefusedError = class extends HavenError {
|
|
|
542
565
|
url;
|
|
543
566
|
refusal;
|
|
544
567
|
beforeRequest;
|
|
545
|
-
|
|
546
|
-
|
|
568
|
+
/** #4257: the redirect status that was not followed, when a redirect was refused. */
|
|
569
|
+
redirectStatus;
|
|
570
|
+
/**
|
|
571
|
+
* #4257: set only when `url` IS the redirect target (a cross-origin body
|
|
572
|
+
* re-send, a spent hop budget, a hop `assertUrl` refused). A refusal that
|
|
573
|
+
* stopped at the answering URL itself (a non-GET redirect, a missing or
|
|
574
|
+
* unresolvable Location) leaves it unset, and `url` is that answering URL.
|
|
575
|
+
*/
|
|
576
|
+
redirectTarget;
|
|
577
|
+
/**
|
|
578
|
+
* #4257: set when the refusal interrupted a PAID delivery and the local
|
|
579
|
+
* completion has re-thrown it with the payment it belongs to — the funded,
|
|
580
|
+
* undelivered state, never "nothing happened".
|
|
581
|
+
*/
|
|
582
|
+
settlementScheme;
|
|
583
|
+
constructor(message, url, refusal, beforeRequest, extra = {}) {
|
|
584
|
+
super(message, MERCHANT_EGRESS_REFUSED_CODE, 400, extra.paymentId);
|
|
547
585
|
this.name = "MerchantEgressRefusedError";
|
|
548
586
|
this.url = url;
|
|
549
587
|
this.refusal = refusal;
|
|
550
588
|
this.beforeRequest = beforeRequest;
|
|
589
|
+
this.redirectStatus = extra.redirectStatus;
|
|
590
|
+
this.redirectTarget = extra.redirectTarget;
|
|
591
|
+
this.settlementScheme = extra.settlementScheme;
|
|
551
592
|
}
|
|
552
593
|
};
|
|
553
594
|
var MERCHANT_EGRESS_RESPONSE_CAP_CODE = "MERCHANT_EGRESS_RESPONSE_CAP";
|
|
@@ -662,6 +703,29 @@ var MCP_X402_PAYMENT_META_KEY = "x402/payment";
|
|
|
662
703
|
var MCP_X402_PAYMENT_RESPONSE_META_KEY = "x402/payment-response";
|
|
663
704
|
var X402_RETRY_REJECTED_STATUS = 402;
|
|
664
705
|
var SESSION_NOT_FOUND_NEXT_ACTION = "reinitialize_then_retry_same_payment_header";
|
|
706
|
+
var CROSS_ORIGIN_REDIRECT_HEADERS = /* @__PURE__ */ new Set([
|
|
707
|
+
"accept",
|
|
708
|
+
"accept-language",
|
|
709
|
+
"user-agent",
|
|
710
|
+
"x402-wallet"
|
|
711
|
+
]);
|
|
712
|
+
function crossOriginRedirectHeaders(headers) {
|
|
713
|
+
const kept = new Headers();
|
|
714
|
+
for (const [name, value] of new Headers(headers)) {
|
|
715
|
+
if (CROSS_ORIGIN_REDIRECT_HEADERS.has(name)) kept.set(name, value);
|
|
716
|
+
}
|
|
717
|
+
return kept;
|
|
718
|
+
}
|
|
719
|
+
var NO_POLICY_MAX_REDIRECTS = 20;
|
|
720
|
+
var REQUEST_BODY_HEADERS = ["content-encoding", "content-language", "content-location", "content-type", "content-length"];
|
|
721
|
+
function redirectedInit(init, status) {
|
|
722
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
723
|
+
const toGet = (status === 301 || status === 302) && method === "POST" || status === 303 && method !== "GET" && method !== "HEAD";
|
|
724
|
+
if (!toGet) return init;
|
|
725
|
+
const headers = new Headers(init.headers);
|
|
726
|
+
for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
|
|
727
|
+
return { ...init, method: "GET", body: null, headers };
|
|
728
|
+
}
|
|
665
729
|
var McpMerchantTransport = class {
|
|
666
730
|
merchantTimeout;
|
|
667
731
|
fetchImpl;
|
|
@@ -692,11 +756,24 @@ var McpMerchantTransport = class {
|
|
|
692
756
|
}
|
|
693
757
|
/** Fetch a merchant with a settlement-sized timeout and caller cancellation. */
|
|
694
758
|
async fetch(url, init = {}, timeoutMs = this.merchantTimeout, maxBytes) {
|
|
759
|
+
return this.send(url, init, timeoutMs, maxBytes, false);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* `fetch`, plus whether this request is the paid delivery (#4257). A paid
|
|
763
|
+
* delivery, or any body carrying an x402 payment in `_meta`, is never
|
|
764
|
+
* re-sent to another origin by a redirect that keeps the body; without a policy every other
|
|
765
|
+
* request follows that redirect as the platform would, headers reduced.
|
|
766
|
+
*/
|
|
767
|
+
async send(url, init, timeoutMs, maxBytes, paidDelivery) {
|
|
768
|
+
const carriesPayment = paidDelivery || bodyCarriesX402Payment(init.body);
|
|
695
769
|
if (!this.egress) {
|
|
696
770
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
697
771
|
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
698
772
|
try {
|
|
699
|
-
|
|
773
|
+
if (init.redirect === "manual" || init.redirect === "error") {
|
|
774
|
+
return await this.fetchImpl(url, { ...init, signal });
|
|
775
|
+
}
|
|
776
|
+
return await this.followRedirects(url, init, signal, void 0, carriesPayment);
|
|
700
777
|
} catch (err) {
|
|
701
778
|
if (timeoutSignal.aborted) {
|
|
702
779
|
throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
|
|
@@ -704,87 +781,144 @@ var McpMerchantTransport = class {
|
|
|
704
781
|
throw err;
|
|
705
782
|
}
|
|
706
783
|
}
|
|
707
|
-
return this.fetchUnderPolicy(url, init, timeoutMs, maxBytes);
|
|
784
|
+
return this.fetchUnderPolicy(url, init, timeoutMs, maxBytes, carriesPayment);
|
|
708
785
|
}
|
|
709
786
|
/**
|
|
710
787
|
* #3747: the policy path. Every URL — the initial request and each redirect
|
|
711
788
|
* hop — is `assertUrl`-checked BEFORE it connects; redirects run `manual`
|
|
712
789
|
* so nothing is ever followed un-checked; GET redirects are followed up to
|
|
713
|
-
* `maxGetRedirects` (a redirect on any other method is refused);
|
|
714
|
-
*
|
|
790
|
+
* `maxGetRedirects` (a redirect on any other method is refused); a hop to
|
|
791
|
+
* another origin carries only `CROSS_ORIGIN_REDIRECT_HEADERS` (#4244);
|
|
792
|
+
* response bodies are capped WHILE being read, not after buffering. The timeout
|
|
715
793
|
* signal spans the WHOLE redirect chain, and `MerchantTimeoutError`
|
|
716
794
|
* classification is preserved.
|
|
717
795
|
*/
|
|
718
|
-
async fetchUnderPolicy(url, init, timeoutMs, maxBytes) {
|
|
796
|
+
async fetchUnderPolicy(url, init, timeoutMs, maxBytes, carriesPayment) {
|
|
719
797
|
const egress = this.egress;
|
|
720
798
|
try {
|
|
721
799
|
egress.assertUrl(url);
|
|
722
800
|
} catch (err) {
|
|
723
801
|
throw asRefusal(err, url, true);
|
|
724
802
|
}
|
|
725
|
-
const method = (init.method ?? "GET").toUpperCase();
|
|
726
803
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
727
804
|
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
728
|
-
|
|
805
|
+
let finalUrl = url;
|
|
806
|
+
try {
|
|
807
|
+
const response = await this.followRedirects(url, init, signal, egress, carriesPayment, (hopUrl) => {
|
|
808
|
+
finalUrl = hopUrl;
|
|
809
|
+
});
|
|
810
|
+
return this.capResponse(response, finalUrl, maxBytes);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
if (timeoutSignal.aborted) {
|
|
813
|
+
throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${finalUrl}`);
|
|
814
|
+
}
|
|
815
|
+
throw err;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* The redirect follower both paths share (#3747, #4244, #4257). Redirects
|
|
820
|
+
* run `manual` and every hop is decided here:
|
|
821
|
+
*
|
|
822
|
+
* - Under a `policy`, every hop is `assertUrl`-checked before it connects,
|
|
823
|
+
* only GET redirects are followed (any other method is refused), up to
|
|
824
|
+
* `maxGetRedirects`; a missing or unresolvable Location is refused.
|
|
825
|
+
* - Without one, the platform's rules are kept: up to
|
|
826
|
+
* `NO_POLICY_MAX_REDIRECTS` hops, the Fetch spec's method/body rewrite,
|
|
827
|
+
* and a 3xx with no Location returned as the answer.
|
|
828
|
+
* - On both, a hop to another origin carries only
|
|
829
|
+
* `CROSS_ORIGIN_REDIRECT_HEADERS`, never restored on a later hop.
|
|
830
|
+
* - A hop that would re-send a request body to another origin is refused
|
|
831
|
+
* under a policy (which follows GET alone, so this is unreachable there),
|
|
832
|
+
* and without one only when `carriesPayment`: the paid delivery, or a body
|
|
833
|
+
* carrying an x402 payment in `_meta` (#4257). Any other body-keeping redirect is
|
|
834
|
+
* followed with its body, as the platform did, headers reduced.
|
|
835
|
+
*
|
|
836
|
+
* `onHop` sees every URL a request goes to, so the caller can name the hop
|
|
837
|
+
* it was on when the chain stops.
|
|
838
|
+
*/
|
|
839
|
+
async followRedirects(url, init, signal, policy, carriesPayment, onHop) {
|
|
840
|
+
const maxRedirects = policy ? policy.maxGetRedirects ?? 0 : NO_POLICY_MAX_REDIRECTS;
|
|
729
841
|
let currentUrl = url;
|
|
842
|
+
let hopInit = init;
|
|
730
843
|
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
|
-
|
|
844
|
+
while (true) {
|
|
845
|
+
onHop?.(currentUrl);
|
|
846
|
+
const response = await this.fetchImpl(currentUrl, {
|
|
847
|
+
...hopInit,
|
|
848
|
+
redirect: init.redirect === "error" ? "error" : "manual",
|
|
849
|
+
signal
|
|
850
|
+
});
|
|
851
|
+
if (!isRedirectStatus(response.status)) return response;
|
|
852
|
+
const method = (hopInit.method ?? "GET").toUpperCase();
|
|
853
|
+
if (policy && method !== "GET") {
|
|
854
|
+
throw new MerchantEgressRefusedError(
|
|
855
|
+
`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.`,
|
|
856
|
+
currentUrl,
|
|
857
|
+
"redirect_on_non_get",
|
|
858
|
+
false,
|
|
859
|
+
{ redirectStatus: response.status }
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
const location = response.headers.get("location");
|
|
863
|
+
if (!location) {
|
|
864
|
+
if (!policy) return response;
|
|
865
|
+
throw new MerchantEgressRefusedError(
|
|
866
|
+
`Hosted egress policy refuses the ${response.status} redirect from ${currentUrl}: it carries no Location header, so there is no checked target to follow.`,
|
|
867
|
+
currentUrl,
|
|
868
|
+
"redirect_missing_location",
|
|
869
|
+
false,
|
|
870
|
+
{ redirectStatus: response.status }
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
let next;
|
|
874
|
+
try {
|
|
875
|
+
next = new URL(location, currentUrl).toString();
|
|
876
|
+
} catch {
|
|
877
|
+
if (!policy) throw new TypeError(`fetch failed: the redirect from ${currentUrl} has an unresolvable Location header`);
|
|
878
|
+
throw new MerchantEgressRefusedError(
|
|
879
|
+
`Hosted egress policy refuses the redirect from ${currentUrl}: its Location header is not a resolvable URL.`,
|
|
880
|
+
currentUrl,
|
|
881
|
+
"redirect_invalid_location",
|
|
882
|
+
false,
|
|
883
|
+
{ redirectStatus: response.status }
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (!policy && !/^https?:$/.test(new URL(next).protocol)) {
|
|
887
|
+
throw new TypeError(`fetch failed: the redirect from ${currentUrl} names a non-HTTP(S) URL`);
|
|
888
|
+
}
|
|
889
|
+
if (hops >= maxRedirects) {
|
|
890
|
+
if (!policy) throw new TypeError(`fetch failed: redirect count exceeded (${maxRedirects}) at ${currentUrl}`);
|
|
891
|
+
throw new MerchantEgressRefusedError(
|
|
892
|
+
`Hosted egress policy refuses the redirect from ${currentUrl} to ${next}: the ${maxRedirects}-redirect budget for this request is spent.`,
|
|
893
|
+
next,
|
|
894
|
+
"redirect_budget_exceeded",
|
|
895
|
+
false,
|
|
896
|
+
{ redirectStatus: response.status, redirectTarget: next }
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
if (policy) {
|
|
757
900
|
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
|
-
);
|
|
901
|
+
policy.assertUrl(next);
|
|
902
|
+
} catch (err) {
|
|
903
|
+
throw asRefusal(err, next, false, { redirectStatus: response.status, redirectTarget: next });
|
|
766
904
|
}
|
|
767
|
-
|
|
905
|
+
}
|
|
906
|
+
hopInit = redirectedInit(hopInit, response.status);
|
|
907
|
+
if (new URL(next).origin !== new URL(currentUrl).origin) {
|
|
908
|
+
if (hopInit.body != null && (policy || carriesPayment)) {
|
|
768
909
|
throw new MerchantEgressRefusedError(
|
|
769
|
-
`
|
|
910
|
+
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
911
|
next,
|
|
771
|
-
"
|
|
772
|
-
false
|
|
912
|
+
"redirect_cross_origin_body",
|
|
913
|
+
false,
|
|
914
|
+
{ redirectStatus: response.status, redirectTarget: next }
|
|
773
915
|
);
|
|
774
916
|
}
|
|
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}`);
|
|
917
|
+
hopInit = { ...hopInit, headers: crossOriginRedirectHeaders(hopInit.headers) };
|
|
786
918
|
}
|
|
787
|
-
|
|
919
|
+
await response.body?.cancel().catch(() => void 0);
|
|
920
|
+
hops++;
|
|
921
|
+
currentUrl = next;
|
|
788
922
|
}
|
|
789
923
|
}
|
|
790
924
|
/**
|
|
@@ -962,7 +1096,7 @@ var McpMerchantTransport = class {
|
|
|
962
1096
|
if (send.includes(name)) headers.set(name, paymentHeader);
|
|
963
1097
|
else headers.delete(name);
|
|
964
1098
|
}
|
|
965
|
-
return this.
|
|
1099
|
+
return this.send(url, withMcpPaymentMeta({ ...init, headers }, paymentHeader), this.budgetFor("delivery"), this.capFor("delivery"), true);
|
|
966
1100
|
}
|
|
967
1101
|
/**
|
|
968
1102
|
* #3171: deliver the payment; if the merchant answers its unknown-session
|
|
@@ -1099,6 +1233,27 @@ function withMcpPaymentMeta(init, paymentHeader) {
|
|
|
1099
1233
|
function encodeMcpSettlementReceipt(settlement) {
|
|
1100
1234
|
return encodeBase64Json(settlement);
|
|
1101
1235
|
}
|
|
1236
|
+
function bodyCarriesX402Payment(body) {
|
|
1237
|
+
let text;
|
|
1238
|
+
if (typeof body === "string") text = body;
|
|
1239
|
+
else if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
|
|
1240
|
+
try {
|
|
1241
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
1242
|
+
} catch {
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
} else return false;
|
|
1246
|
+
let request;
|
|
1247
|
+
try {
|
|
1248
|
+
request = JSON.parse(text);
|
|
1249
|
+
} catch {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
return Array.isArray(request) ? request.some(messageCarriesX402Payment) : messageCarriesX402Payment(request);
|
|
1253
|
+
}
|
|
1254
|
+
function messageCarriesX402Payment(message) {
|
|
1255
|
+
return isRecord(message) && isRecord(message.params) && isRecord(message.params._meta) && message.params._meta[MCP_X402_PAYMENT_META_KEY] !== void 0;
|
|
1256
|
+
}
|
|
1102
1257
|
function parseJsonRpcToolsCall(body) {
|
|
1103
1258
|
if (typeof body !== "string") return void 0;
|
|
1104
1259
|
let request;
|
|
@@ -1137,10 +1292,10 @@ function isRecord(value) {
|
|
|
1137
1292
|
function isRedirectStatus(status) {
|
|
1138
1293
|
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
1139
1294
|
}
|
|
1140
|
-
function asRefusal(err, url, beforeRequest) {
|
|
1295
|
+
function asRefusal(err, url, beforeRequest, redirect) {
|
|
1141
1296
|
if (!(err instanceof MerchantEgressRefusedError)) return err;
|
|
1142
|
-
if (err.beforeRequest === beforeRequest) return err;
|
|
1143
|
-
return new MerchantEgressRefusedError(err.message, url, err.refusal, beforeRequest);
|
|
1297
|
+
if (err.beforeRequest === beforeRequest && !redirect) return err;
|
|
1298
|
+
return new MerchantEgressRefusedError(err.message, url, err.refusal, beforeRequest, redirect);
|
|
1144
1299
|
}
|
|
1145
1300
|
async function captureMerchantResponse(response) {
|
|
1146
1301
|
const merchant_body = await response.text().catch((err) => {
|
|
@@ -1214,6 +1369,17 @@ function deriveReadiness(status, allowances) {
|
|
|
1214
1369
|
if (status !== "active") return "revoked";
|
|
1215
1370
|
return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
|
|
1216
1371
|
}
|
|
1372
|
+
function deriveBudgetRefillsAt(readiness, allowances) {
|
|
1373
|
+
if (readiness !== "needs_approval" || allowances.length === 0) return null;
|
|
1374
|
+
const times = allowances.map((allowance) => allowance.nextRefillAt).filter((at) => at !== null && !Number.isNaN(Date.parse(at)));
|
|
1375
|
+
if (times.length === 0) return null;
|
|
1376
|
+
return times.reduce((earliest, at) => Date.parse(at) < Date.parse(earliest) ? at : earliest);
|
|
1377
|
+
}
|
|
1378
|
+
function deriveBudgetEndedAt(readiness, allowances, budgetEndedAt) {
|
|
1379
|
+
if (readiness !== "needs_approval" || allowances.length > 0) return null;
|
|
1380
|
+
if (!budgetEndedAt || Number.isNaN(Date.parse(budgetEndedAt))) return null;
|
|
1381
|
+
return budgetEndedAt;
|
|
1382
|
+
}
|
|
1217
1383
|
function formatRemainingDisplay(tokenAddress, tokenSymbol, remainingAtomic) {
|
|
1218
1384
|
const token = resolveTokenFromAddress(tokenAddress);
|
|
1219
1385
|
return token ? `${formatAtomicAmount(safeBigInt(remainingAtomic), token.decimals)} ${tokenSymbol}` : `${remainingAtomic} ${tokenSymbol} (atomic; unknown decimals)`;
|
|
@@ -1260,6 +1426,7 @@ function mapTaskBudget(raw) {
|
|
|
1260
1426
|
status: raw.status,
|
|
1261
1427
|
expiresAt: raw.expires_at,
|
|
1262
1428
|
isExpired: raw.is_expired,
|
|
1429
|
+
...raw.is_live === void 0 ? {} : { isLive: raw.is_live },
|
|
1263
1430
|
createdAt: raw.created_at,
|
|
1264
1431
|
openedAt: raw.opened_at,
|
|
1265
1432
|
closedAt: raw.closed_at,
|
|
@@ -1381,11 +1548,23 @@ var AccountReads = class {
|
|
|
1381
1548
|
isResetPending: allowance.onchain.isResetPending,
|
|
1382
1549
|
// #3731: same provenance as the detailed read's field — the compact
|
|
1383
1550
|
// row is a projection of the same allowance, never a second source.
|
|
1384
|
-
fundsCoverRemaining: allowance.fundsCoverRemaining
|
|
1551
|
+
fundsCoverRemaining: allowance.fundsCoverRemaining,
|
|
1552
|
+
nextRefillAt: allowance.nextRefillAt
|
|
1385
1553
|
};
|
|
1386
1554
|
});
|
|
1387
1555
|
const readiness = deriveReadiness(agent.status, allowances);
|
|
1388
|
-
|
|
1556
|
+
const budgetRefillsAt = deriveBudgetRefillsAt(readiness, allowances);
|
|
1557
|
+
const budgetEndedAt = deriveBudgetEndedAt(readiness, allowances, allowanceSummary.budgetEndedAt);
|
|
1558
|
+
return {
|
|
1559
|
+
...agent,
|
|
1560
|
+
readiness,
|
|
1561
|
+
spend_authority_readiness: readiness,
|
|
1562
|
+
budgetRefillsAt,
|
|
1563
|
+
budgetEndedAt,
|
|
1564
|
+
allowances,
|
|
1565
|
+
taskBudgets,
|
|
1566
|
+
pendingSubBudgetSignatures
|
|
1567
|
+
};
|
|
1389
1568
|
}
|
|
1390
1569
|
/**
|
|
1391
1570
|
* #3329: `GET /task-budgets?status=open`, mapped to the condensed summary
|
|
@@ -1413,7 +1592,8 @@ var AccountReads = class {
|
|
|
1413
1592
|
* an agent can still act on, and read-by-id (`haven_get_task_budget`) is
|
|
1414
1593
|
* the any-status surface for everything else:
|
|
1415
1594
|
*
|
|
1416
|
-
* - `closing`
|
|
1595
|
+
* - `closing` stays visible until its expiry (#3773; an expired `closing`
|
|
1596
|
+
* row drops, #3923) — the close submit is
|
|
1417
1597
|
* in flight and the signer's close refusal tells the caller to re-check
|
|
1418
1598
|
* exactly this row's status;
|
|
1419
1599
|
* - `closed` rows drop (terminal — `closed_at` names what happened, and
|
|
@@ -1422,10 +1602,14 @@ var AccountReads = class {
|
|
|
1422
1602
|
* budget reserves nothing and pays nothing);
|
|
1423
1603
|
* - unexpired `pending` and `open` rows stay.
|
|
1424
1604
|
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
* `
|
|
1605
|
+
* A filter also runs here, client-side. A row carrying the backend's
|
|
1606
|
+
* `is_live` (#3922) is kept exactly when it is true — the server's own
|
|
1607
|
+
* `status=live` verdict. A row without it (an older backend) is derived by
|
|
1608
|
+
* the same rule: `closing` stays until its expiry, then drops (#3923), as
|
|
1609
|
+
* the backend does since #3773. That is
|
|
1610
|
+
* what keeps the list bounded against a backend that predates `status=live`,
|
|
1611
|
+
* which refuses the value with a 400 — that refusal (and only that) falls
|
|
1612
|
+
* back to `status=all`. A row whose wire carries
|
|
1429
1613
|
* no `is_expired` is treated as unexpired — the degraded read errs toward
|
|
1430
1614
|
* KEEPING rows, never toward hiding one the agent could still act on.
|
|
1431
1615
|
*/
|
|
@@ -1439,7 +1623,9 @@ var AccountReads = class {
|
|
|
1439
1623
|
});
|
|
1440
1624
|
const rows = raw?.task_budgets;
|
|
1441
1625
|
if (!Array.isArray(rows)) return [];
|
|
1442
|
-
return rows.map((row) => mapTaskBudget(row)).filter(
|
|
1626
|
+
return rows.map((row) => mapTaskBudget(row)).filter(
|
|
1627
|
+
(tb) => tb.isLive !== void 0 ? tb.isLive : !tb.isExpired && tb.status !== "closed"
|
|
1628
|
+
).map((tb) => summarizeTaskBudget(tb));
|
|
1443
1629
|
} catch {
|
|
1444
1630
|
return [];
|
|
1445
1631
|
}
|
|
@@ -1474,6 +1660,9 @@ var AccountReads = class {
|
|
|
1474
1660
|
accountAddress: raw.account_address,
|
|
1475
1661
|
delegateAddress: raw.delegate_address,
|
|
1476
1662
|
chainId: raw.chain_id,
|
|
1663
|
+
// #4352: a non-empty parseable date → itself; anything else (absent on
|
|
1664
|
+
// an older backend, null when no budget has ended) → null.
|
|
1665
|
+
budgetEndedAt: typeof raw.budget_ended_at === "string" && !Number.isNaN(Date.parse(raw.budget_ended_at)) ? raw.budget_ended_at : null,
|
|
1477
1666
|
supersededBy: mapSupersession(raw.superseded_by),
|
|
1478
1667
|
allowances: raw.allowances.map((allowance) => ({
|
|
1479
1668
|
id: allowance.id,
|
|
@@ -1492,6 +1681,9 @@ var AccountReads = class {
|
|
|
1492
1681
|
// "unverifiable", not a missing key. The key is absent on the wire
|
|
1493
1682
|
// when the remaining is 0; that maps to null here too.
|
|
1494
1683
|
fundsCoverRemaining: allowance.funds_cover_remaining === true || allowance.funds_cover_remaining === false ? allowance.funds_cover_remaining : null,
|
|
1684
|
+
// #4355: a non-empty string → itself; anything else (absent on an
|
|
1685
|
+
// older backend, null when the budget never refills) → null.
|
|
1686
|
+
nextRefillAt: typeof allowance.next_refill_at === "string" && allowance.next_refill_at !== "" ? allowance.next_refill_at : null,
|
|
1495
1687
|
remainingDisplay: formatRemainingDisplay(allowance.token_address, allowance.token_symbol, allowance.onchain.remaining),
|
|
1496
1688
|
onchain: {
|
|
1497
1689
|
amount: allowance.onchain.amount,
|
|
@@ -2117,7 +2309,7 @@ var X402FundingLeg = class {
|
|
|
2117
2309
|
}
|
|
2118
2310
|
this.assertSignableAuthorizationState("x402 payment", raw);
|
|
2119
2311
|
if (!raw.sign_data?.hash) {
|
|
2120
|
-
throw
|
|
2312
|
+
throw HavenApiError.deterministic("No sign_hash returned from x402/authorize", 500, raw);
|
|
2121
2313
|
}
|
|
2122
2314
|
const sig = await this.signForData(raw.sign_data, {
|
|
2123
2315
|
asset: option.asset,
|
|
@@ -2239,20 +2431,27 @@ var X402FundingLeg = class {
|
|
|
2239
2431
|
* Skipped when `chainRpcs` does not include the chain; in that case Haven's
|
|
2240
2432
|
* backend has already confirmed on-chain submission and callers accept the
|
|
2241
2433
|
* small propagation window as a trade-off for not configuring an RPC URL.
|
|
2434
|
+
*
|
|
2435
|
+
* #4470: the two outcomes are typed apart. ethers 6 `waitForTransaction`
|
|
2436
|
+
* REJECTS on its timeout (and on an RPC failure) rather than resolving
|
|
2437
|
+
* null, so that path is `FundingConfirmationUnknownError` — funding may
|
|
2438
|
+
* have moved. A mined receipt with `status !== 1` is `FundingRevertedError`.
|
|
2242
2439
|
*/
|
|
2243
2440
|
async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
|
|
2244
2441
|
if (!txHash || !chainId) return;
|
|
2245
2442
|
const rpcUrl = this.chainRpcs[chainId];
|
|
2246
2443
|
if (!rpcUrl) return;
|
|
2247
2444
|
const provider = createJsonRpcProvider(rpcUrl);
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
{ txHash, chainId }
|
|
2254
|
-
);
|
|
2445
|
+
let onChainReceipt;
|
|
2446
|
+
try {
|
|
2447
|
+
onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
throw new FundingConfirmationUnknownError(txHash, chainId, err instanceof Error ? err.message : String(err));
|
|
2255
2450
|
}
|
|
2451
|
+
if (!onChainReceipt) {
|
|
2452
|
+
throw new FundingConfirmationUnknownError(txHash, chainId, "no receipt within the timeout window");
|
|
2453
|
+
}
|
|
2454
|
+
if (onChainReceipt.status !== 1) throw new FundingRevertedError(txHash, chainId);
|
|
2256
2455
|
}
|
|
2257
2456
|
/**
|
|
2258
2457
|
* Can the delegate EOA still fund an authorization for `amountAtomic`?
|
|
@@ -2434,7 +2633,7 @@ var X402Erc7710 = class {
|
|
|
2434
2633
|
...options.subBudgetId ? { subBudgetId: options.subBudgetId } : {}
|
|
2435
2634
|
});
|
|
2436
2635
|
if (!raw.payment_id) {
|
|
2437
|
-
throw
|
|
2636
|
+
throw HavenApiError.deterministic("No payment_id returned from x402/authorize", 500, raw);
|
|
2438
2637
|
}
|
|
2439
2638
|
if (raw.status === "confirmed" && raw.tx_hash) {
|
|
2440
2639
|
const requestedUrl = options.resourceUrl ?? paymentRequired.resource?.url;
|
|
@@ -2451,7 +2650,7 @@ var X402Erc7710 = class {
|
|
|
2451
2650
|
}
|
|
2452
2651
|
const signData = raw.sign_data;
|
|
2453
2652
|
if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
|
|
2454
|
-
throw
|
|
2653
|
+
throw HavenApiError.deterministic(
|
|
2455
2654
|
`x402/authorize did not return an erc7710 settlement child (signature_scheme was ${JSON.stringify(signData?.signature_scheme)}). Refusing to sign a payload this path did not ask for.`,
|
|
2456
2655
|
500,
|
|
2457
2656
|
raw
|
|
@@ -2494,7 +2693,7 @@ var X402Erc7710 = class {
|
|
|
2494
2693
|
throw err;
|
|
2495
2694
|
}
|
|
2496
2695
|
if (!settled.payment_header) {
|
|
2497
|
-
throw
|
|
2696
|
+
throw HavenApiError.deterministic(
|
|
2498
2697
|
"x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
|
|
2499
2698
|
500,
|
|
2500
2699
|
settled
|
|
@@ -2615,6 +2814,26 @@ function toolError(err) {
|
|
|
2615
2814
|
};
|
|
2616
2815
|
}
|
|
2617
2816
|
|
|
2817
|
+
// src/merchant-answer.ts
|
|
2818
|
+
function decidePostFundingMerchantAnswer(input) {
|
|
2819
|
+
const inBandChallenge = input.toolResult ? extractMcpPaymentRequired(input.toolResult) : void 0;
|
|
2820
|
+
const headerReceipt = input.paymentResponseHeader;
|
|
2821
|
+
const protocolReceiptHeaderName = headerReceipt ? "PAYMENT-RESPONSE" : input.metaSettlement ? `_meta.${MCP_X402_PAYMENT_RESPONSE_META_KEY}` : void 0;
|
|
2822
|
+
const protocolReceiptHeader = headerReceipt ?? (input.metaSettlement ? encodeMcpSettlementReceipt(input.metaSettlement) : void 0);
|
|
2823
|
+
const settlement = parseMerchantSettlement(headerReceipt ?? null);
|
|
2824
|
+
const settlementTxHash = settlement.settlementTxHash ?? (typeof input.metaSettlement?.transaction === "string" ? input.metaSettlement.transaction : void 0);
|
|
2825
|
+
const plainToolErrorAfterFunding = input.toolResult?.isError === true && inBandChallenge === void 0 && !settlementTxHash;
|
|
2826
|
+
const inBandRejection = inBandChallenge !== void 0 || input.metaSettlement?.success === false || plainToolErrorAfterFunding;
|
|
2827
|
+
return {
|
|
2828
|
+
accepted: input.httpOk && !inBandRejection,
|
|
2829
|
+
inBandRejection,
|
|
2830
|
+
plainToolErrorAfterFunding,
|
|
2831
|
+
settlementTxHash,
|
|
2832
|
+
protocolReceiptHeader,
|
|
2833
|
+
protocolReceiptHeaderName
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2618
2837
|
// src/merchant-completion.ts
|
|
2619
2838
|
var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
|
|
2620
2839
|
var EVIDENCE_RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
|
|
@@ -2645,10 +2864,10 @@ var MerchantCompletion = class {
|
|
|
2645
2864
|
}
|
|
2646
2865
|
async retryRequest(url, initialInit, paymentRequired, receipt) {
|
|
2647
2866
|
if (!receipt.accepted) {
|
|
2648
|
-
throw
|
|
2867
|
+
throw HavenApiError.deterministic("No accepted x402 option was recorded for payment retry", 500);
|
|
2649
2868
|
}
|
|
2650
2869
|
if (!receipt.paymentHeader) {
|
|
2651
|
-
throw
|
|
2870
|
+
throw HavenApiError.deterministic("No x402 payment header was returned for payment retry", 500);
|
|
2652
2871
|
}
|
|
2653
2872
|
let retryInit = initialInit;
|
|
2654
2873
|
if (this.getTaxDeclarationHeader && this.delegateKey) {
|
|
@@ -2664,16 +2883,31 @@ var MerchantCompletion = class {
|
|
|
2664
2883
|
retryInit = withTaxDeclarationHeader(initialInit, void 0);
|
|
2665
2884
|
}
|
|
2666
2885
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2886
|
+
let retryResponse;
|
|
2887
|
+
try {
|
|
2888
|
+
retryResponse = await this.merchantTransport.deliverPaymentRecoveringSession(
|
|
2889
|
+
url,
|
|
2890
|
+
retryInit,
|
|
2891
|
+
receipt.paymentHeader,
|
|
2892
|
+
() => this.merchantTransport.initialize(url, initialInit)
|
|
2893
|
+
);
|
|
2894
|
+
} catch (err) {
|
|
2895
|
+
if (err instanceof MerchantEgressRefusedError && !err.beforeRequest) {
|
|
2896
|
+
throw await this.paidDeliveryInterrupted(err, url, receipt, receipt.paymentHeader, retryInit?.body != null);
|
|
2897
|
+
}
|
|
2898
|
+
throw err;
|
|
2899
|
+
}
|
|
2673
2900
|
const toolResult = retryResponse.ok ? await this.merchantTransport.readToolResult(retryResponse) : void 0;
|
|
2674
|
-
const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
|
|
2675
2901
|
const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
|
|
2676
|
-
const
|
|
2902
|
+
const headerReceipt = retryResponse.headers.get("PAYMENT-RESPONSE");
|
|
2903
|
+
const answer = decidePostFundingMerchantAnswer({
|
|
2904
|
+
toolResult,
|
|
2905
|
+
httpOk: retryResponse.ok,
|
|
2906
|
+
paymentResponseHeader: headerReceipt ?? void 0,
|
|
2907
|
+
metaSettlement
|
|
2908
|
+
});
|
|
2909
|
+
const reportedSettlementTxHash = answer.settlementTxHash;
|
|
2910
|
+
const inBandRejection = answer.inBandRejection;
|
|
2677
2911
|
if (!retryResponse.ok || inBandRejection) {
|
|
2678
2912
|
const merchant = await captureMerchantResponse(retryResponse);
|
|
2679
2913
|
await this.recordRetryRejected({
|
|
@@ -2688,7 +2922,7 @@ var MerchantCompletion = class {
|
|
|
2688
2922
|
}
|
|
2689
2923
|
});
|
|
2690
2924
|
throw new HavenApiError(
|
|
2691
|
-
"x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
|
|
2925
|
+
"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
2926
|
inBandRejection ? X402_RETRY_REJECTED_STATUS : merchant.merchant_status,
|
|
2693
2927
|
{
|
|
2694
2928
|
marker: "x402_retry_rejected_after_funding",
|
|
@@ -2701,10 +2935,10 @@ var MerchantCompletion = class {
|
|
|
2701
2935
|
}
|
|
2702
2936
|
);
|
|
2703
2937
|
}
|
|
2704
|
-
const
|
|
2705
|
-
const
|
|
2706
|
-
|
|
2707
|
-
|
|
2938
|
+
const { protocolReceiptHeader, protocolReceiptHeaderName } = answer;
|
|
2939
|
+
const merchantSettlement = {
|
|
2940
|
+
settlementTxHash: reportedSettlementTxHash ?? null
|
|
2941
|
+
};
|
|
2708
2942
|
if (receipt.merchant && merchantSettlement.settlementTxHash) {
|
|
2709
2943
|
receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
|
|
2710
2944
|
receipt.merchant.settlementExplorerUrl = buildExplorerUrl(receipt.chainId, merchantSettlement.settlementTxHash) || null;
|
|
@@ -2724,7 +2958,12 @@ var MerchantCompletion = class {
|
|
|
2724
2958
|
});
|
|
2725
2959
|
const settlementCandidate = merchantSettlement.settlementTxHash;
|
|
2726
2960
|
if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && settlementCandidate.toLowerCase() !== receipt.txHash.toLowerCase()) {
|
|
2727
|
-
await this.
|
|
2961
|
+
await this.reportEvidence({
|
|
2962
|
+
paymentId: receipt.paymentId,
|
|
2963
|
+
rail: "x402",
|
|
2964
|
+
txHash: settlementCandidate,
|
|
2965
|
+
...settlementProofFields(receipt.paymentHeader)
|
|
2966
|
+
});
|
|
2728
2967
|
}
|
|
2729
2968
|
await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
|
|
2730
2969
|
return retryResponse;
|
|
@@ -2827,6 +3066,51 @@ var MerchantCompletion = class {
|
|
|
2827
3066
|
// and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
|
|
2828
3067
|
// surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
|
|
2829
3068
|
// for agent-to-merchant payments.
|
|
3069
|
+
/**
|
|
3070
|
+
* #4257: record a paid delivery that a refused redirect interrupted, and
|
|
3071
|
+
* return the refusal re-thrown with its payment and an accurate account:
|
|
3072
|
+
* the payment header (and the body, when there is one) reached `url`, the
|
|
3073
|
+
* merchant answered with a redirect Haven did not follow, and the merchant
|
|
3074
|
+
* may still settle. The record is the same
|
|
3075
|
+
* `merchant_retry_rejected_after_payment` event a non-2xx answer writes: its
|
|
3076
|
+
* `retry_status` is the redirect status, `retry_body` is null, and `details`
|
|
3077
|
+
* carry `refusal` plus `redirect_location` when the refusal names a redirect
|
|
3078
|
+
* target, or `refused_url` (the URL that answered) otherwise. The payment
|
|
3079
|
+
* reads as funded and undelivered.
|
|
3080
|
+
*/
|
|
3081
|
+
async paidDeliveryInterrupted(err, url, receipt, paymentHeader, hasBody) {
|
|
3082
|
+
const scheme = x402PaymentHeaderNamesFor(paymentHeader).length > 1 ? "eip3009" : "erc7710";
|
|
3083
|
+
const status = err.redirectStatus ?? 0;
|
|
3084
|
+
await this.recordRetryRejected({
|
|
3085
|
+
rail: "x402",
|
|
3086
|
+
paymentId: receipt.paymentId,
|
|
3087
|
+
txHash: receipt.txHash,
|
|
3088
|
+
resourceUrl: receipt.resourceUrl,
|
|
3089
|
+
merchant: {
|
|
3090
|
+
merchant_status: status,
|
|
3091
|
+
merchant_status_text: "redirect not followed",
|
|
3092
|
+
merchant_headers: {},
|
|
3093
|
+
merchant_body: ""
|
|
3094
|
+
},
|
|
3095
|
+
details: {
|
|
3096
|
+
merchant_to: receipt.merchantTo,
|
|
3097
|
+
delegate_to: receipt.to,
|
|
3098
|
+
refusal: err.refusal,
|
|
3099
|
+
// The refusal's url is the redirect target only when it carries one;
|
|
3100
|
+
// otherwise it is the URL that answered (a non-GET redirect, a missing
|
|
3101
|
+
// or unresolvable Location), recorded under a neutral key.
|
|
3102
|
+
...err.redirectTarget ? { redirect_location: err.redirectTarget } : { refused_url: err.url }
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
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: `;
|
|
3106
|
+
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.";
|
|
3107
|
+
return new MerchantEgressRefusedError(what + next, err.url, err.refusal, false, {
|
|
3108
|
+
redirectStatus: err.redirectStatus,
|
|
3109
|
+
redirectTarget: err.redirectTarget,
|
|
3110
|
+
paymentId: receipt.paymentId,
|
|
3111
|
+
settlementScheme: scheme
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
2830
3114
|
async recordRetryRejected(input) {
|
|
2831
3115
|
try {
|
|
2832
3116
|
await this.post("/machine-payments/reconciliation-events", {
|
|
@@ -2969,17 +3253,19 @@ var MerchantCompletion = class {
|
|
|
2969
3253
|
};
|
|
2970
3254
|
for (let attempt = 0; ; attempt += 1) {
|
|
2971
3255
|
const result = await this.postEvidenceOnce(body);
|
|
2972
|
-
if (result.outcome !== "retryable" || attempt >= EVIDENCE_RETRY_DELAYS_MS.length) {
|
|
3256
|
+
if (result.outcome !== "retryable" || result.statusCode !== EVIDENCE_RETRYABLE_STATUS || attempt >= EVIDENCE_RETRY_DELAYS_MS.length) {
|
|
2973
3257
|
return result;
|
|
2974
3258
|
}
|
|
2975
3259
|
await this.sleep(EVIDENCE_RETRY_DELAYS_MS[attempt]);
|
|
2976
3260
|
}
|
|
2977
3261
|
}
|
|
2978
3262
|
/**
|
|
2979
|
-
* ONE evidence attempt — the body of `reportEvidence`'s loop above
|
|
2980
|
-
*
|
|
2981
|
-
* immediately; the caller decides whether to sleep and
|
|
2982
|
-
* (`reportEvidence
|
|
3263
|
+
* ONE evidence attempt — the body of `reportEvidence`'s loop above. A 503,
|
|
3264
|
+
* or no verdict at all (a non-answer or a 408, #4470), answers `retryable`
|
|
3265
|
+
* immediately; the caller decides whether to sleep and
|
|
3266
|
+
* re-attempt (`reportEvidence`, and — #4215 — the local settlement report
|
|
3267
|
+
* in `retryRequest`) or hand the outcome to its consumer (#3764 D2, the
|
|
3268
|
+
* hosted settlement report).
|
|
2983
3269
|
*/
|
|
2984
3270
|
async postEvidenceOnce(body) {
|
|
2985
3271
|
try {
|
|
@@ -2987,12 +3273,15 @@ var MerchantCompletion = class {
|
|
|
2987
3273
|
return { outcome: "confirmed" };
|
|
2988
3274
|
} catch (err) {
|
|
2989
3275
|
const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
|
|
2990
|
-
|
|
3276
|
+
const noVerdict = statusCode === 408 || err instanceof HavenApiError && err.failureCause?.kind === "no_answer";
|
|
3277
|
+
if (statusCode !== EVIDENCE_RETRYABLE_STATUS && !noVerdict) {
|
|
2991
3278
|
const reason = refusalReasonFromBody(err);
|
|
3279
|
+
const errorCode = refusalErrorCodeFromBody(err);
|
|
2992
3280
|
return {
|
|
2993
3281
|
outcome: "refused",
|
|
2994
3282
|
statusCode: statusCode ?? 0,
|
|
2995
|
-
...reason ? { reason } : {}
|
|
3283
|
+
...reason ? { reason } : {},
|
|
3284
|
+
...errorCode ? { errorCode } : {}
|
|
2996
3285
|
};
|
|
2997
3286
|
}
|
|
2998
3287
|
return { outcome: "retryable", statusCode };
|
|
@@ -3029,39 +3318,64 @@ var MerchantCompletion = class {
|
|
|
3029
3318
|
* gate uses, so a caller cannot "fix" a missing hash by reporting the demo
|
|
3030
3319
|
* merchant's own marker and getting a different verdict than the settle
|
|
3031
3320
|
* path already gave it.
|
|
3321
|
+
*
|
|
3322
|
+
* #4061: `options.paymentHeader` is the payment header the caller sent to
|
|
3323
|
+
* the merchant on its paid retry. On eip3009 it is REQUIRED for the record
|
|
3324
|
+
* (#3925): its signed authorization proves the nonce the settlement burned
|
|
3325
|
+
* is this payment's, and a report without it is refused with
|
|
3326
|
+
* `errorCode: 'settlement_proof_header_missing'`. It is forwarded exactly
|
|
3327
|
+
* as the funding report forwards it (`paymentProofHeader` plus the names
|
|
3328
|
+
* `x402PaymentHeaderNamesSent` derives from the same payload). erc7710
|
|
3329
|
+
* needs none — Haven assembled that header itself.
|
|
3032
3330
|
*/
|
|
3033
|
-
async reportSettlementEvidence(paymentId, settlementTxHash) {
|
|
3331
|
+
async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
|
|
3034
3332
|
if (isZeroSettlementTxHash(settlementTxHash)) {
|
|
3035
3333
|
throw new HavenZeroSettlementHashError(paymentId);
|
|
3036
3334
|
}
|
|
3037
3335
|
return this.reportEvidence({
|
|
3038
3336
|
paymentId,
|
|
3039
3337
|
rail: "x402",
|
|
3040
|
-
txHash: settlementTxHash
|
|
3338
|
+
txHash: settlementTxHash,
|
|
3339
|
+
...settlementProofFields(options.paymentHeader)
|
|
3041
3340
|
});
|
|
3042
3341
|
}
|
|
3043
3342
|
/**
|
|
3044
3343
|
* #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
|
|
3344
|
+
* after a completed hosted purchase — the second leg of a payment that
|
|
3345
|
+
* already has a funding-evidence row. Same endpoint and payload shape as
|
|
3047
3346
|
* `reportSettlementEvidence` above, but with #3764's D2 budget: exactly ONE
|
|
3048
3347
|
* attempt, no `EVIDENCE_RETRY_DELAYS_MS` backoff. The resource is already
|
|
3049
3348
|
* paid for and delivered, so a retryable answer must not stretch the
|
|
3050
3349
|
* completed purchase — the HOSTED tools hand the outcome to the agent
|
|
3051
|
-
* (`settlementEvidenceOutcome` → `haven_report_settlement_evidence`)
|
|
3052
|
-
*
|
|
3053
|
-
*
|
|
3350
|
+
* (`settlementEvidenceOutcome` → `haven_report_settlement_evidence`). Since
|
|
3351
|
+
* #4215 this ONE-attempt form serves the HOSTED path only
|
|
3352
|
+
* (`completeX402MerchantCall`); the local `retryRequest` posts the same
|
|
3353
|
+
* report through `reportEvidence`, whose bounded loop re-attempts while
|
|
3354
|
+
* Haven answers `retryable`. Callers validate the hash before this call
|
|
3355
|
+
* (well-formed, non-zero, different from the funding hash
|
|
3054
3356
|
* case-insensitively), so there is no client-side refusal here — the
|
|
3055
3357
|
* backend verifies on-chain and is the only judge.
|
|
3358
|
+
*
|
|
3359
|
+
* #4061: `paymentHeader` is the header the paid retry sent — the #3925
|
|
3360
|
+
* nonce proof, forwarded as `reportSettlementEvidence` forwards it. Both
|
|
3361
|
+
* callers hold it; without it the backend refuses the record.
|
|
3056
3362
|
*/
|
|
3057
|
-
async reportSettlementEvidenceOnce(paymentId, settlementTxHash) {
|
|
3363
|
+
async reportSettlementEvidenceOnce(paymentId, settlementTxHash, paymentHeader) {
|
|
3058
3364
|
return this.postEvidenceOnce({
|
|
3059
3365
|
paymentId,
|
|
3060
3366
|
rail: "x402",
|
|
3061
|
-
txHash: settlementTxHash
|
|
3367
|
+
txHash: settlementTxHash,
|
|
3368
|
+
...settlementProofFields(paymentHeader)
|
|
3062
3369
|
});
|
|
3063
3370
|
}
|
|
3064
3371
|
};
|
|
3372
|
+
function settlementProofFields(paymentHeader) {
|
|
3373
|
+
if (!paymentHeader || !paymentHeader.trim()) return {};
|
|
3374
|
+
return {
|
|
3375
|
+
paymentProofHeaderName: x402PaymentHeaderNamesSent(paymentHeader),
|
|
3376
|
+
paymentProofHeader: paymentHeader
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3065
3379
|
function refusalReasonFromBody(err) {
|
|
3066
3380
|
if (!(err instanceof HavenApiError)) return void 0;
|
|
3067
3381
|
const body = err.body;
|
|
@@ -3069,6 +3383,13 @@ function refusalReasonFromBody(err) {
|
|
|
3069
3383
|
const reason = body.reason;
|
|
3070
3384
|
return typeof reason === "string" && reason.length > 0 ? reason : void 0;
|
|
3071
3385
|
}
|
|
3386
|
+
function refusalErrorCodeFromBody(err) {
|
|
3387
|
+
if (!(err instanceof HavenApiError)) return void 0;
|
|
3388
|
+
const body = err.body;
|
|
3389
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
3390
|
+
const code = body.error_code;
|
|
3391
|
+
return typeof code === "string" && code.length > 0 ? code : void 0;
|
|
3392
|
+
}
|
|
3072
3393
|
function isZeroSettlementTxHash(hash) {
|
|
3073
3394
|
return typeof hash === "string" && /^0x0+$/i.test(hash);
|
|
3074
3395
|
}
|
|
@@ -3089,6 +3410,25 @@ function parseProtocolReceiptHeader(value) {
|
|
|
3089
3410
|
}
|
|
3090
3411
|
}
|
|
3091
3412
|
}
|
|
3413
|
+
function unfollowedRedirect(err) {
|
|
3414
|
+
const target = err.redirectTarget ?? err.url;
|
|
3415
|
+
switch (err.refusal) {
|
|
3416
|
+
case "redirect_cross_origin_body":
|
|
3417
|
+
return ` to a different origin (${target}). Haven did not follow it, so the payment was not re-sent there.`;
|
|
3418
|
+
case "redirect_on_non_get":
|
|
3419
|
+
return ". Under the egress policy only GET redirects are followed, so Haven did not follow it and nothing was sent to the redirect target.";
|
|
3420
|
+
case "redirect_missing_location":
|
|
3421
|
+
return " that named no target (no Location header). Haven did not follow it.";
|
|
3422
|
+
case "redirect_invalid_location":
|
|
3423
|
+
return " whose Location header is not a resolvable URL. Haven did not follow it.";
|
|
3424
|
+
case "redirect_budget_exceeded":
|
|
3425
|
+
return ` past the redirect budget. The chain stopped before ${target}, and nothing was sent there.`;
|
|
3426
|
+
case "url_not_allowed":
|
|
3427
|
+
return ` to a URL the egress policy does not allow (${target}). Haven did not follow it, so nothing was sent there.`;
|
|
3428
|
+
default:
|
|
3429
|
+
return ". Haven did not follow it.";
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3092
3432
|
|
|
3093
3433
|
// src/client.ts
|
|
3094
3434
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
@@ -3355,7 +3695,7 @@ var HavenClient = class {
|
|
|
3355
3695
|
}
|
|
3356
3696
|
const fundingTo = options.delegateAddress ?? (await this.getAgent()).delegateAddress;
|
|
3357
3697
|
if (!fundingTo) {
|
|
3358
|
-
throw
|
|
3698
|
+
throw HavenApiError.deterministic("Authenticated agent has no delegate address registered.", 502);
|
|
3359
3699
|
}
|
|
3360
3700
|
const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
|
|
3361
3701
|
const raw = await this.post("/x402", {
|
|
@@ -3393,7 +3733,7 @@ var HavenClient = class {
|
|
|
3393
3733
|
throwPaymentStateError("x402 payment", raw);
|
|
3394
3734
|
}
|
|
3395
3735
|
if (!raw.sign_data?.hash) {
|
|
3396
|
-
throw
|
|
3736
|
+
throw HavenApiError.deterministic("No sign_hash returned from x402/authorize", 500, raw);
|
|
3397
3737
|
}
|
|
3398
3738
|
if (raw.sign_data.signature_scheme !== void 0 && !raw.sign_data.typed_data) {
|
|
3399
3739
|
throw new HavenSigningError(
|
|
@@ -3401,7 +3741,7 @@ var HavenClient = class {
|
|
|
3401
3741
|
);
|
|
3402
3742
|
}
|
|
3403
3743
|
if (!raw.x402_expected_auth) {
|
|
3404
|
-
throw
|
|
3744
|
+
throw HavenApiError.deterministic("No x402 expected-context binding returned from x402/authorize", 500, raw);
|
|
3405
3745
|
}
|
|
3406
3746
|
return {
|
|
3407
3747
|
paymentId: raw.payment_id,
|
|
@@ -3631,6 +3971,12 @@ var HavenClient = class {
|
|
|
3631
3971
|
* degrade-to-warning path and the ledger row would still land while the
|
|
3632
3972
|
* purchase proceeded.
|
|
3633
3973
|
*
|
|
3974
|
+
* #4458: an agent whose only budget for the token has ENDED is refused the
|
|
3975
|
+
* same way with `error_code: 'delegation_expired'` and `budget_ended_at`
|
|
3976
|
+
* (ISO 8601) — also thrown, also recorded (`source: 'hosted_prepare'`).
|
|
3977
|
+
* The remedy differs: the owner sets a NEW budget, waiting for a period to
|
|
3978
|
+
* reset cannot help.
|
|
3979
|
+
*
|
|
3634
3980
|
* #3518: without `merchantTo`, an agent whose budgets for the token are all
|
|
3635
3981
|
* merchant-locked gets 409 `budget_requires_recipient` (thrown, nothing
|
|
3636
3982
|
* recorded) naming the pins in `budget_recipient_addresses` — repeat the
|
|
@@ -3648,18 +3994,20 @@ var HavenClient = class {
|
|
|
3648
3994
|
amountAtomic: input.amountAtomic,
|
|
3649
3995
|
...input.merchantTo !== void 0 ? { merchantTo: input.merchantTo } : {},
|
|
3650
3996
|
...input.resourceUrl !== void 0 ? { resourceUrl: input.resourceUrl } : {},
|
|
3651
|
-
...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {}
|
|
3997
|
+
...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {},
|
|
3998
|
+
...input.recordRefusal !== void 0 ? { recordRefusal: input.recordRefusal } : {}
|
|
3652
3999
|
});
|
|
3653
4000
|
}
|
|
3654
4001
|
/**
|
|
3655
4002
|
* Post-purchase allowance/budget summary for a settled payment (#1310).
|
|
3656
4003
|
*
|
|
3657
|
-
* Reuses the EXACT rail-aware read path {@link getAllowances}
|
|
3658
|
-
*
|
|
3659
|
-
*
|
|
3660
|
-
* the #1090 `deriveDelegationBudgets`-backed enforcer read, never
|
|
4004
|
+
* Reuses the EXACT rail-aware read path {@link getAllowances} uses —
|
|
4005
|
+
* `GET /machine-payments/allowances`, with delegation-rail values coming
|
|
4006
|
+
* from the #1090 `deriveDelegationBudgets`-backed enforcer read, never
|
|
3661
4007
|
* `agent_allowances` — so this can never disagree with
|
|
3662
|
-
* {@link getAllowances} for the same fixture.
|
|
4008
|
+
* {@link getAllowances} for the same fixture. (#1306's catalog-purchase
|
|
4009
|
+
* preflight read the same path until #3054; it now calls
|
|
4010
|
+
* `POST /machine-payments/budget-precheck` instead.) The settled token is
|
|
3663
4011
|
* resolved from {@link getPaymentStatus} so callers pass only
|
|
3664
4012
|
* `paymentId`, never a second haven_get_agent-style round trip.
|
|
3665
4013
|
*
|
|
@@ -3755,10 +4103,10 @@ var HavenClient = class {
|
|
|
3755
4103
|
return this.get(`/catalog/submit/${encodeURIComponent(id)}`);
|
|
3756
4104
|
}
|
|
3757
4105
|
/**
|
|
3758
|
-
* Fetch one
|
|
4106
|
+
* Fetch one catalog entry by id (#1306).
|
|
3759
4107
|
*
|
|
3760
4108
|
* Chain-scoped for free by the backend's SQL when the client is
|
|
3761
|
-
* agent-authenticated (#1299): an unknown id and an id
|
|
4109
|
+
* agent-authenticated (#1299): an unknown id and an id listed for a
|
|
3762
4110
|
* DIFFERENT chain than this agent's both 404 identically — this method does
|
|
3763
4111
|
* not (and must not) re-filter by chain in JS. Read-only, like
|
|
3764
4112
|
* {@link discoverTools}.
|
|
@@ -3914,9 +4262,10 @@ var HavenClient = class {
|
|
|
3914
4262
|
return this.accountReads.listPendingSubBudgetSignatures();
|
|
3915
4263
|
}
|
|
3916
4264
|
/**
|
|
3917
|
-
* Close a sub-budget row (delegating agent only). A `pending` or
|
|
3918
|
-
* closes immediately; a live child
|
|
3919
|
-
*
|
|
4265
|
+
* Close a sub-budget row (delegating agent only). A `pending` row, or an
|
|
4266
|
+
* `open` one already expired, closes immediately; otherwise (a live child, or
|
|
4267
|
+
* any `closing` row — its disable is still owed, #3923) it returns `signData`
|
|
4268
|
+
* for the on-chain revocation — sign it (`haven_sign` with `sub_budget_id`) and call
|
|
3920
4269
|
* `submitSubBudget`.
|
|
3921
4270
|
*/
|
|
3922
4271
|
async closeSubBudget(id) {
|
|
@@ -4342,13 +4691,15 @@ var HavenClient = class {
|
|
|
4342
4691
|
body = text;
|
|
4343
4692
|
}
|
|
4344
4693
|
const toolResult = mcpToolResultOf(body);
|
|
4345
|
-
const inBandChallenge = toolResult ? extractMcpPaymentRequired(toolResult) : void 0;
|
|
4346
4694
|
const metaSettlement = toolResult ? mcpSettlementFromToolResult(toolResult) : void 0;
|
|
4347
4695
|
const headerReceipt = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
|
|
4348
|
-
const
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4696
|
+
const answer = decidePostFundingMerchantAnswer({
|
|
4697
|
+
toolResult,
|
|
4698
|
+
httpOk: surfaced.ok,
|
|
4699
|
+
paymentResponseHeader: headerReceipt,
|
|
4700
|
+
metaSettlement
|
|
4701
|
+
});
|
|
4702
|
+
const merchantAccepted = answer.accepted;
|
|
4352
4703
|
let evidenceOutcome;
|
|
4353
4704
|
let settlementEvidenceOutcome;
|
|
4354
4705
|
if (!merchantAccepted) {
|
|
@@ -4370,7 +4721,7 @@ var HavenClient = class {
|
|
|
4370
4721
|
});
|
|
4371
4722
|
}
|
|
4372
4723
|
} else {
|
|
4373
|
-
const rawEvidenceTxHash = input.noFundingLeg ?
|
|
4724
|
+
const rawEvidenceTxHash = input.noFundingLeg ? answer.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
|
|
4374
4725
|
const evidenceTxHash = rawEvidenceTxHash && !isZeroSettlementTxHash(rawEvidenceTxHash) ? rawEvidenceTxHash : void 0;
|
|
4375
4726
|
if (evidenceTxHash) {
|
|
4376
4727
|
evidenceOutcome = await this.merchantCompletion.reportEvidence({
|
|
@@ -4381,19 +4732,20 @@ var HavenClient = class {
|
|
|
4381
4732
|
merchantStatus: surfaced.status,
|
|
4382
4733
|
paymentProofHeaderName: x402PaymentHeaderNamesSent(input.paymentHeader),
|
|
4383
4734
|
paymentProofHeader: input.paymentHeader,
|
|
4384
|
-
protocolReceiptHeaderName,
|
|
4385
|
-
protocolReceiptHeader,
|
|
4735
|
+
protocolReceiptHeaderName: answer.protocolReceiptHeaderName,
|
|
4736
|
+
protocolReceiptHeader: answer.protocolReceiptHeader,
|
|
4386
4737
|
// #3778: the delivery pointer rides the accepted arm's evidence
|
|
4387
4738
|
// report — a rejection delivered nothing to point at.
|
|
4388
4739
|
...input.deliveryReference ? { deliveryReference: input.deliveryReference } : {}
|
|
4389
4740
|
});
|
|
4390
4741
|
}
|
|
4391
4742
|
if (!input.noFundingLeg) {
|
|
4392
|
-
const settlementCandidate =
|
|
4743
|
+
const settlementCandidate = answer.settlementTxHash;
|
|
4393
4744
|
if (settlementCandidate && isWellFormedSettlementTxHash(settlementCandidate) && !isZeroSettlementTxHash(settlementCandidate) && fundingTxHash && settlementCandidate.toLowerCase() !== fundingTxHash.toLowerCase()) {
|
|
4394
4745
|
settlementEvidenceOutcome = await this.merchantCompletion.reportSettlementEvidenceOnce(
|
|
4395
4746
|
evidenceContext.paymentId,
|
|
4396
|
-
settlementCandidate
|
|
4747
|
+
settlementCandidate,
|
|
4748
|
+
input.paymentHeader
|
|
4397
4749
|
);
|
|
4398
4750
|
}
|
|
4399
4751
|
}
|
|
@@ -4404,7 +4756,7 @@ var HavenClient = class {
|
|
|
4404
4756
|
ok: merchantAccepted,
|
|
4405
4757
|
body,
|
|
4406
4758
|
// #3155 review S3: a transaction beside a rejection is not a settlement.
|
|
4407
|
-
settlementTxHash: merchantAccepted ?
|
|
4759
|
+
settlementTxHash: merchantAccepted ? answer.settlementTxHash ?? void 0 : void 0,
|
|
4408
4760
|
evidenceOutcome,
|
|
4409
4761
|
settlementEvidenceOutcome
|
|
4410
4762
|
};
|
|
@@ -4437,9 +4789,15 @@ var HavenClient = class {
|
|
|
4437
4789
|
* Haven verifies the delegate → merchant transfer on-chain
|
|
4438
4790
|
* (`observeEip3009MerchantSettlement`) and records it beside the funding
|
|
4439
4791
|
* hash, so receipts name the merchant's settlement.
|
|
4792
|
+
*
|
|
4793
|
+
* #4061: on eip3009 pass `options.paymentHeader` — the payment header you
|
|
4794
|
+
* sent the merchant on your paid retry. Since #3925 it is the proof the
|
|
4795
|
+
* settlement is recorded against; without it the report is `refused` with
|
|
4796
|
+
* `errorCode: 'settlement_proof_header_missing'`, and repeating it WITH the
|
|
4797
|
+
* header can succeed.
|
|
4440
4798
|
*/
|
|
4441
|
-
async reportSettlementEvidence(paymentId, settlementTxHash) {
|
|
4442
|
-
return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash);
|
|
4799
|
+
async reportSettlementEvidence(paymentId, settlementTxHash, options = {}) {
|
|
4800
|
+
return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash, options);
|
|
4443
4801
|
}
|
|
4444
4802
|
/**
|
|
4445
4803
|
* #3770: record the agent's evidence-only delivery verdict on a settled
|
|
@@ -4674,7 +5032,7 @@ var toolDescriptions = {
|
|
|
4674
5032
|
getAgent: {
|
|
4675
5033
|
summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, per-token remaining allowance (atomic + human-readable). The recommended first call in a new session.",
|
|
4676
5034
|
selectionGuidance: "Use this as the session bootstrap, or to confirm identity together with whether the agent has spend authority right now. For per-token detail (configured vs spent vs reset window) use haven_get_allowances.",
|
|
4677
|
-
behavior: `Reads identity plus the live spend-authority snapshot \u2014 the active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. Readiness is AUTHORITY
|
|
5035
|
+
behavior: `Reads identity plus the live spend-authority snapshot \u2014 the active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. needs_approval with no allowances[] rows and budgetEndedAt set: the budget ENDED on that date \u2014 ask the owner to set a new one, waiting will not help; with no rows and no budgetEndedAt: nothing approved yet; with rows: spent for this period, refilling at budgetRefillsAt (null: it ends first). Readiness is AUTHORITY \u2014 a budget is live, not that funds are held. Each allowances[] row carries fundsCoverRemaining: false when the account's balance cannot back that row's whole remaining budget \u2014 a heads-up to mention, not a refusal (budgets above the balance are normal; an owner may top up weekly) \u2014 and null when the coverage read failed or the remaining was not read live; the key is absent at remaining 0, and rows for one token are compared alone. Covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify it with a signer tool call or connect --doctor. An over-budget payment is declined before any money moves; there is no approval queue \u2014 ask the owner to grant or raise the budget. allowances[] carries id, tokenAddress, remainingAtomic, remainingDisplay, nextRefillAt per token. taskBudgets[] rows carry maxAtomic/maxDisplay plus spentAtomic/remainingAtomic/remainingDisplay \u2014 what the chain still allows that budget \u2014 and remainingIsFromChain: false marks a failed read (figures null, never the cap); only live rows are listed, each with its status (pending | open | closing) and isExpired. pendingSubBudgetSignatures[] lists sub-budget rows awaiting YOUR signature, with the haven_sign step. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.`,
|
|
4678
5036
|
nextActionGuidance: ""
|
|
4679
5037
|
},
|
|
4680
5038
|
getAllowances: {
|
|
@@ -4688,7 +5046,7 @@ var toolDescriptions = {
|
|
|
4688
5046
|
checkFunds: {
|
|
4689
5047
|
summary: "Check whether the agent's account actually holds at least the given amount of a token \u2014 funds held, not spend permitted.",
|
|
4690
5048
|
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
|
|
5049
|
+
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
5050
|
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
5051
|
},
|
|
4694
5052
|
listReceipts: {
|
|
@@ -4716,9 +5074,9 @@ var toolDescriptions = {
|
|
|
4716
5074
|
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
5075
|
},
|
|
4718
5076
|
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
|
|
5077
|
+
summary: "Step 1 of a purchase: discover payable services from Haven's merchant catalog \u2014 names, prices, the next call.",
|
|
5078
|
+
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.",
|
|
5079
|
+
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
5080
|
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
5081
|
},
|
|
4724
5082
|
submitCatalogEntry: {
|
|
@@ -4741,8 +5099,8 @@ var toolDescriptions = {
|
|
|
4741
5099
|
},
|
|
4742
5100
|
reportSettlementEvidence: {
|
|
4743
5101
|
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."
|
|
5102
|
+
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.",
|
|
5103
|
+
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
5104
|
}
|
|
4747
5105
|
};
|
|
4748
5106
|
|
|
@@ -4996,13 +5354,13 @@ You are an AI agent whose user has no Haven account yet, or has one but has not
|
|
|
4996
5354
|
|
|
4997
5355
|
## What Haven is, in three sentences
|
|
4998
5356
|
|
|
4999
|
-
Your user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.
|
|
5357
|
+
Your user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. The budget also ENDS on a date your user chooses \u2014 30, 90 (default), 180 or 365 days out \u2014 and that day is enforced on-chain too. When the budget ends, your authority to pay ends with it; ask your user to renew it (a new approval on the agent's page, or a new \`budget grant\`). Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.
|
|
5000
5358
|
|
|
5001
5359
|
## The sequence
|
|
5002
5360
|
|
|
5003
5361
|
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
5362
|
|
|
5005
|
-
1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey
|
|
5363
|
+
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
5364
|
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
5365
|
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
5366
|
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 +5384,7 @@ Send these as your own message, \`<host>\` replaced by the host you fetched this
|
|
|
5026
5384
|
|
|
5027
5385
|
**Before signup** (step 1):
|
|
5028
5386
|
|
|
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
|
|
5387
|
+
> 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
5388
|
|
|
5031
5389
|
**At the passkey step:**
|
|
5032
5390
|
|
|
@@ -5069,7 +5427,7 @@ If the approval wait times out, nothing is lost \u2014 your agent is registered
|
|
|
5069
5427
|
Call \`haven_get_agent\`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus \`spend_authority_readiness\`:
|
|
5070
5428
|
|
|
5071
5429
|
- \`ready\` \u2014 a budget is live: you have the authority to pay. It does not say the account holds funds \u2014 the \`allowances[]\` rows carry \`funds_cover_remaining\`, and \`false\` there is a heads-up to mention to your user, not a refusal.
|
|
5072
|
-
- \`needs_approval\` \u2014
|
|
5430
|
+
- \`needs_approval\` \u2014 no budget can pay. \`allowances[]\` empty with \`budgetEndedAt\` set: your budget ENDED on that date; waiting will not help \u2014 ask your user to set a new one in their Haven tab. \`allowances[]\` empty with \`budgetEndedAt\` null: nobody approved one yet; ask your user again \u2014 there is no queue. Rows, all spent: it refills at \`budgetRefillsAt\`; tell your user, then wait or ask them to raise it (\`null\`: it ends first; ask them to renew it).
|
|
5073
5431
|
- \`revoked\` \u2014 the credential is not active; ask your user to create a new agent.
|
|
5074
5432
|
|
|
5075
5433
|
\`ready\` covers hosted identity and the budget only, not your local signer. Check that with \`npx -y @haven_ai/connect@<channel> --doctor\`, the same tag your prompt named \u2014 a separate command, so the command-modification rule does not bind it.
|
|
@@ -5105,7 +5463,7 @@ Then:
|
|
|
5105
5463
|
1. Run \`<packages.cli.one_liner> login\`. That command is the manifest's \`packages.cli.one_liner\` (\`/.well-known/haven.json\`) \u2014 run it as given: \`packages.cli.channel\` serves the full spec. It prints a link and a code and does not need my password \u2014 it must never ask for it. Give me the link straight away and wait for me to approve it in my browser.
|
|
5106
5464
|
2. Once I have approved, run \`haven agents connect --name <a name you choose> --budget <amount> --token USDC --period <minutes>\` with the budget I tell you. If I have not given you one, ask me before running it. Add \`--run\` to complete the connection in the same step.
|
|
5107
5465
|
3. ${AGENT_APPROVAL_RELAY_JSON_SENTENCE}
|
|
5108
|
-
4. Once I have approved the budget, verify with the \`haven_get_agent\` tool: \`ready\` means I have given you the authority to pay \u2014 it does not confirm the account holds funds, so if its allowances rows carry \`funds_cover_remaining: false\`, tell me and still try the payment; \`needs_approval\` means my approval has not landed yet.
|
|
5466
|
+
4. Once I have approved the budget, verify with the \`haven_get_agent\` tool: \`ready\` means I have given you the authority to pay \u2014 it does not confirm the account holds funds, so if its allowances rows carry \`funds_cover_remaining: false\`, tell me and still try the payment; \`needs_approval\` with no allowances rows and \`budgetEndedAt\` set means my budget ended on that date \u2014 tell me, and I will set a new one; with no rows and no \`budgetEndedAt\`, my approval has not landed yet; with rows, the budget is spent \u2014 it refills at \`budgetRefillsAt\` (\`null\`: it ends first), so tell me, and I can raise or renew it.
|
|
5109
5467
|
|
|
5110
5468
|
Two things only I can do: approving that budget with my passkey, and funding the account with USDC \u2014 no ETH, Haven sponsors the gas. Tell me if either is missing rather than working around it.
|
|
5111
5469
|
|
|
@@ -5225,8 +5583,10 @@ Do not guess the wallet address, network, or budget.
|
|
|
5225
5583
|
|
|
5226
5584
|
For instant orientation at the start of a session, read the non-secret
|
|
5227
5585
|
\`agent.json\` the connector wrote to your Haven credential directory (typically
|
|
5228
|
-
\`~/.haven/agents/<
|
|
5229
|
-
|
|
5586
|
+
\`~/.haven/agents/<slug>/agent.json\`, where \`<slug>\` is the wiring name of the
|
|
5587
|
+
\`haven-<slug>\` pair \u2014 only a \`--bare\` run, or a \`--replace\` without \`--name\`,
|
|
5588
|
+
names the directory by the agent id instead; if you don't know the folder, list
|
|
5589
|
+
\`~/.haven/agents/\` to find it). It
|
|
5230
5590
|
holds your agent id, Haven wallet address, network, and *configured* per-token
|
|
5231
5591
|
budget, and contains no keys \u2014 the fastest way to answer "who am I and what may
|
|
5232
5592
|
I spend" with no round trip. If that file is absent (some setups don't write
|
|
@@ -5247,7 +5607,13 @@ spending:
|
|
|
5247
5607
|
the user, not a refusal \u2014 a budget above the balance is a normal setup, so
|
|
5248
5608
|
still try the payment; \`null\` means the coverage read failed or the remaining
|
|
5249
5609
|
figure was not read live; the key is absent when the remaining is 0. Rows for
|
|
5250
|
-
one token are compared alone.
|
|
5610
|
+
one token are compared alone. \`needs_approval\` with no \`allowances[]\`
|
|
5611
|
+
rows and \`budgetEndedAt\` set means the budget ENDED on that date \u2014 waiting
|
|
5612
|
+
will not help, ask the owner to set a new one; with no rows and no
|
|
5613
|
+
\`budgetEndedAt\`, nobody approved a budget yet; with rows (all at remaining 0) the
|
|
5614
|
+
budget is spent for this period, and \`budgetRefillsAt\` says when it refills
|
|
5615
|
+
(\`null\`: it ends before it refills) \u2014 tell the user, then wait or ask them
|
|
5616
|
+
to raise it.
|
|
5251
5617
|
- \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
|
|
5252
5618
|
(configured, spent, reset window) when you need more than the summary.
|
|
5253
5619
|
- \`mcp__haven__haven_check_funds\` \u2014 whether the account actually HOLDS at
|
|
@@ -5394,10 +5760,13 @@ request, this is the only way it can learn the purchase failed \u2014 without it
|
|
|
5394
5760
|
failed purchase reads as complete for fifteen minutes. If the merchant's
|
|
5395
5761
|
\`PAYMENT-RESPONSE\` header names a \`transaction\`, pass that raw header as
|
|
5396
5762
|
\`payment_response\` on the SAME \`mcp__haven__haven_report_x402_outcome\`
|
|
5397
|
-
call
|
|
5398
|
-
|
|
5763
|
+
call, together with \`payment_header\`: the exact header you retried the
|
|
5764
|
+
merchant with. Haven records a reported settlement only with that header,
|
|
5765
|
+
which proves the settlement is this payment's. It decodes the response,
|
|
5766
|
+
verifies the settlement on-chain, and the receipt then shows the merchant's
|
|
5767
|
+
settlement, not only the funding transaction. Call
|
|
5399
5768
|
\`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
|
|
5769
|
+
names it as the next step, and pass the same \`payment_header\` there too. On the **erc7710** scheme (the pay result says
|
|
5401
5770
|
\`settlement_scheme: "erc7710"\`), sign with \`mcp__haven-signer__haven_sign\`,
|
|
5402
5771
|
then \`mcp__haven__haven_submit\` with \`settlement_scheme: "erc7710"\`
|
|
5403
5772
|
returns the \`payment_header\`; retry the merchant yourself with it as
|
|
@@ -5427,7 +5796,9 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
|
|
|
5427
5796
|
\`invoice_id\`, beside a raw deposit address and a web payment link.
|
|
5428
5797
|
- **Quote the exact request:** \`mcp__haven__haven_quote_x402\` with \`url\`,
|
|
5429
5798
|
\`method\`, \`headers\` and \`body\`, exactly as the merchant described it.
|
|
5430
|
-
\`body\` is a JSON **string**, not an object.
|
|
5799
|
+
\`body\` is a JSON **string**, not an object. When the price is stated up
|
|
5800
|
+
front (an invoice total, say), also pass \`expected_amount\` \u2014 the price you
|
|
5801
|
+
expect, in whole tokens (\`"0.10"\`). On a JSON POST always pass
|
|
5431
5802
|
\`headers: {"Content-Type": "application/json"}\` \u2014 the tool does not infer
|
|
5432
5803
|
it, and a merchant may answer a request without it with a generic challenge
|
|
5433
5804
|
Haven cannot pay (for example, only the \`upto\` scheme).
|
|
@@ -5456,6 +5827,18 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
|
|
|
5456
5827
|
report it, the merchant's settlement evidence. (A deposit transfer is bound
|
|
5457
5828
|
to its payee and amount on-chain too; the difference is the record, not
|
|
5458
5829
|
the binding.)
|
|
5830
|
+
- **If the quote says stop, stop.** The hosted quote checks the agent's
|
|
5831
|
+
remaining budget at quote time, and it can check a price you state
|
|
5832
|
+
(\`expected_amount\`). On the local runtime (\`@haven_ai/mcp\`) the quote
|
|
5833
|
+
checks only a price you state; the budget is checked when you pay.
|
|
5834
|
+
When it returns \`safe_to_continue: false\` \u2014 the amount exceeds the budget, or
|
|
5835
|
+
the price does not match what you expected \u2014 it prefills no cap and names no
|
|
5836
|
+
next tool: stop and tell the user. If the x402 challenge itself is wrong (a
|
|
5837
|
+
price wildly off the invoice) and NOTHING has been paid via x402, the
|
|
5838
|
+
invoice's own deposit address MAY be an alternative, but only with the
|
|
5839
|
+
user's explicit go-ahead \u2014 never automatically. Once an x402 payment has
|
|
5840
|
+
been attempted or refused, never pay the deposit address: it can pay the
|
|
5841
|
+
invoice twice.
|
|
5459
5842
|
- **Pay exactly one route, and never fall back silently.** If the x402 route
|
|
5460
5843
|
is refused or fails, do not then pay the deposit address \u2014 that pays the
|
|
5461
5844
|
invoice twice. Follow the result's guidance fields instead: on EIP-3009, a
|
|
@@ -5592,8 +5975,8 @@ only part of it was usable), and an optional \`note\` (max 2000 characters)
|
|
|
5592
5975
|
saying what was wrong. It is evidence only: it moves no money, never changes
|
|
5593
5976
|
\`settled\` or any money field, and works only on your own settled
|
|
5594
5977
|
payments \u2014 another agent's payment is refused. A re-report replaces your
|
|
5595
|
-
earlier verdict; the receipt then carries it beside the payment, so
|
|
5596
|
-
|
|
5978
|
+
earlier verdict; the receipt then carries it beside the payment, so a junk
|
|
5979
|
+
delivery does not read as a success.
|
|
5597
5980
|
### Relay a delivered code or credential to the owner \u2014 immediately, verbatim
|
|
5598
5981
|
|
|
5599
5982
|
If a merchant response carries what the user PAID FOR \u2014 a redemption code, gift
|