@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.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
2
|
import { exact } from 'x402/schemes';
|
|
3
|
+
import { hashTypedData } from 'viem';
|
|
3
4
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
4
5
|
import { ethers } from 'ethers';
|
|
5
6
|
import { createHash } from 'crypto';
|
|
@@ -240,6 +241,22 @@ function signHash(privateKey, hash) {
|
|
|
240
241
|
);
|
|
241
242
|
}
|
|
242
243
|
}
|
|
244
|
+
async function signUserOpTypedDataForDelegation(privateKey, typedData) {
|
|
245
|
+
try {
|
|
246
|
+
const wallet = new ethers.Wallet(privateKey);
|
|
247
|
+
const types = { ...typedData.types };
|
|
248
|
+
delete types.EIP712Domain;
|
|
249
|
+
return await wallet.signTypedData(
|
|
250
|
+
typedData.domain,
|
|
251
|
+
types,
|
|
252
|
+
typedData.message
|
|
253
|
+
);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
throw new HavenSigningError(
|
|
256
|
+
`Failed to sign delegation UserOperation: ${err instanceof Error ? err.message : String(err)}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
243
260
|
function addressFromKey(privateKey) {
|
|
244
261
|
try {
|
|
245
262
|
return new ethers.Wallet(privateKey).address;
|
|
@@ -257,6 +274,24 @@ function verifySignature(hash, signature, expectedAddress) {
|
|
|
257
274
|
return false;
|
|
258
275
|
}
|
|
259
276
|
}
|
|
277
|
+
var RECEIPT_VERSION = "haven-receipt-1";
|
|
278
|
+
function defaultRecover(hash, signature) {
|
|
279
|
+
return ethers.recoverAddress(hash, signature);
|
|
280
|
+
}
|
|
281
|
+
function verifyPaymentReceipt(receipt, recover = defaultRecover) {
|
|
282
|
+
const { delegate, signHash: signHash2, signature } = receipt.authorization;
|
|
283
|
+
if (!signature) return { verified: false, reason: "missing_signature" };
|
|
284
|
+
let recovered;
|
|
285
|
+
try {
|
|
286
|
+
recovered = recover(signHash2, signature);
|
|
287
|
+
} catch {
|
|
288
|
+
return { verified: false, reason: "bad_signature" };
|
|
289
|
+
}
|
|
290
|
+
if (recovered.toLowerCase() !== delegate.toLowerCase()) {
|
|
291
|
+
return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
|
|
292
|
+
}
|
|
293
|
+
return { verified: true, recoveredSigner: recovered };
|
|
294
|
+
}
|
|
260
295
|
|
|
261
296
|
// src/base64.ts
|
|
262
297
|
function normalizeBase64(value) {
|
|
@@ -301,6 +336,8 @@ function decodeBase64Json(value, label) {
|
|
|
301
336
|
|
|
302
337
|
// src/x402.ts
|
|
303
338
|
var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
339
|
+
var BASE_SEPOLIA_USDC_ADDRESS = "0x036cbd53842c5426634e7929541ec2318f3dcf7e";
|
|
340
|
+
var STANDARD_X402_USDC_ADDRESSES = /* @__PURE__ */ new Set([BASE_USDC_ADDRESS, BASE_SEPOLIA_USDC_ADDRESS]);
|
|
304
341
|
var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
|
|
305
342
|
var DECIMAL_ATOMIC_AMOUNT_RE = /^[0-9]+$/;
|
|
306
343
|
function isPositiveDecimalAtomicAmount(value) {
|
|
@@ -309,6 +346,12 @@ function isPositiveDecimalAtomicAmount(value) {
|
|
|
309
346
|
function optionAuthorizationAmount(option) {
|
|
310
347
|
return option.maxAmountRequired ?? option.amount;
|
|
311
348
|
}
|
|
349
|
+
var X402_MAX_AUTHORIZATION_WINDOW_SECONDS = 600;
|
|
350
|
+
var X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = 300;
|
|
351
|
+
function clampAuthorizationWindow(seconds) {
|
|
352
|
+
const requested = typeof seconds === "number" && Number.isFinite(seconds) ? seconds : 30;
|
|
353
|
+
return Math.min(Math.max(Math.floor(requested), 1), X402_MAX_AUTHORIZATION_WINDOW_SECONDS);
|
|
354
|
+
}
|
|
312
355
|
function normalizePaymentOption(value) {
|
|
313
356
|
const candidate = value;
|
|
314
357
|
if (!candidate || typeof candidate !== "object" || typeof candidate.scheme !== "string" || typeof candidate.network !== "string" || typeof candidate.asset !== "string" || typeof candidate.payTo !== "string") {
|
|
@@ -330,7 +373,7 @@ function normalizePaymentOption(value) {
|
|
|
330
373
|
mimeType: candidate.mimeType,
|
|
331
374
|
asset: candidate.asset,
|
|
332
375
|
payTo: candidate.payTo,
|
|
333
|
-
maxTimeoutSeconds: candidate.maxTimeoutSeconds
|
|
376
|
+
maxTimeoutSeconds: clampAuthorizationWindow(candidate.maxTimeoutSeconds),
|
|
334
377
|
extra: candidate.extra
|
|
335
378
|
};
|
|
336
379
|
}
|
|
@@ -360,11 +403,15 @@ function normalizePaymentRequired(value) {
|
|
|
360
403
|
var SUPPORTED_X402_NETWORKS = {
|
|
361
404
|
"eip155:100": "Gnosis Chain",
|
|
362
405
|
"eip155:8453": "Base",
|
|
363
|
-
"base": "Base"
|
|
406
|
+
"base": "Base",
|
|
407
|
+
"eip155:84532": "Base Sepolia",
|
|
408
|
+
"base-sepolia": "Base Sepolia"
|
|
364
409
|
};
|
|
365
410
|
var STANDARD_X402_NETWORKS = {
|
|
366
411
|
"eip155:8453": "base",
|
|
367
|
-
"base": "base"
|
|
412
|
+
"base": "base",
|
|
413
|
+
"eip155:84532": "base-sepolia",
|
|
414
|
+
"base-sepolia": "base-sepolia"
|
|
368
415
|
};
|
|
369
416
|
var GNOSIS_TOKENS = {
|
|
370
417
|
"0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
|
|
@@ -375,14 +422,21 @@ var BASE_TOKENS = {
|
|
|
375
422
|
"0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
|
|
376
423
|
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { symbol: "USDC", decimals: 6 }
|
|
377
424
|
};
|
|
425
|
+
var BASE_SEPOLIA_TOKENS = {
|
|
426
|
+
"0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
|
|
427
|
+
"0x036cbd53842c5426634e7929541ec2318f3dcf7e": { symbol: "USDC", decimals: 6 }
|
|
428
|
+
};
|
|
378
429
|
var ALL_TOKENS = {
|
|
379
430
|
...GNOSIS_TOKENS,
|
|
380
|
-
...BASE_TOKENS
|
|
431
|
+
...BASE_TOKENS,
|
|
432
|
+
...BASE_SEPOLIA_TOKENS
|
|
381
433
|
};
|
|
382
434
|
var NETWORK_TOKENS = {
|
|
383
435
|
"eip155:100": GNOSIS_TOKENS,
|
|
384
436
|
"eip155:8453": BASE_TOKENS,
|
|
385
|
-
"base": BASE_TOKENS
|
|
437
|
+
"base": BASE_TOKENS,
|
|
438
|
+
"eip155:84532": BASE_SEPOLIA_TOKENS,
|
|
439
|
+
"base-sepolia": BASE_SEPOLIA_TOKENS
|
|
386
440
|
};
|
|
387
441
|
function parsePaymentRequired(response) {
|
|
388
442
|
const v2Header = response.headers.get("PAYMENT-REQUIRED");
|
|
@@ -436,7 +490,7 @@ function selectPaymentOption(accepts) {
|
|
|
436
490
|
function selectStandardPaymentOption(accepts) {
|
|
437
491
|
if (!accepts || accepts.length === 0) return null;
|
|
438
492
|
for (const opt of accepts) {
|
|
439
|
-
if (opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && opt.asset.toLowerCase()
|
|
493
|
+
if (opt.scheme === "exact" && opt.network in STANDARD_X402_NETWORKS && STANDARD_X402_USDC_ADDRESSES.has(opt.asset.toLowerCase()) && isPositiveDecimalAtomicAmount(optionAuthorizationAmount(opt))) {
|
|
440
494
|
return opt;
|
|
441
495
|
}
|
|
442
496
|
}
|
|
@@ -450,8 +504,9 @@ function x402AuthorizationAmount(option) {
|
|
|
450
504
|
return amount;
|
|
451
505
|
}
|
|
452
506
|
function buildX402ExpectedMessage(context) {
|
|
507
|
+
const version = context.typedDataHash ? 2 : 1;
|
|
453
508
|
const payload = {
|
|
454
|
-
version
|
|
509
|
+
version,
|
|
455
510
|
kind: "haven.x402.expected",
|
|
456
511
|
paymentId: context.paymentId,
|
|
457
512
|
payloadHash: context.payloadHash.toLowerCase(),
|
|
@@ -464,7 +519,10 @@ function buildX402ExpectedMessage(context) {
|
|
|
464
519
|
if (context.expiresAt) {
|
|
465
520
|
payload.expiresAt = context.expiresAt;
|
|
466
521
|
}
|
|
467
|
-
|
|
522
|
+
if (context.typedDataHash) {
|
|
523
|
+
payload.typedDataHash = context.typedDataHash.toLowerCase();
|
|
524
|
+
}
|
|
525
|
+
return `Haven x402 expected context v${version}
|
|
468
526
|
${stableStringify(payload)}`;
|
|
469
527
|
}
|
|
470
528
|
function toStandardPaymentRequirements(paymentRequired, option) {
|
|
@@ -484,7 +542,15 @@ function toStandardPaymentRequirements(paymentRequired, option) {
|
|
|
484
542
|
mimeType: option.mimeType ?? paymentRequired.resource.mimeType ?? "application/octet-stream",
|
|
485
543
|
payTo: option.payTo,
|
|
486
544
|
asset: option.asset,
|
|
487
|
-
|
|
545
|
+
// Second enforcement point (#715): the parse path clamps too, but this is
|
|
546
|
+
// the last stop before the x402 library turns the timeout into
|
|
547
|
+
// `validBefore` — options constructed without parsing are bounded here.
|
|
548
|
+
// The forward margin (#1256) is added ONLY here, at signing: the parse
|
|
549
|
+
// path keeps recording the merchant's advertised timeout unchanged, and
|
|
550
|
+
// the library's `validBefore = now + this value` then carries enough
|
|
551
|
+
// slack to satisfy the facilitator's `validBefore ≥ now + maxTimeout`
|
|
552
|
+
// verify rule after our funding leg confirms.
|
|
553
|
+
maxTimeoutSeconds: clampAuthorizationWindow(option.maxTimeoutSeconds) + X402_SETTLEMENT_FORWARD_MARGIN_SECONDS,
|
|
488
554
|
extra: option.extra
|
|
489
555
|
};
|
|
490
556
|
}
|
|
@@ -618,7 +684,8 @@ var CHAIN_EXPLORER_TX = {
|
|
|
618
684
|
8453: "https://basescan.org/tx"
|
|
619
685
|
};
|
|
620
686
|
var CHAIN_USDC = {
|
|
621
|
-
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
687
|
+
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
688
|
+
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
622
689
|
};
|
|
623
690
|
function buildExplorerUrl(chainId, txHash) {
|
|
624
691
|
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
@@ -781,6 +848,16 @@ function parseSseJsonRpcMessages(text) {
|
|
|
781
848
|
function selectJsonRpcResult(messages) {
|
|
782
849
|
return messages.find((m) => "result" in m || "error" in m) ?? messages[messages.length - 1];
|
|
783
850
|
}
|
|
851
|
+
function x402TypedDataDigest(typedData) {
|
|
852
|
+
if (!typedData || typeof typedData !== "object") return void 0;
|
|
853
|
+
try {
|
|
854
|
+
return hashTypedData(typedData);
|
|
855
|
+
} catch (err) {
|
|
856
|
+
throw new HavenSigningError(
|
|
857
|
+
`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)}`
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
784
861
|
var HavenClient = class {
|
|
785
862
|
apiKey;
|
|
786
863
|
delegateKey;
|
|
@@ -930,6 +1007,11 @@ var HavenClient = class {
|
|
|
930
1007
|
if (!raw.sign_data?.hash) {
|
|
931
1008
|
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
932
1009
|
}
|
|
1010
|
+
if (raw.sign_data.signature_scheme !== void 0 && !raw.sign_data.typed_data) {
|
|
1011
|
+
throw new HavenSigningError(
|
|
1012
|
+
`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.`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
933
1015
|
if (!raw.x402_expected_auth) {
|
|
934
1016
|
throw new HavenApiError("No x402 expected-context binding returned from x402/authorize", 500, raw);
|
|
935
1017
|
}
|
|
@@ -946,6 +1028,14 @@ var HavenClient = class {
|
|
|
946
1028
|
asset: option.asset,
|
|
947
1029
|
network: option.network,
|
|
948
1030
|
expectedAuth: raw.x402_expected_auth,
|
|
1031
|
+
// #1138: the digest the delegation-rail expected context commits to.
|
|
1032
|
+
// Re-derived locally, exactly like every other context field the edge
|
|
1033
|
+
// signer is handed (amount, merchantTo, …) — none of them are trusted
|
|
1034
|
+
// because they arrived, they are trusted because the reconstructed
|
|
1035
|
+
// message has to match Haven's signature over it. A typed_data altered in
|
|
1036
|
+
// transit therefore fails message equality and is refused, and the signer
|
|
1037
|
+
// re-derives this digest a second time from the payload it actually signs.
|
|
1038
|
+
expectedTypedDataHash: x402TypedDataDigest(raw.sign_data.typed_data),
|
|
949
1039
|
fundingTo
|
|
950
1040
|
};
|
|
951
1041
|
}
|
|
@@ -969,6 +1059,42 @@ var HavenClient = class {
|
|
|
969
1059
|
}
|
|
970
1060
|
return signature;
|
|
971
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Sign a payment's `sign_data` with the correct scheme for its rail.
|
|
1064
|
+
*
|
|
1065
|
+
* Dispatching on the server-provided scheme means a caller never has to
|
|
1066
|
+
* know which rail an account is on; an unknown scheme is a hard error,
|
|
1067
|
+
* never a guessed signature. The session rail's 'eip191_userop' is retired
|
|
1068
|
+
* (#834) — the backend refuses those intents with HTTP 410 before any
|
|
1069
|
+
* sign_data reaches a client, so encountering it here is a hard error too.
|
|
1070
|
+
*/
|
|
1071
|
+
async signForData(signData) {
|
|
1072
|
+
if (!this.delegateKey) {
|
|
1073
|
+
throw new HavenSigningError(
|
|
1074
|
+
"Cannot sign without a delegateKey. Pass the private key in HavenClient config, or sign externally."
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
const scheme = signData.signature_scheme;
|
|
1078
|
+
if (scheme === "eip191_userop") {
|
|
1079
|
+
throw new HavenSigningError(
|
|
1080
|
+
"The session rail is retired \u2014 'eip191_userop' intents can no longer be signed. Re-onboard the account on the delegation rail."
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
if (scheme === "eip712_userop") {
|
|
1084
|
+
if (!signData.typed_data) {
|
|
1085
|
+
throw new HavenSigningError(
|
|
1086
|
+
"sign_data.signature_scheme is eip712_userop but typed_data is missing \u2014 refusing to sign the bare hash (the account would reject it)."
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
return signUserOpTypedDataForDelegation(this.delegateKey, signData.typed_data);
|
|
1090
|
+
}
|
|
1091
|
+
if (scheme === void 0) {
|
|
1092
|
+
return signHash(this.delegateKey, signData.hash);
|
|
1093
|
+
}
|
|
1094
|
+
throw new HavenSigningError(
|
|
1095
|
+
`Unknown sign_data.signature_scheme '${scheme}' \u2014 refusing to guess a signing scheme. Update @haven_ai/sdk.`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
972
1098
|
/**
|
|
973
1099
|
* Step 3: Submit a signature to execute the payment.
|
|
974
1100
|
*
|
|
@@ -1089,9 +1215,10 @@ var HavenClient = class {
|
|
|
1089
1215
|
}
|
|
1090
1216
|
const ethBalance = await provider.getBalance(delegateAddress);
|
|
1091
1217
|
if (ethBalance > 0n) {
|
|
1092
|
-
const
|
|
1218
|
+
const fee = await provider.getFeeData();
|
|
1219
|
+
const effectiveGasPrice = fee.maxFeePerGas ?? fee.gasPrice ?? 1000000n;
|
|
1093
1220
|
const gasLimit = 21000n;
|
|
1094
|
-
const gasCost =
|
|
1221
|
+
const gasCost = effectiveGasPrice * gasLimit * 2n;
|
|
1095
1222
|
const ethToSend = ethBalance > gasCost ? ethBalance - gasCost : 0n;
|
|
1096
1223
|
if (ethToSend > 0n) {
|
|
1097
1224
|
const tx = await wallet.sendTransaction({ to: safeAddress, value: ethToSend });
|
|
@@ -1204,6 +1331,18 @@ var HavenClient = class {
|
|
|
1204
1331
|
const raw = await this.get(`/machine-payments/receipts${query}`);
|
|
1205
1332
|
return raw.receipts.map((receipt) => this.mapPaymentReceipt(receipt));
|
|
1206
1333
|
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Fetch the verifiable receipt bundle for a settled payment and verify it
|
|
1336
|
+
* locally. The server's own verification is ignored — the receipt is verified
|
|
1337
|
+
* here (independently of Haven) by recovering the signer from the
|
|
1338
|
+
* authorisation, so the result is trustworthy even if the backend lied.
|
|
1339
|
+
*/
|
|
1340
|
+
async getReceipt(paymentId) {
|
|
1341
|
+
const { receipt } = await this.get(
|
|
1342
|
+
`/payments/${paymentId}/receipt`
|
|
1343
|
+
);
|
|
1344
|
+
return { receipt, verification: verifyPaymentReceipt(receipt) };
|
|
1345
|
+
}
|
|
1207
1346
|
/**
|
|
1208
1347
|
* Rehydrate the x402/MPP resume-state bundle for a payment id.
|
|
1209
1348
|
*
|
|
@@ -1346,7 +1485,7 @@ var HavenClient = class {
|
|
|
1346
1485
|
if (!raw.sign_data?.hash) {
|
|
1347
1486
|
throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
|
|
1348
1487
|
}
|
|
1349
|
-
const sig =
|
|
1488
|
+
const sig = await this.signForData(raw.sign_data);
|
|
1350
1489
|
const execResult = await this.post(
|
|
1351
1490
|
`/payments/${raw.payment_id}/sign`,
|
|
1352
1491
|
{ signature: sig }
|
|
@@ -1705,8 +1844,39 @@ var HavenClient = class {
|
|
|
1705
1844
|
protocolReceiptHeaderName: "PAYMENT-RESPONSE",
|
|
1706
1845
|
protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
|
|
1707
1846
|
});
|
|
1847
|
+
await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
|
|
1708
1848
|
return retryResponse;
|
|
1709
1849
|
}
|
|
1850
|
+
/**
|
|
1851
|
+
* #956: capture the merchant's OWN receipt when the paid response carries
|
|
1852
|
+
* one, and report it to Haven so the reporting feed can attach it next to
|
|
1853
|
+
* the Haven-generated payment evidence (#498). Two supported signals on the
|
|
1854
|
+
* paid response:
|
|
1855
|
+
*
|
|
1856
|
+
* x-receipt-json: base64-encoded JSON receipt document (inline)
|
|
1857
|
+
* x-receipt-url: https URL to the receipt document (reference)
|
|
1858
|
+
*
|
|
1859
|
+
* Strictly best-effort: absence is the normal case, and no failure here may
|
|
1860
|
+
* ever affect the completed payment — the response is already paid for.
|
|
1861
|
+
*/
|
|
1862
|
+
async reportMerchantReceipt(paymentId, response) {
|
|
1863
|
+
try {
|
|
1864
|
+
const inlineB64 = response.headers.get("x-receipt-json");
|
|
1865
|
+
const url = response.headers.get("x-receipt-url");
|
|
1866
|
+
if (!inlineB64 && !url) return;
|
|
1867
|
+
let body = null;
|
|
1868
|
+
if (inlineB64) {
|
|
1869
|
+
if (inlineB64.length > Math.ceil(64 * 1024 * 4 / 3)) return;
|
|
1870
|
+
const decoded = JSON.parse(Buffer.from(inlineB64, "base64").toString("utf8"));
|
|
1871
|
+
if (decoded && typeof decoded === "object") body = { json: decoded };
|
|
1872
|
+
} else if (url && url.startsWith("https://") && url.length <= 2048) {
|
|
1873
|
+
body = { url };
|
|
1874
|
+
}
|
|
1875
|
+
if (!body) return;
|
|
1876
|
+
await this.post(`/machine-payments/${paymentId}/merchant-receipt`, body);
|
|
1877
|
+
} catch {
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1710
1880
|
/**
|
|
1711
1881
|
* Deliver an already-signed x402 payment header to the merchant and return
|
|
1712
1882
|
* the merchant's response. Used by the hosted MCP server to complete the
|
|
@@ -1794,6 +1964,7 @@ var HavenClient = class {
|
|
|
1794
1964
|
protocolReceiptHeaderName: protocolReceiptHeader ? "PAYMENT-RESPONSE" : void 0,
|
|
1795
1965
|
protocolReceiptHeader
|
|
1796
1966
|
});
|
|
1967
|
+
await this.reportMerchantReceipt(evidenceContext.paymentId, surfaced);
|
|
1797
1968
|
}
|
|
1798
1969
|
return {
|
|
1799
1970
|
status: surfaced.status,
|
|
@@ -1888,7 +2059,7 @@ var HavenClient = class {
|
|
|
1888
2059
|
if (!raw.sign_data?.hash) {
|
|
1889
2060
|
throw new HavenApiError("No sign_hash returned from machine payment authorization", 500, raw);
|
|
1890
2061
|
}
|
|
1891
|
-
const sig =
|
|
2062
|
+
const sig = await this.signForData(raw.sign_data);
|
|
1892
2063
|
const execResult = await this.post(
|
|
1893
2064
|
`/payments/${raw.payment_id}/sign`,
|
|
1894
2065
|
{ signature: sig }
|
|
@@ -1991,6 +2162,7 @@ var HavenClient = class {
|
|
|
1991
2162
|
protocolReceiptHeaderName: retryResponse.headers.has("Payment-Receipt") ? "Payment-Receipt" : retryResponse.headers.has("MACHINE-PAYMENT-RESPONSE") ? "MACHINE-PAYMENT-RESPONSE" : void 0,
|
|
1992
2163
|
protocolReceiptHeader: retryResponse.headers.get("Payment-Receipt") ?? retryResponse.headers.get("MACHINE-PAYMENT-RESPONSE") ?? void 0
|
|
1993
2164
|
});
|
|
2165
|
+
await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
|
|
1994
2166
|
return retryResponse;
|
|
1995
2167
|
}
|
|
1996
2168
|
assertCanResumeX402(status, paymentRequired, option) {
|
|
@@ -2820,7 +2992,11 @@ var HavenClient = class {
|
|
|
2820
2992
|
});
|
|
2821
2993
|
const data = await res.json();
|
|
2822
2994
|
if (!res.ok) {
|
|
2823
|
-
const
|
|
2995
|
+
const record = data;
|
|
2996
|
+
const errorText = typeof record.error === "string" ? record.error : void 0;
|
|
2997
|
+
const rawDetails = record.details ?? record.detail;
|
|
2998
|
+
const detailsText = typeof rawDetails === "string" ? rawDetails : rawDetails != null ? JSON.stringify(rawDetails) : void 0;
|
|
2999
|
+
const message = errorText && detailsText ? `${errorText}: ${detailsText}` : errorText ?? detailsText ?? `API request failed`;
|
|
2824
3000
|
throw new HavenApiError(message, res.status, data);
|
|
2825
3001
|
}
|
|
2826
3002
|
return data;
|
|
@@ -2848,6 +3024,12 @@ var HavenClient = class {
|
|
|
2848
3024
|
txHash: raw.tx_hash,
|
|
2849
3025
|
errorMessage: raw.error_message,
|
|
2850
3026
|
explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : null),
|
|
3027
|
+
fee: raw.fee ? {
|
|
3028
|
+
amount: raw.fee.amount,
|
|
3029
|
+
token: raw.fee.token,
|
|
3030
|
+
basisPoints: raw.fee.basis_points,
|
|
3031
|
+
applied: raw.fee.applied
|
|
3032
|
+
} : null,
|
|
2851
3033
|
createdAt: raw.created_at,
|
|
2852
3034
|
signedAt: raw.signed_at,
|
|
2853
3035
|
submittedAt: raw.submitted_at,
|
|
@@ -2871,6 +3053,12 @@ var HavenClient = class {
|
|
|
2871
3053
|
expiresAt: raw.expires_at,
|
|
2872
3054
|
chainId: raw.chain_id,
|
|
2873
3055
|
message: raw.message,
|
|
3056
|
+
fee: raw.fee ? {
|
|
3057
|
+
amount: raw.fee.amount,
|
|
3058
|
+
token: raw.fee.token,
|
|
3059
|
+
basisPoints: raw.fee.basis_points,
|
|
3060
|
+
applied: raw.fee.applied
|
|
3061
|
+
} : null,
|
|
2874
3062
|
amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? null,
|
|
2875
3063
|
asset: raw.asset ?? raw.x402?.asset ?? null,
|
|
2876
3064
|
network: raw.network ?? raw.x402?.network ?? null,
|
|
@@ -3016,13 +3204,13 @@ var toolDescriptions = {
|
|
|
3016
3204
|
getAgent: {
|
|
3017
3205
|
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.",
|
|
3018
3206
|
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.",
|
|
3019
|
-
behavior: 'Reads identity plus the on-chain AllowanceModule
|
|
3207
|
+
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.',
|
|
3020
3208
|
nextActionGuidance: ""
|
|
3021
3209
|
},
|
|
3022
3210
|
getAllowances: {
|
|
3023
3211
|
summary: "Return configured and on-chain allowance state for the authenticated agent. On-chain allowance is the real spend gate.",
|
|
3024
3212
|
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.",
|
|
3025
|
-
behavior: "
|
|
3213
|
+
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.",
|
|
3026
3214
|
nextActionGuidance: ""
|
|
3027
3215
|
},
|
|
3028
3216
|
listReceipts: {
|
|
@@ -3031,6 +3219,12 @@ var toolDescriptions = {
|
|
|
3031
3219
|
behavior: "Returns the agent's recent machine-payment receipts ordered by recency. Proof header values are not returned.",
|
|
3032
3220
|
nextActionGuidance: ""
|
|
3033
3221
|
},
|
|
3222
|
+
verifyReceipt: {
|
|
3223
|
+
summary: "Verify a payment receipt offline \u2014 confirm the agent authorised the transfer.",
|
|
3224
|
+
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.",
|
|
3225
|
+
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.",
|
|
3226
|
+
nextActionGuidance: ""
|
|
3227
|
+
},
|
|
3034
3228
|
payMcpTool: {
|
|
3035
3229
|
summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip.",
|
|
3036
3230
|
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.",
|
|
@@ -3418,20 +3612,72 @@ for that credential.
|
|
|
3418
3612
|
`;
|
|
3419
3613
|
var SKILL_FOLDER_NAME = "haven-pay";
|
|
3420
3614
|
|
|
3615
|
+
// src/node-version.ts
|
|
3616
|
+
var HAVEN_MINIMUM_NODE_VERSION = "24.0.0";
|
|
3617
|
+
function parseNodeVersion(value) {
|
|
3618
|
+
const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
3619
|
+
if (!match) return [0, 0, 0];
|
|
3620
|
+
return [Number(match[1] ?? 0), Number(match[2] ?? 0), Number(match[3] ?? 0)];
|
|
3621
|
+
}
|
|
3622
|
+
function compareNodeVersions(left, right) {
|
|
3623
|
+
const leftParts = parseNodeVersion(left);
|
|
3624
|
+
const rightParts = parseNodeVersion(right);
|
|
3625
|
+
for (let i = 0; i < 3; i += 1) {
|
|
3626
|
+
if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
|
|
3627
|
+
}
|
|
3628
|
+
return 0;
|
|
3629
|
+
}
|
|
3630
|
+
function isSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = HAVEN_MINIMUM_NODE_VERSION) {
|
|
3631
|
+
return compareNodeVersions(nodeVersion, minimumNodeVersion) >= 0;
|
|
3632
|
+
}
|
|
3633
|
+
function unsupportedNodeVersionMessage(options) {
|
|
3634
|
+
const nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
3635
|
+
const minimum = options.minimumNodeVersion ?? HAVEN_MINIMUM_NODE_VERSION;
|
|
3636
|
+
const lines = [
|
|
3637
|
+
`${options.subject} requires Node.js >=${minimum}, but this is Node.js ${nodeVersion}.`,
|
|
3638
|
+
"",
|
|
3639
|
+
"Upgrade Node, then try again:",
|
|
3640
|
+
` nvm install ${major(minimum)} && nvm use ${major(minimum)} (nvm)`,
|
|
3641
|
+
` fnm install ${major(minimum)} && fnm use ${major(minimum)} (fnm)`,
|
|
3642
|
+
` volta install node@${major(minimum)} (volta)`,
|
|
3643
|
+
` or download Node ${major(minimum)} from https://nodejs.org`,
|
|
3644
|
+
"",
|
|
3645
|
+
"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."
|
|
3646
|
+
];
|
|
3647
|
+
if (options.retryHint) lines.push("", options.retryHint);
|
|
3648
|
+
return lines.join("\n");
|
|
3649
|
+
}
|
|
3650
|
+
function major(version) {
|
|
3651
|
+
return String(parseNodeVersion(version)[0]);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3421
3654
|
// src/sweep.ts
|
|
3422
3655
|
var SWEEP_BASE_CHAIN_ID = 8453;
|
|
3656
|
+
var SWEEP_BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
3423
3657
|
var SWEEP_BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
3658
|
+
var SWEEP_BASE_SEPOLIA_USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
3424
3659
|
var USDC_EIP712_DOMAIN_BY_CHAIN = {
|
|
3425
3660
|
[SWEEP_BASE_CHAIN_ID]: {
|
|
3426
3661
|
name: "USD Coin",
|
|
3427
3662
|
version: "2",
|
|
3428
3663
|
chainId: SWEEP_BASE_CHAIN_ID,
|
|
3429
3664
|
verifyingContract: SWEEP_BASE_USDC_ADDRESS
|
|
3665
|
+
},
|
|
3666
|
+
[SWEEP_BASE_SEPOLIA_CHAIN_ID]: {
|
|
3667
|
+
name: "USDC",
|
|
3668
|
+
version: "2",
|
|
3669
|
+
chainId: SWEEP_BASE_SEPOLIA_CHAIN_ID,
|
|
3670
|
+
verifyingContract: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
|
|
3430
3671
|
}
|
|
3431
3672
|
};
|
|
3432
3673
|
var USDC_ADDRESS_BY_CHAIN = {
|
|
3433
|
-
[SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS
|
|
3674
|
+
[SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS,
|
|
3675
|
+
[SWEEP_BASE_SEPOLIA_CHAIN_ID]: SWEEP_BASE_SEPOLIA_USDC_ADDRESS
|
|
3434
3676
|
};
|
|
3677
|
+
var SWEEPABLE_CHAIN_IDS = Object.keys(USDC_ADDRESS_BY_CHAIN).map(Number);
|
|
3678
|
+
function isSweepableChain(chainId) {
|
|
3679
|
+
return chainId in USDC_ADDRESS_BY_CHAIN;
|
|
3680
|
+
}
|
|
3435
3681
|
var TRANSFER_WITH_AUTHORIZATION_TYPES = {
|
|
3436
3682
|
TransferWithAuthorization: [
|
|
3437
3683
|
{ name: "from", type: "address" },
|
|
@@ -3446,7 +3692,7 @@ function sweepUsdcAddress(chainId) {
|
|
|
3446
3692
|
const address = USDC_ADDRESS_BY_CHAIN[chainId];
|
|
3447
3693
|
if (!address) {
|
|
3448
3694
|
throw new HavenSigningError(
|
|
3449
|
-
`Sweep is
|
|
3695
|
+
`Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
|
|
3450
3696
|
);
|
|
3451
3697
|
}
|
|
3452
3698
|
return address;
|
|
@@ -3455,7 +3701,7 @@ function sweepUsdcDomain(chainId) {
|
|
|
3455
3701
|
const domain = USDC_EIP712_DOMAIN_BY_CHAIN[chainId];
|
|
3456
3702
|
if (!domain) {
|
|
3457
3703
|
throw new HavenSigningError(
|
|
3458
|
-
`Sweep is
|
|
3704
|
+
`Sweep is not supported on chain ${chainId}. Supported: ${SWEEPABLE_CHAIN_IDS.join(", ")}.`
|
|
3459
3705
|
);
|
|
3460
3706
|
}
|
|
3461
3707
|
return domain;
|
|
@@ -3510,6 +3756,6 @@ function stableStringify2(value) {
|
|
|
3510
3756
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
|
|
3511
3757
|
}
|
|
3512
3758
|
|
|
3513
|
-
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, HAVEN_SKILL_MD, HavenApiError, HavenClient, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, SKILL_FOLDER_NAME, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, addressFromKey, buildMachinePaymentIdempotencyKey, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, composeDescription, decodeBase64Json, decodeBase64Utf8, encodeBase64Json, encodeBase64Utf8, encodeMachinePaymentProof, encodePaymentProof, havenTools, parseMachinePaymentChallenge, parseMachinePaymentChallengeResponse, parsePaymentRequired, parsePaymentRequiredResponse, selectPaymentOption, selectStandardPaymentOption, signHash, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, toolDescriptions, verifySignature, x402AuthorizationAmount };
|
|
3759
|
+
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, HAVEN_MINIMUM_NODE_VERSION, HAVEN_SKILL_MD, HavenApiError, HavenClient, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, RECEIPT_VERSION, SKILL_FOLDER_NAME, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, X402_MAX_AUTHORIZATION_WINDOW_SECONDS, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, addressFromKey, buildMachinePaymentIdempotencyKey, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, composeDescription, decodeBase64Json, decodeBase64Utf8, encodeBase64Json, encodeBase64Utf8, encodeMachinePaymentProof, encodePaymentProof, havenTools, isSupportedNodeVersion, isSweepableChain, parseMachinePaymentChallenge, parseMachinePaymentChallengeResponse, parsePaymentRequired, parsePaymentRequiredResponse, selectPaymentOption, selectStandardPaymentOption, signHash, signUserOpTypedDataForDelegation, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, toolDescriptions, unsupportedNodeVersionMessage, verifyPaymentReceipt, verifySignature, x402AuthorizationAmount };
|
|
3514
3760
|
//# sourceMappingURL=index.js.map
|
|
3515
3761
|
//# sourceMappingURL=index.js.map
|