@haven_ai/sdk 0.1.17-alpha.0 → 0.1.19-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/dist/index.cjs +278 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +328 -10
- package/dist/index.d.ts +328 -10
- package/dist/index.js +267 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var async_hooks = require('async_hooks');
|
|
4
4
|
var schemes = require('x402/schemes');
|
|
5
|
+
var viem = require('viem');
|
|
5
6
|
var accounts = require('viem/accounts');
|
|
6
7
|
var ethers = require('ethers');
|
|
7
8
|
var crypto = require('crypto');
|
|
@@ -242,6 +243,22 @@ function signHash(privateKey, hash) {
|
|
|
242
243
|
);
|
|
243
244
|
}
|
|
244
245
|
}
|
|
246
|
+
async function signUserOpTypedDataForDelegation(privateKey, typedData) {
|
|
247
|
+
try {
|
|
248
|
+
const wallet = new ethers.ethers.Wallet(privateKey);
|
|
249
|
+
const types = { ...typedData.types };
|
|
250
|
+
delete types.EIP712Domain;
|
|
251
|
+
return await wallet.signTypedData(
|
|
252
|
+
typedData.domain,
|
|
253
|
+
types,
|
|
254
|
+
typedData.message
|
|
255
|
+
);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
throw new HavenSigningError(
|
|
258
|
+
`Failed to sign delegation UserOperation: ${err instanceof Error ? err.message : String(err)}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
245
262
|
function addressFromKey(privateKey) {
|
|
246
263
|
try {
|
|
247
264
|
return new ethers.ethers.Wallet(privateKey).address;
|
|
@@ -259,6 +276,24 @@ function verifySignature(hash, signature, expectedAddress) {
|
|
|
259
276
|
return false;
|
|
260
277
|
}
|
|
261
278
|
}
|
|
279
|
+
var RECEIPT_VERSION = "haven-receipt-1";
|
|
280
|
+
function defaultRecover(hash, signature) {
|
|
281
|
+
return ethers.ethers.recoverAddress(hash, signature);
|
|
282
|
+
}
|
|
283
|
+
function verifyPaymentReceipt(receipt, recover = defaultRecover) {
|
|
284
|
+
const { delegate, signHash: signHash2, signature } = receipt.authorization;
|
|
285
|
+
if (!signature) return { verified: false, reason: "missing_signature" };
|
|
286
|
+
let recovered;
|
|
287
|
+
try {
|
|
288
|
+
recovered = recover(signHash2, signature);
|
|
289
|
+
} catch {
|
|
290
|
+
return { verified: false, reason: "bad_signature" };
|
|
291
|
+
}
|
|
292
|
+
if (recovered.toLowerCase() !== delegate.toLowerCase()) {
|
|
293
|
+
return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
|
|
294
|
+
}
|
|
295
|
+
return { verified: true, recoveredSigner: recovered };
|
|
296
|
+
}
|
|
262
297
|
|
|
263
298
|
// src/base64.ts
|
|
264
299
|
function normalizeBase64(value) {
|
|
@@ -303,6 +338,8 @@ function decodeBase64Json(value, label) {
|
|
|
303
338
|
|
|
304
339
|
// src/x402.ts
|
|
305
340
|
var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
341
|
+
var BASE_SEPOLIA_USDC_ADDRESS = "0x036cbd53842c5426634e7929541ec2318f3dcf7e";
|
|
342
|
+
var STANDARD_X402_USDC_ADDRESSES = /* @__PURE__ */ new Set([BASE_USDC_ADDRESS, BASE_SEPOLIA_USDC_ADDRESS]);
|
|
306
343
|
var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
|
|
307
344
|
var DECIMAL_ATOMIC_AMOUNT_RE = /^[0-9]+$/;
|
|
308
345
|
function isPositiveDecimalAtomicAmount(value) {
|
|
@@ -311,6 +348,12 @@ function isPositiveDecimalAtomicAmount(value) {
|
|
|
311
348
|
function optionAuthorizationAmount(option) {
|
|
312
349
|
return option.maxAmountRequired ?? option.amount;
|
|
313
350
|
}
|
|
351
|
+
var X402_MAX_AUTHORIZATION_WINDOW_SECONDS = 600;
|
|
352
|
+
var X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = 300;
|
|
353
|
+
function clampAuthorizationWindow(seconds) {
|
|
354
|
+
const requested = typeof seconds === "number" && Number.isFinite(seconds) ? seconds : 30;
|
|
355
|
+
return Math.min(Math.max(Math.floor(requested), 1), X402_MAX_AUTHORIZATION_WINDOW_SECONDS);
|
|
356
|
+
}
|
|
314
357
|
function normalizePaymentOption(value) {
|
|
315
358
|
const candidate = value;
|
|
316
359
|
if (!candidate || typeof candidate !== "object" || typeof candidate.scheme !== "string" || typeof candidate.network !== "string" || typeof candidate.asset !== "string" || typeof candidate.payTo !== "string") {
|
|
@@ -332,7 +375,7 @@ function normalizePaymentOption(value) {
|
|
|
332
375
|
mimeType: candidate.mimeType,
|
|
333
376
|
asset: candidate.asset,
|
|
334
377
|
payTo: candidate.payTo,
|
|
335
|
-
maxTimeoutSeconds: candidate.maxTimeoutSeconds
|
|
378
|
+
maxTimeoutSeconds: clampAuthorizationWindow(candidate.maxTimeoutSeconds),
|
|
336
379
|
extra: candidate.extra
|
|
337
380
|
};
|
|
338
381
|
}
|
|
@@ -362,11 +405,15 @@ function normalizePaymentRequired(value) {
|
|
|
362
405
|
var SUPPORTED_X402_NETWORKS = {
|
|
363
406
|
"eip155:100": "Gnosis Chain",
|
|
364
407
|
"eip155:8453": "Base",
|
|
365
|
-
"base": "Base"
|
|
408
|
+
"base": "Base",
|
|
409
|
+
"eip155:84532": "Base Sepolia",
|
|
410
|
+
"base-sepolia": "Base Sepolia"
|
|
366
411
|
};
|
|
367
412
|
var STANDARD_X402_NETWORKS = {
|
|
368
413
|
"eip155:8453": "base",
|
|
369
|
-
"base": "base"
|
|
414
|
+
"base": "base",
|
|
415
|
+
"eip155:84532": "base-sepolia",
|
|
416
|
+
"base-sepolia": "base-sepolia"
|
|
370
417
|
};
|
|
371
418
|
var GNOSIS_TOKENS = {
|
|
372
419
|
"0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
|
|
@@ -377,14 +424,21 @@ var BASE_TOKENS = {
|
|
|
377
424
|
"0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
|
|
378
425
|
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { symbol: "USDC", decimals: 6 }
|
|
379
426
|
};
|
|
427
|
+
var BASE_SEPOLIA_TOKENS = {
|
|
428
|
+
"0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
|
|
429
|
+
"0x036cbd53842c5426634e7929541ec2318f3dcf7e": { symbol: "USDC", decimals: 6 }
|
|
430
|
+
};
|
|
380
431
|
var ALL_TOKENS = {
|
|
381
432
|
...GNOSIS_TOKENS,
|
|
382
|
-
...BASE_TOKENS
|
|
433
|
+
...BASE_TOKENS,
|
|
434
|
+
...BASE_SEPOLIA_TOKENS
|
|
383
435
|
};
|
|
384
436
|
var NETWORK_TOKENS = {
|
|
385
437
|
"eip155:100": GNOSIS_TOKENS,
|
|
386
438
|
"eip155:8453": BASE_TOKENS,
|
|
387
|
-
"base": BASE_TOKENS
|
|
439
|
+
"base": BASE_TOKENS,
|
|
440
|
+
"eip155:84532": BASE_SEPOLIA_TOKENS,
|
|
441
|
+
"base-sepolia": BASE_SEPOLIA_TOKENS
|
|
388
442
|
};
|
|
389
443
|
function parsePaymentRequired(response) {
|
|
390
444
|
const v2Header = response.headers.get("PAYMENT-REQUIRED");
|
|
@@ -438,7 +492,7 @@ function selectPaymentOption(accepts) {
|
|
|
438
492
|
function selectStandardPaymentOption(accepts) {
|
|
439
493
|
if (!accepts || accepts.length === 0) return null;
|
|
440
494
|
for (const opt of accepts) {
|
|
441
|
-
if (opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && opt.asset.toLowerCase()
|
|
495
|
+
if (opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && STANDARD_X402_USDC_ADDRESSES.has(opt.asset.toLowerCase()) && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
|
|
442
496
|
return opt;
|
|
443
497
|
}
|
|
444
498
|
}
|
|
@@ -452,8 +506,9 @@ function x402AuthorizationAmount(option) {
|
|
|
452
506
|
return amount;
|
|
453
507
|
}
|
|
454
508
|
function buildX402ExpectedMessage(context) {
|
|
509
|
+
const version = context.typedDataHash ? 2 : 1;
|
|
455
510
|
const payload = {
|
|
456
|
-
version
|
|
511
|
+
version,
|
|
457
512
|
kind: "haven.x402.expected",
|
|
458
513
|
paymentId: context.paymentId,
|
|
459
514
|
payloadHash: context.payloadHash.toLowerCase(),
|
|
@@ -466,7 +521,10 @@ function buildX402ExpectedMessage(context) {
|
|
|
466
521
|
if (context.expiresAt) {
|
|
467
522
|
payload.expiresAt = context.expiresAt;
|
|
468
523
|
}
|
|
469
|
-
|
|
524
|
+
if (context.typedDataHash) {
|
|
525
|
+
payload.typedDataHash = context.typedDataHash.toLowerCase();
|
|
526
|
+
}
|
|
527
|
+
return `Haven x402 expected context v${version}
|
|
470
528
|
${stableStringify(payload)}`;
|
|
471
529
|
}
|
|
472
530
|
function toStandardPaymentRequirements(paymentRequired, option) {
|
|
@@ -486,7 +544,15 @@ function toStandardPaymentRequirements(paymentRequired, option) {
|
|
|
486
544
|
mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
|
|
487
545
|
payTo: option.payTo,
|
|
488
546
|
asset: option.asset,
|
|
489
|
-
|
|
547
|
+
// Second enforcement point (#715): the parse path clamps too, but this is
|
|
548
|
+
// the last stop before the x402 library turns the timeout into
|
|
549
|
+
// `validBefore` — options constructed without parsing are bounded here.
|
|
550
|
+
// The forward margin (#1256) is added ONLY here, at signing: the parse
|
|
551
|
+
// path keeps recording the merchant's advertised timeout unchanged, and
|
|
552
|
+
// the library's `validBefore = now + this value` then carries enough
|
|
553
|
+
// slack to satisfy the facilitator's `validBefore ≥ now + maxTimeout`
|
|
554
|
+
// verify rule after our funding leg confirms.
|
|
555
|
+
maxTimeoutSeconds: clampAuthorizationWindow(option.maxTimeoutSeconds) + X402_SETTLEMENT_FORWARD_MARGIN_SECONDS,
|
|
490
556
|
extra: option.extra
|
|
491
557
|
};
|
|
492
558
|
}
|
|
@@ -620,7 +686,8 @@ var CHAIN_EXPLORER_TX = {
|
|
|
620
686
|
8453: "https://basescan.org/tx"
|
|
621
687
|
};
|
|
622
688
|
var CHAIN_USDC = {
|
|
623
|
-
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
689
|
+
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
690
|
+
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
624
691
|
};
|
|
625
692
|
function buildExplorerUrl(chainId, txHash) {
|
|
626
693
|
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
@@ -783,6 +850,16 @@ function parseSseJsonRpcMessages(text) {
|
|
|
783
850
|
function selectJsonRpcResult(messages) {
|
|
784
851
|
return messages.find((m) => "result" in m || "error" in m) ?? messages[messages.length - 1];
|
|
785
852
|
}
|
|
853
|
+
function x402TypedDataDigest(typedData) {
|
|
854
|
+
if (!typedData || typeof typedData !== "object") return void 0;
|
|
855
|
+
try {
|
|
856
|
+
return viem.hashTypedData(typedData);
|
|
857
|
+
} catch (err) {
|
|
858
|
+
throw new HavenSigningError(
|
|
859
|
+
`The x402 funding intent carried a sign_data.typed_data that is not a valid EIP-712 payload (needs domain, types, primaryType, message), so its digest cannot be derived and no signer could accept it. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
786
863
|
var HavenClient = class {
|
|
787
864
|
apiKey;
|
|
788
865
|
delegateKey;
|
|
@@ -932,6 +1009,11 @@ var HavenClient = class {
|
|
|
932
1009
|
if (!raw.sign_data?.hash) {
|
|
933
1010
|
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
934
1011
|
}
|
|
1012
|
+
if (raw.sign_data.signature_scheme !== void 0 && !raw.sign_data.typed_data) {
|
|
1013
|
+
throw new HavenSigningError(
|
|
1014
|
+
`This account's x402 funding intent declares signature scheme '${raw.sign_data.signature_scheme}' but carried no typed_data to sign. Refusing to fall back to the bare hash \u2014 the account would reject that signature on-chain. This is a backend contract violation; report it rather than working around it.`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
935
1017
|
if (!raw.x402_expected_auth) {
|
|
936
1018
|
throw new HavenApiError("No x402 expected-context binding returned from x402/authorize", 500, raw);
|
|
937
1019
|
}
|
|
@@ -948,6 +1030,14 @@ var HavenClient = class {
|
|
|
948
1030
|
asset: option.asset,
|
|
949
1031
|
network: option.network,
|
|
950
1032
|
expectedAuth: raw.x402_expected_auth,
|
|
1033
|
+
// #1138: the digest the delegation-rail expected context commits to.
|
|
1034
|
+
// Re-derived locally, exactly like every other context field the edge
|
|
1035
|
+
// signer is handed (amount, merchantTo, …) — none of them are trusted
|
|
1036
|
+
// because they arrived, they are trusted because the reconstructed
|
|
1037
|
+
// message has to match Haven's signature over it. A typed_data altered in
|
|
1038
|
+
// transit therefore fails message equality and is refused, and the signer
|
|
1039
|
+
// re-derives this digest a second time from the payload it actually signs.
|
|
1040
|
+
expectedTypedDataHash: x402TypedDataDigest(raw.sign_data.typed_data),
|
|
951
1041
|
fundingTo
|
|
952
1042
|
};
|
|
953
1043
|
}
|
|
@@ -971,6 +1061,42 @@ var HavenClient = class {
|
|
|
971
1061
|
}
|
|
972
1062
|
return signature;
|
|
973
1063
|
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Sign a payment's `sign_data` with the correct scheme for its rail.
|
|
1066
|
+
*
|
|
1067
|
+
* Dispatching on the server-provided scheme means a caller never has to
|
|
1068
|
+
* know which rail an account is on; an unknown scheme is a hard error,
|
|
1069
|
+
* never a guessed signature. The session rail's 'eip191_userop' is retired
|
|
1070
|
+
* (#834) — the backend refuses those intents with HTTP 410 before any
|
|
1071
|
+
* sign_data reaches a client, so encountering it here is a hard error too.
|
|
1072
|
+
*/
|
|
1073
|
+
async signForData(signData) {
|
|
1074
|
+
if (!this.delegateKey) {
|
|
1075
|
+
throw new HavenSigningError(
|
|
1076
|
+
"Cannot sign without a delegateKey. Pass the private key in HavenClient config, or sign externally."
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
const scheme = signData.signature_scheme;
|
|
1080
|
+
if (scheme === "eip191_userop") {
|
|
1081
|
+
throw new HavenSigningError(
|
|
1082
|
+
"The session rail is retired \u2014 'eip191_userop' intents can no longer be signed. Re-onboard the account on the delegation rail."
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
if (scheme === "eip712_userop") {
|
|
1086
|
+
if (!signData.typed_data) {
|
|
1087
|
+
throw new HavenSigningError(
|
|
1088
|
+
"sign_data.signature_scheme is eip712_userop but typed_data is missing \u2014 refusing to sign the bare hash (the account would reject it)."
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
return signUserOpTypedDataForDelegation(this.delegateKey, signData.typed_data);
|
|
1092
|
+
}
|
|
1093
|
+
if (scheme === void 0) {
|
|
1094
|
+
return signHash(this.delegateKey, signData.hash);
|
|
1095
|
+
}
|
|
1096
|
+
throw new HavenSigningError(
|
|
1097
|
+
`Unknown sign_data.signature_scheme '${scheme}' \u2014 refusing to guess a signing scheme. Update @haven_ai/sdk.`
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
974
1100
|
/**
|
|
975
1101
|
* Step 3: Submit a signature to execute the payment.
|
|
976
1102
|
*
|
|
@@ -1091,9 +1217,10 @@ var HavenClient = class {
|
|
|
1091
1217
|
}
|
|
1092
1218
|
const ethBalance = await provider.getBalance(delegateAddress);
|
|
1093
1219
|
if (ethBalance > 0n) {
|
|
1094
|
-
const
|
|
1220
|
+
const fee = await provider.getFeeData();
|
|
1221
|
+
const effectiveGasPrice = fee.maxFeePerGas ?? fee.gasPrice ?? 1000000n;
|
|
1095
1222
|
const gasLimit = 21000n;
|
|
1096
|
-
const gasCost =
|
|
1223
|
+
const gasCost = effectiveGasPrice * gasLimit * 2n;
|
|
1097
1224
|
const ethToSend = ethBalance > gasCost ? ethBalance - gasCost : 0n;
|
|
1098
1225
|
if (ethToSend > 0n) {
|
|
1099
1226
|
const tx = await wallet.sendTransaction({ to: safeAddress, value: ethToSend });
|
|
@@ -1206,6 +1333,18 @@ var HavenClient = class {
|
|
|
1206
1333
|
const raw = await this.get(`/machine-payments/receipts${query}`);
|
|
1207
1334
|
return raw.receipts.map((receipt) => this.mapPaymentReceipt(receipt));
|
|
1208
1335
|
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Fetch the verifiable receipt bundle for a settled payment and verify it
|
|
1338
|
+
* locally. The server's own verification is ignored — the receipt is verified
|
|
1339
|
+
* here (independently of Haven) by recovering the signer from the
|
|
1340
|
+
* authorisation, so the result is trustworthy even if the backend lied.
|
|
1341
|
+
*/
|
|
1342
|
+
async getReceipt(paymentId) {
|
|
1343
|
+
const { receipt } = await this.get(
|
|
1344
|
+
`/payments/${paymentId}/receipt`
|
|
1345
|
+
);
|
|
1346
|
+
return { receipt, verification: verifyPaymentReceipt(receipt) };
|
|
1347
|
+
}
|
|
1209
1348
|
/**
|
|
1210
1349
|
* Rehydrate the x402/MPP resume-state bundle for a payment id.
|
|
1211
1350
|
*
|
|
@@ -1348,7 +1487,7 @@ var HavenClient = class {
|
|
|
1348
1487
|
if (!raw.sign_data?.hash) {
|
|
1349
1488
|
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
1350
1489
|
}
|
|
1351
|
-
const sig =
|
|
1490
|
+
const sig = await this.signForData(raw.sign_data);
|
|
1352
1491
|
const execResult = await this.post(
|
|
1353
1492
|
`/payments/${raw.payment_id}/sign`,
|
|
1354
1493
|
{ signature: sig }
|
|
@@ -1707,8 +1846,39 @@ var HavenClient = class {
|
|
|
1707
1846
|
protocolReceiptHeaderName: "PAYMENT-RESPONSE",
|
|
1708
1847
|
protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
|
|
1709
1848
|
});
|
|
1849
|
+
await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
|
|
1710
1850
|
return retryResponse;
|
|
1711
1851
|
}
|
|
1852
|
+
/**
|
|
1853
|
+
* #956: capture the merchant's OWN receipt when the paid response carries
|
|
1854
|
+
* one, and report it to Haven so the reporting feed can attach it next to
|
|
1855
|
+
* the Haven-generated payment evidence (#498). Two supported signals on the
|
|
1856
|
+
* paid response:
|
|
1857
|
+
*
|
|
1858
|
+
* x-receipt-json: base64-encoded JSON receipt document (inline)
|
|
1859
|
+
* x-receipt-url: https URL to the receipt document (reference)
|
|
1860
|
+
*
|
|
1861
|
+
* Strictly best-effort: absence is the normal case, and no failure here may
|
|
1862
|
+
* ever affect the completed payment — the response is already paid for.
|
|
1863
|
+
*/
|
|
1864
|
+
async reportMerchantReceipt(paymentId, response) {
|
|
1865
|
+
try {
|
|
1866
|
+
const inlineB64 = response.headers.get("x-receipt-json");
|
|
1867
|
+
const url = response.headers.get("x-receipt-url");
|
|
1868
|
+
if (!inlineB64 && !url) return;
|
|
1869
|
+
let body = null;
|
|
1870
|
+
if (inlineB64) {
|
|
1871
|
+
if (inlineB64.length > Math.ceil(64 * 1024 * 4 / 3)) return;
|
|
1872
|
+
const decoded = JSON.parse(Buffer.from(inlineB64, "base64").toString("utf8"));
|
|
1873
|
+
if (decoded && typeof decoded === "object") body = { json: decoded };
|
|
1874
|
+
} else if (url && url.startsWith("https://") && url.length <= 2048) {
|
|
1875
|
+
body = { url };
|
|
1876
|
+
}
|
|
1877
|
+
if (!body) return;
|
|
1878
|
+
await this.post(`/machine-payments/${paymentId}/merchant-receipt`, body);
|
|
1879
|
+
} catch {
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1712
1882
|
/**
|
|
1713
1883
|
* Deliver an already-signed x402 payment header to the merchant and return
|
|
1714
1884
|
* the merchant's response. Used by the hosted MCP server to complete the
|
|
@@ -1796,6 +1966,7 @@ var HavenClient = class {
|
|
|
1796
1966
|
protocolReceiptHeaderName: protocolReceiptHeader ? "PAYMENT-RESPONSE" : void 0,
|
|
1797
1967
|
protocolReceiptHeader
|
|
1798
1968
|
});
|
|
1969
|
+
await this.reportMerchantReceipt(evidenceContext.paymentId, surfaced);
|
|
1799
1970
|
}
|
|
1800
1971
|
return {
|
|
1801
1972
|
status: surfaced.status,
|
|
@@ -1890,7 +2061,7 @@ var HavenClient = class {
|
|
|
1890
2061
|
if (!raw.sign_data?.hash) {
|
|
1891
2062
|
throw new HavenApiError("No sign_hash returned from machine payment authorization", 500, raw);
|
|
1892
2063
|
}
|
|
1893
|
-
const sig =
|
|
2064
|
+
const sig = await this.signForData(raw.sign_data);
|
|
1894
2065
|
const execResult = await this.post(
|
|
1895
2066
|
`/payments/${raw.payment_id}/sign`,
|
|
1896
2067
|
{ signature: sig }
|
|
@@ -1993,6 +2164,7 @@ var HavenClient = class {
|
|
|
1993
2164
|
protocolReceiptHeaderName: retryResponse.headers.has("Payment-Receipt") ? "Payment-Receipt" : retryResponse.headers.has("MACHINE-PAYMENT-RESPONSE") ? "MACHINE-PAYMENT-RESPONSE" : void 0,
|
|
1994
2165
|
protocolReceiptHeader: retryResponse.headers.get("Payment-Receipt") ?? retryResponse.headers.get("MACHINE-PAYMENT-RESPONSE") ?? void 0
|
|
1995
2166
|
});
|
|
2167
|
+
await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
|
|
1996
2168
|
return retryResponse;
|
|
1997
2169
|
}
|
|
1998
2170
|
assertCanResumeX402(status, paymentRequired, option) {
|
|
@@ -2822,7 +2994,11 @@ var HavenClient = class {
|
|
|
2822
2994
|
});
|
|
2823
2995
|
const data = await res.json();
|
|
2824
2996
|
if (!res.ok) {
|
|
2825
|
-
const
|
|
2997
|
+
const record = data;
|
|
2998
|
+
const errorText = typeof record.error === "string" ? record.error : void 0;
|
|
2999
|
+
const rawDetails = record.details ?? record.detail;
|
|
3000
|
+
const detailsText = typeof rawDetails === "string" ? rawDetails : rawDetails != null ? JSON.stringify(rawDetails) : void 0;
|
|
3001
|
+
const message = errorText && detailsText ? `${errorText}: ${detailsText}` : errorText ?? detailsText ?? `API request failed`;
|
|
2826
3002
|
throw new HavenApiError(message, res.status, data);
|
|
2827
3003
|
}
|
|
2828
3004
|
return data;
|
|
@@ -2850,6 +3026,12 @@ var HavenClient = class {
|
|
|
2850
3026
|
txHash: raw.tx_hash,
|
|
2851
3027
|
errorMessage: raw.error_message,
|
|
2852
3028
|
explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : null),
|
|
3029
|
+
fee: raw.fee ? {
|
|
3030
|
+
amount: raw.fee.amount,
|
|
3031
|
+
token: raw.fee.token,
|
|
3032
|
+
basisPoints: raw.fee.basis_points,
|
|
3033
|
+
applied: raw.fee.applied
|
|
3034
|
+
} : null,
|
|
2853
3035
|
createdAt: raw.created_at,
|
|
2854
3036
|
signedAt: raw.signed_at,
|
|
2855
3037
|
submittedAt: raw.submitted_at,
|
|
@@ -2873,6 +3055,12 @@ var HavenClient = class {
|
|
|
2873
3055
|
expiresAt: raw.expires_at,
|
|
2874
3056
|
chainId: raw.chain_id,
|
|
2875
3057
|
message: raw.message,
|
|
3058
|
+
fee: raw.fee ? {
|
|
3059
|
+
amount: raw.fee.amount,
|
|
3060
|
+
token: raw.fee.token,
|
|
3061
|
+
basisPoints: raw.fee.basis_points,
|
|
3062
|
+
applied: raw.fee.applied
|
|
3063
|
+
} : null,
|
|
2876
3064
|
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? null,
|
|
2877
3065
|
asset: raw.asset ?? raw.x402?.asset ?? null,
|
|
2878
3066
|
network: raw.network ?? raw.x402?.network ?? null,
|
|
@@ -3018,13 +3206,13 @@ var toolDescriptions = {
|
|
|
3018
3206
|
getAgent: {
|
|
3019
3207
|
summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, a readiness signal, and per-token remaining allowance (atomic + human-readable). The recommended first call in a new session to confirm who you are and whether you can pay right now.",
|
|
3020
3208
|
selectionGuidance: "Use this as the one-shot orientation/bootstrap at the start of a session, or whenever you need to confirm identity together with whether the agent can spend right now. For a detailed per-token breakdown (configured vs spent vs reset window) use haven_get_allowances.",
|
|
3021
|
-
behavior: 'Reads identity plus the on-chain AllowanceModule
|
|
3209
|
+
behavior: 'Reads identity plus the live spend-authority snapshot in one shot \u2014 the on-chain AllowanceModule on the legacy rail, the active budget delegation on the delegation rail. readiness 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. What an over-budget payment does differs by rail: on the legacy AllowanceModule rail it is queued for the wallet owner to approve in Haven; on the delegation rail there is no approval queue \u2014 an over-budget redemption reverts on-chain, so ask the owner to grant or raise the budget in Haven rather than waiting for an approval. allowances[] carries remainingAtomic and remainingDisplay per token. Identity fields (id, name, status, safeAddress, delegateAddress, chainId) are unchanged from before.',
|
|
3022
3210
|
nextActionGuidance: ""
|
|
3023
3211
|
},
|
|
3024
3212
|
getAllowances: {
|
|
3025
3213
|
summary: "Return configured and on-chain allowance state for the authenticated agent. On-chain allowance is the real spend gate.",
|
|
3026
3214
|
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.",
|
|
3027
|
-
behavior: "
|
|
3215
|
+
behavior: "Returns the per-token spend authority for the account's rail: the Safe AllowanceModule snapshot (allowance, spent, remaining, reset window) on the legacy rail, or the active budget delegation (remaining = the period budget; over-budget redemptions revert on-chain, nothing queues) on the delegation rail. Configured amounts from Haven are returned alongside.",
|
|
3028
3216
|
nextActionGuidance: ""
|
|
3029
3217
|
},
|
|
3030
3218
|
listReceipts: {
|
|
@@ -3033,6 +3221,12 @@ var toolDescriptions = {
|
|
|
3033
3221
|
behavior: "Returns the agent's recent machine-payment receipts ordered by recency. Proof header values are not returned.",
|
|
3034
3222
|
nextActionGuidance: ""
|
|
3035
3223
|
},
|
|
3224
|
+
verifyReceipt: {
|
|
3225
|
+
summary: "Verify a payment receipt offline \u2014 confirm the agent authorised the transfer.",
|
|
3226
|
+
selectionGuidance: "Use this to check a receipt you already hold; it needs no network and does not trust Haven. Use the history tool to fetch receipts in the first place.",
|
|
3227
|
+
behavior: "Recovers the signer from the receipt authorisation and confirms it matches the agent delegate. Returns verified true/false with the recovered signer or a reason. Pure and local \u2014 no backend call.",
|
|
3228
|
+
nextActionGuidance: ""
|
|
3229
|
+
},
|
|
3036
3230
|
payMcpTool: {
|
|
3037
3231
|
summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip.",
|
|
3038
3232
|
selectionGuidance: "Use this when the agent wants to call a specific tool on an MCP merchant (e.g. Soundside, Coinbase Bazaar) and payment is required. Prefer this over haven_pay_x402 when you know the merchant_url and tool_name \u2014 it builds the JSON-RPC envelope internally. Use haven_pay_x402 for arbitrary HTTP resources. Do NOT use for read-only allowance or budget questions \u2014 use haven_get_allowances.",
|
|
@@ -3420,20 +3614,72 @@ for that credential.
|
|
|
3420
3614
|
`;
|
|
3421
3615
|
var SKILL_FOLDER_NAME = "haven-pay";
|
|
3422
3616
|
|
|
3617
|
+
// src/node-version.ts
|
|
3618
|
+
var HAVEN_MINIMUM_NODE_VERSION = "24.0.0";
|
|
3619
|
+
function parseNodeVersion(value) {
|
|
3620
|
+
const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
3621
|
+
if (!match) return [0, 0, 0];
|
|
3622
|
+
return [Number(match[1] ?? 0), Number(match[2] ?? 0), Number(match[3] ?? 0)];
|
|
3623
|
+
}
|
|
3624
|
+
function compareNodeVersions(left, right) {
|
|
3625
|
+
const leftParts = parseNodeVersion(left);
|
|
3626
|
+
const rightParts = parseNodeVersion(right);
|
|
3627
|
+
for (let i = 0; i < 3; i += 1) {
|
|
3628
|
+
if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
|
|
3629
|
+
}
|
|
3630
|
+
return 0;
|
|
3631
|
+
}
|
|
3632
|
+
function isSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = HAVEN_MINIMUM_NODE_VERSION) {
|
|
3633
|
+
return compareNodeVersions(nodeVersion, minimumNodeVersion) >= 0;
|
|
3634
|
+
}
|
|
3635
|
+
function unsupportedNodeVersionMessage(options) {
|
|
3636
|
+
const nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
3637
|
+
const minimum = options.minimumNodeVersion ?? HAVEN_MINIMUM_NODE_VERSION;
|
|
3638
|
+
const lines = [
|
|
3639
|
+
`${options.subject} requires Node.js >=${minimum}, but this is Node.js ${nodeVersion}.`,
|
|
3640
|
+
"",
|
|
3641
|
+
"Upgrade Node, then try again:",
|
|
3642
|
+
` nvm install ${major(minimum)} && nvm use ${major(minimum)} (nvm)`,
|
|
3643
|
+
` fnm install ${major(minimum)} && fnm use ${major(minimum)} (fnm)`,
|
|
3644
|
+
` volta install node@${major(minimum)} (volta)`,
|
|
3645
|
+
` or download Node ${major(minimum)} from https://nodejs.org`,
|
|
3646
|
+
"",
|
|
3647
|
+
"If you use a version manager, check that the agent runtime launching Haven picks up the same version \u2014 upgrading your shell does not always change what a desktop app spawns."
|
|
3648
|
+
];
|
|
3649
|
+
if (options.retryHint) lines.push("", options.retryHint);
|
|
3650
|
+
return lines.join("\n");
|
|
3651
|
+
}
|
|
3652
|
+
function major(version) {
|
|
3653
|
+
return String(parseNodeVersion(version)[0]);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3423
3656
|
// src/sweep.ts
|
|
3424
3657
|
var SWEEP_BASE_CHAIN_ID = 8453;
|
|
3658
|
+
var SWEEP_BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
3425
3659
|
var SWEEP_BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
3660
|
+
var SWEEP_BASE_SEPOLIA_USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
3426
3661
|
var USDC_EIP712_DOMAIN_BY_CHAIN = {
|
|
3427
3662
|
[SWEEP_BASE_CHAIN_ID]: {
|
|
3428
3663
|
name: "USD Coin",
|
|
3429
3664
|
version: "2",
|
|
3430
3665
|
chainId: SWEEP_BASE_CHAIN_ID,
|
|
3431
3666
|
verifyingContract: SWEEP_BASE_USDC_ADDRESS
|
|
3667
|
+
},
|
|
3668
|
+
[SWEEP_BASE_SEPOLIA_CHAIN_ID]: {
|
|
3669
|
+
name: "USDC",
|
|
3670
|
+
version: "2",
|
|
3671
|
+
chainId: SWEEP_BASE_SEPOLIA_CHAIN_ID,
|
|
3672
|
+
verifyingContract: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
|
|
3432
3673
|
}
|
|
3433
3674
|
};
|
|
3434
3675
|
var USDC_ADDRESS_BY_CHAIN = {
|
|
3435
|
-
[SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS
|
|
3676
|
+
[SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS,
|
|
3677
|
+
[SWEEP_BASE_SEPOLIA_CHAIN_ID]: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
|
|
3436
3678
|
};
|
|
3679
|
+
var SWEEPABLE_CHAIN_IDS = Object.keys(USDC_ADDRESS_BY_CHAIN).map(Number);
|
|
3680
|
+
function isSweepableChain(chainId) {
|
|
3681
|
+
return chainId in USDC_ADDRESS_BY_CHAIN;
|
|
3682
|
+
}
|
|
3437
3683
|
var TRANSFER_WITH_AUTHORIZATION_TYPES = {
|
|
3438
3684
|
TransferWithAuthorization: [
|
|
3439
3685
|
{ name: "from", type: "address" },
|
|
@@ -3448,7 +3694,7 @@ function sweepUsdcAddress(chainId) {
|
|
|
3448
3694
|
const address = USDC_ADDRESS_BY_CHAIN[chainId];
|
|
3449
3695
|
if (!address) {
|
|
3450
3696
|
throw new HavenSigningError(
|
|
3451
|
-
`Sweep is
|
|
3697
|
+
`Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
|
|
3452
3698
|
);
|
|
3453
3699
|
}
|
|
3454
3700
|
return address;
|
|
@@ -3457,7 +3703,7 @@ function sweepUsdcDomain(chainId) {
|
|
|
3457
3703
|
const domain = USDC_EIP712_DOMAIN_BY_CHAIN[chainId];
|
|
3458
3704
|
if (!domain) {
|
|
3459
3705
|
throw new HavenSigningError(
|
|
3460
|
-
`Sweep is
|
|
3706
|
+
`Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
|
|
3461
3707
|
);
|
|
3462
3708
|
}
|
|
3463
3709
|
return domain;
|
|
@@ -3528,6 +3774,7 @@ exports.AgentPaymentPhaseSchema = AgentPaymentPhaseSchema;
|
|
|
3528
3774
|
exports.AgentPaymentRail = AgentPaymentRail;
|
|
3529
3775
|
exports.AgentPaymentRailDescriptions = AgentPaymentRailDescriptions;
|
|
3530
3776
|
exports.AgentPaymentRailSchema = AgentPaymentRailSchema;
|
|
3777
|
+
exports.HAVEN_MINIMUM_NODE_VERSION = HAVEN_MINIMUM_NODE_VERSION;
|
|
3531
3778
|
exports.HAVEN_SKILL_MD = HAVEN_SKILL_MD;
|
|
3532
3779
|
exports.HavenApiError = HavenApiError;
|
|
3533
3780
|
exports.HavenClient = HavenClient;
|
|
@@ -3535,15 +3782,21 @@ exports.HavenError = HavenError;
|
|
|
3535
3782
|
exports.HavenPaymentStateError = HavenPaymentStateError;
|
|
3536
3783
|
exports.HavenSigningError = HavenSigningError;
|
|
3537
3784
|
exports.HavenTimeoutError = HavenTimeoutError;
|
|
3785
|
+
exports.RECEIPT_VERSION = RECEIPT_VERSION;
|
|
3538
3786
|
exports.SKILL_FOLDER_NAME = SKILL_FOLDER_NAME;
|
|
3539
3787
|
exports.SWEEP_BASE_CHAIN_ID = SWEEP_BASE_CHAIN_ID;
|
|
3788
|
+
exports.SWEEP_BASE_SEPOLIA_CHAIN_ID = SWEEP_BASE_SEPOLIA_CHAIN_ID;
|
|
3789
|
+
exports.SWEEP_BASE_SEPOLIA_USDC_ADDRESS = SWEEP_BASE_SEPOLIA_USDC_ADDRESS;
|
|
3540
3790
|
exports.SWEEP_BASE_USDC_ADDRESS = SWEEP_BASE_USDC_ADDRESS;
|
|
3541
3791
|
exports.TRANSFER_WITH_AUTHORIZATION_TYPES = TRANSFER_WITH_AUTHORIZATION_TYPES;
|
|
3792
|
+
exports.X402_MAX_AUTHORIZATION_WINDOW_SECONDS = X402_MAX_AUTHORIZATION_WINDOW_SECONDS;
|
|
3793
|
+
exports.X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = X402_SETTLEMENT_FORWARD_MARGIN_SECONDS;
|
|
3542
3794
|
exports.addressFromKey = addressFromKey;
|
|
3543
3795
|
exports.buildMachinePaymentIdempotencyKey = buildMachinePaymentIdempotencyKey;
|
|
3544
3796
|
exports.buildSweepAuthorizationMessage = buildSweepAuthorizationMessage;
|
|
3545
3797
|
exports.buildSweepTypedData = buildSweepTypedData;
|
|
3546
3798
|
exports.buildX402ExpectedMessage = buildX402ExpectedMessage;
|
|
3799
|
+
exports.compareNodeVersions = compareNodeVersions;
|
|
3547
3800
|
exports.composeDescription = composeDescription;
|
|
3548
3801
|
exports.decodeBase64Json = decodeBase64Json;
|
|
3549
3802
|
exports.decodeBase64Utf8 = decodeBase64Utf8;
|
|
@@ -3552,6 +3805,8 @@ exports.encodeBase64Utf8 = encodeBase64Utf8;
|
|
|
3552
3805
|
exports.encodeMachinePaymentProof = encodeMachinePaymentProof;
|
|
3553
3806
|
exports.encodePaymentProof = encodePaymentProof;
|
|
3554
3807
|
exports.havenTools = havenTools;
|
|
3808
|
+
exports.isSupportedNodeVersion = isSupportedNodeVersion;
|
|
3809
|
+
exports.isSweepableChain = isSweepableChain;
|
|
3555
3810
|
exports.parseMachinePaymentChallenge = parseMachinePaymentChallenge;
|
|
3556
3811
|
exports.parseMachinePaymentChallengeResponse = parseMachinePaymentChallengeResponse;
|
|
3557
3812
|
exports.parsePaymentRequired = parsePaymentRequired;
|
|
@@ -3559,10 +3814,13 @@ exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
|
|
|
3559
3814
|
exports.selectPaymentOption = selectPaymentOption;
|
|
3560
3815
|
exports.selectStandardPaymentOption = selectStandardPaymentOption;
|
|
3561
3816
|
exports.signHash = signHash;
|
|
3817
|
+
exports.signUserOpTypedDataForDelegation = signUserOpTypedDataForDelegation;
|
|
3562
3818
|
exports.sweepUsdcAddress = sweepUsdcAddress;
|
|
3563
3819
|
exports.sweepUsdcDomain = sweepUsdcDomain;
|
|
3564
3820
|
exports.toStandardPaymentRequirements = toStandardPaymentRequirements;
|
|
3565
3821
|
exports.toolDescriptions = toolDescriptions;
|
|
3822
|
+
exports.unsupportedNodeVersionMessage = unsupportedNodeVersionMessage;
|
|
3823
|
+
exports.verifyPaymentReceipt = verifyPaymentReceipt;
|
|
3566
3824
|
exports.verifySignature = verifySignature;
|
|
3567
3825
|
exports.x402AuthorizationAmount = x402AuthorizationAmount;
|
|
3568
3826
|
//# sourceMappingURL=index.cjs.map
|