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