@haven_ai/signer 0.3.0-alpha.0 → 0.5.0-alpha.1
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 +204 -38
- package/dist/cli.cjs +1477 -1130
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1385 -1038
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +778 -448
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -35
- package/dist/index.d.ts +337 -35
- package/dist/index.js +744 -423
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -3,150 +3,22 @@
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
4
|
var viem = require('viem');
|
|
5
5
|
var accounts = require('viem/accounts');
|
|
6
|
-
var
|
|
7
|
-
var sdk = require('@haven_ai/sdk');
|
|
6
|
+
var edge = require('@haven_ai/sdk/edge');
|
|
8
7
|
var promises = require('fs/promises');
|
|
8
|
+
var path = require('path');
|
|
9
|
+
var zod = require('zod');
|
|
9
10
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
10
11
|
var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
11
12
|
var os = require('os');
|
|
12
|
-
var path = require('path');
|
|
13
13
|
var v3 = require('zod/v3');
|
|
14
14
|
|
|
15
15
|
// src/core.ts
|
|
16
|
-
var DELEGATION_MANAGER = "0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3";
|
|
17
|
-
var CAVEAT_ENFORCERS = {
|
|
18
|
-
erc20TransferAmount: "0xf100b0819427117EcF76Ed94B358B1A5b5C6D2Fc",
|
|
19
|
-
allowedCalldata: "0xc2b0d624c1c4319760C96503BA27C347F3260f55",
|
|
20
|
-
timestamp: "0x1046bb45C8d673d4ea75321280DB34899413c069"};
|
|
21
|
-
var MAX_SETTLEMENT_WINDOW_SECONDS = 600;
|
|
22
|
-
function isSettlementChildTypedData(value) {
|
|
23
|
-
const td = value;
|
|
24
|
-
return !!td && td.primaryType === "Delegation" && typeof td.domain?.verifyingContract === "string" && Array.isArray(td.message?.caveats);
|
|
25
|
-
}
|
|
26
|
-
function same(a, b) {
|
|
27
|
-
return !!a && !!b && a.toLowerCase() === b.toLowerCase();
|
|
28
|
-
}
|
|
29
|
-
function hex(value) {
|
|
30
|
-
return value.startsWith("0x") ? value.slice(2).toLowerCase() : value.toLowerCase();
|
|
31
|
-
}
|
|
32
|
-
function slice(terms, start, end) {
|
|
33
|
-
const body = hex(terms);
|
|
34
|
-
return end === void 0 ? body.slice(start * 2) : body.slice(start * 2, end * 2);
|
|
35
|
-
}
|
|
36
|
-
function findCaveat(caveats, enforcer) {
|
|
37
|
-
return caveats.find((c) => same(c.enforcer, enforcer));
|
|
38
|
-
}
|
|
39
|
-
function refuse(what, detail) {
|
|
40
|
-
throw new sdk.HavenSigningError(
|
|
41
|
-
`Refusing to sign the x402 settlement child: ${what}. ${detail} The signature on this child is what lets a merchant pull from the treasury, so it is verified locally against the Haven-signed expected context rather than trusted.`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
function chainIdForNetwork(network) {
|
|
45
|
-
if (!network) return void 0;
|
|
46
|
-
const caip = /^eip155:(\d+)$/.exec(network);
|
|
47
|
-
if (caip) return Number(caip[1]);
|
|
48
|
-
if (network === "base") return 8453;
|
|
49
|
-
if (network === "base-sepolia") return 84532;
|
|
50
|
-
return void 0;
|
|
51
|
-
}
|
|
52
|
-
function verifySettlementChild(typedData, expected, now = Date.now()) {
|
|
53
|
-
if (typedData.primaryType !== "Delegation") {
|
|
54
|
-
refuse("it is not a Delegation payload", `primaryType was '${typedData.primaryType}'.`);
|
|
55
|
-
}
|
|
56
|
-
if (!same(typedData.domain?.verifyingContract, DELEGATION_MANAGER)) {
|
|
57
|
-
refuse(
|
|
58
|
-
"the EIP-712 domain names an unknown DelegationManager",
|
|
59
|
-
`Expected ${DELEGATION_MANAGER}, got ${typedData.domain?.verifyingContract}.`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
const domainChain = Number(typedData.domain?.chainId);
|
|
63
|
-
if (!Number.isFinite(domainChain) || domainChain !== expected.chainId) {
|
|
64
|
-
refuse(
|
|
65
|
-
"it is scoped to the wrong chain",
|
|
66
|
-
`The expected context says chain ${expected.chainId}; the child says ${typedData.domain?.chainId}.`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
const caveats = typedData.message?.caveats ?? [];
|
|
70
|
-
if (caveats.length === 0) {
|
|
71
|
-
refuse("it carries no caveats at all", "An unconstrained delegation is not a payment.");
|
|
72
|
-
}
|
|
73
|
-
const transfer = findCaveat(caveats, CAVEAT_ENFORCERS.erc20TransferAmount);
|
|
74
|
-
if (!transfer) {
|
|
75
|
-
refuse(
|
|
76
|
-
"it has no ERC-20 transfer-amount caveat",
|
|
77
|
-
"Without it the delegation is not bounded to an amount."
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
const token = `0x${slice(transfer.terms, 0, 20)}`;
|
|
81
|
-
if (!same(token, expected.asset)) {
|
|
82
|
-
refuse("it spends a different token", `Expected ${expected.asset}, child pins ${token}.`);
|
|
83
|
-
}
|
|
84
|
-
const amount = BigInt(`0x${slice(transfer.terms, 20, 52)}`);
|
|
85
|
-
if (amount !== BigInt(expected.amount)) {
|
|
86
|
-
refuse(
|
|
87
|
-
"the amount does not match",
|
|
88
|
-
`The expected context says ${expected.amount}; the child allows ${amount.toString()}.`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
const calldata = findCaveat(caveats, CAVEAT_ENFORCERS.allowedCalldata);
|
|
92
|
-
if (!calldata) {
|
|
93
|
-
refuse(
|
|
94
|
-
"it has no payee pin",
|
|
95
|
-
"Without an allowed-calldata caveat the merchant could redeem it to any address."
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
const startIndex = BigInt(`0x${slice(calldata.terms, 0, 32)}`);
|
|
99
|
-
if (startIndex !== 4n) {
|
|
100
|
-
refuse(
|
|
101
|
-
"the payee pin points at the wrong calldata offset",
|
|
102
|
-
`Expected offset 4 (the transfer's \`to\` word), got ${startIndex.toString()}.`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
const paddedPayee = slice(calldata.terms, 32, 64);
|
|
106
|
-
if (paddedPayee.slice(0, 24) !== "0".repeat(24)) {
|
|
107
|
-
refuse("the pinned payee is not a plain address", "Its 32-byte word is not a padded address.");
|
|
108
|
-
}
|
|
109
|
-
const payee = `0x${paddedPayee.slice(24)}`;
|
|
110
|
-
if (!same(payee, expected.merchantTo)) {
|
|
111
|
-
refuse(
|
|
112
|
-
"it pays a different address than Haven declared",
|
|
113
|
-
`The expected context says ${expected.merchantTo}; the child pins ${payee}.`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
const timestamp = findCaveat(caveats, CAVEAT_ENFORCERS.timestamp);
|
|
117
|
-
if (!timestamp) {
|
|
118
|
-
refuse("it never expires", "A settlement child without a timestamp caveat is open-ended.");
|
|
119
|
-
}
|
|
120
|
-
const beforeThreshold = Number(BigInt(`0x${slice(timestamp.terms, 16, 32)}`));
|
|
121
|
-
if (beforeThreshold <= 0) {
|
|
122
|
-
refuse("its expiry is unset", "The timestamp caveat has no upper bound.");
|
|
123
|
-
}
|
|
124
|
-
const nowSec = Math.floor(now / 1e3);
|
|
125
|
-
if (beforeThreshold <= nowSec) {
|
|
126
|
-
refuse("it has already expired", `Expiry ${beforeThreshold} is not in the future.`);
|
|
127
|
-
}
|
|
128
|
-
if (beforeThreshold > nowSec + MAX_SETTLEMENT_WINDOW_SECONDS) {
|
|
129
|
-
refuse(
|
|
130
|
-
"its window is longer than a settlement may live",
|
|
131
|
-
`Expiry is ${beforeThreshold - nowSec}s out; the ceiling is ${MAX_SETTLEMENT_WINDOW_SECONDS}s.`
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if (expected.expiresAt) {
|
|
135
|
-
const declared = Math.floor(Date.parse(expected.expiresAt) / 1e3);
|
|
136
|
-
if (Number.isFinite(declared) && beforeThreshold > declared) {
|
|
137
|
-
refuse(
|
|
138
|
-
"it outlives the payment window Haven declared",
|
|
139
|
-
`The expected context expires at ${expected.expiresAt}; the child lives to ${beforeThreshold}.`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
16
|
function createEdgeSigner(delegateKey, options = {}) {
|
|
145
17
|
let delegateAddress;
|
|
146
18
|
try {
|
|
147
|
-
delegateAddress =
|
|
19
|
+
delegateAddress = edge.addressFromKey(delegateKey);
|
|
148
20
|
} catch (err) {
|
|
149
|
-
throw new
|
|
21
|
+
throw new edge.HavenSigningError(
|
|
150
22
|
`Invalid delegate key: ${err instanceof Error ? err.message : String(err)}`
|
|
151
23
|
);
|
|
152
24
|
}
|
|
@@ -160,49 +32,36 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
160
32
|
if (!oldest.done) retiredX402Bindings.delete(oldest.value);
|
|
161
33
|
}
|
|
162
34
|
}
|
|
163
|
-
function signAndVerify(hash) {
|
|
164
|
-
const signature = sdk.signHash(delegateKey, hash);
|
|
165
|
-
if (!sdk.verifySignature(hash, signature, delegateAddress)) {
|
|
166
|
-
throw new sdk.HavenSigningError(
|
|
167
|
-
"Local signature verification failed \u2014 recovered address does not match the delegate key."
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
return signature;
|
|
171
|
-
}
|
|
172
35
|
return {
|
|
173
36
|
delegateAddress,
|
|
174
|
-
signPaymentHash(hash) {
|
|
175
|
-
return signAndVerify(hash);
|
|
176
|
-
},
|
|
177
37
|
async signDelegationTypedData(typedData) {
|
|
178
38
|
const account = accounts.privateKeyToAccount(delegateKey);
|
|
179
39
|
return account.signTypedData(typedData);
|
|
180
40
|
},
|
|
181
|
-
signX402FundingHash(hash, expected) {
|
|
182
|
-
assertExpectedBinding(hash, expected, options.x402BindingSigner, "hash");
|
|
183
|
-
assertPayerMatchesDelegate(expected, delegateAddress, options.agentId);
|
|
184
|
-
const signature = signAndVerify(hash);
|
|
185
|
-
const x402Binding = crypto.randomUUID();
|
|
186
|
-
x402Bindings.set(x402Binding, { ...expected });
|
|
187
|
-
return { signature, x402Binding };
|
|
188
|
-
},
|
|
189
41
|
async signX402FundingTypedData(typedData, expected) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
42
|
+
if (expected?.auth) {
|
|
43
|
+
assertSupportedBindingVersion(
|
|
44
|
+
expected.auth.version,
|
|
45
|
+
SUPPORTED_X402_EXPECTED_VERSIONS,
|
|
46
|
+
"x402 expected context"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (!typedData) {
|
|
50
|
+
throw new edge.HavenSigningError(
|
|
51
|
+
"This x402 funding intent is on the delegation rail: it commits to EIP-712 typed data, which was not supplied. Pass typed_data_b64 from haven_pay_mcp_tool / haven_pay_x402_quote through unchanged (or typed_data verbatim) \u2014 a bare payload_hash is not what the account validates."
|
|
196
52
|
);
|
|
197
53
|
}
|
|
198
|
-
|
|
199
|
-
|
|
54
|
+
const digest = viem.hashTypedData(typedData);
|
|
55
|
+
assertExpectedBinding(digest, expected, options.x402BindingSigner);
|
|
56
|
+
assertPayerMatchesDelegate(expected, delegateAddress, options.agentId);
|
|
57
|
+
if (edge.isSettlementChildTypedData(typedData)) {
|
|
58
|
+
const settlementChainId = edge.chainIdForNetwork(expected.network);
|
|
200
59
|
if (settlementChainId === void 0) {
|
|
201
|
-
throw new
|
|
60
|
+
throw new edge.HavenSigningError(
|
|
202
61
|
`Refusing to sign the x402 settlement child: this signer cannot map the network '${expected.network}' to a chain id, so it cannot check which chain the child is scoped to. Update @haven_ai/signer.`
|
|
203
62
|
);
|
|
204
63
|
}
|
|
205
|
-
|
|
64
|
+
edge.assertOwnSettlementChild(typedData, {
|
|
206
65
|
merchantTo: expected.merchantTo,
|
|
207
66
|
amount: expected.amount,
|
|
208
67
|
asset: expected.asset,
|
|
@@ -212,7 +71,21 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
212
71
|
// signed another" class this file exists to catch (#1455 review).
|
|
213
72
|
chainId: settlementChainId,
|
|
214
73
|
expiresAt: expected.expiresAt
|
|
74
|
+
}, delegateAddress);
|
|
75
|
+
} else if (edge.isPackedUserOperationTypedData(typedData)) {
|
|
76
|
+
edge.assertUserOpTypedDataBinding(typedData, expected.payloadHash);
|
|
77
|
+
edge.assertBoundDirectPaymentUserOp(typedData, delegateAddress);
|
|
78
|
+
edge.assertFundingLegPaysDelegate(typedData, {
|
|
79
|
+
delegateAddress,
|
|
80
|
+
asset: expected.asset,
|
|
81
|
+
amount: expected.amount
|
|
215
82
|
});
|
|
83
|
+
} else {
|
|
84
|
+
throw new edge.HavenTypedDataRefusedError(
|
|
85
|
+
`This signer's x402 arm signs only a funding-leg PackedUserOperation or an erc7710 settlement Delegation; this typed data is '${String(
|
|
86
|
+
typedData.primaryType
|
|
87
|
+
)}'. Refusing, even though its digest is bound by the expected context.`
|
|
88
|
+
);
|
|
216
89
|
}
|
|
217
90
|
const account = accounts.privateKeyToAccount(delegateKey);
|
|
218
91
|
const signature = await account.signTypedData(
|
|
@@ -227,37 +100,42 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
227
100
|
if (!expected) {
|
|
228
101
|
const retired = retiredX402Bindings.get(x402Binding);
|
|
229
102
|
if (retired === "header_built") {
|
|
230
|
-
throw new
|
|
103
|
+
throw new edge.HavenSigningError(
|
|
231
104
|
"This x402 binding was already used to build a merchant header. Bindings are single-use. If you called haven_sign_x402, it already returned the payment_header \u2014 retry the merchant with THAT header instead of building another; haven_x402_sign_header is the follow-up to haven_sign, not to haven_sign_x402. If you no longer have the header, re-run the quote tool with the same idempotency_key."
|
|
232
105
|
);
|
|
233
106
|
}
|
|
234
107
|
if (retired === "window_expired") {
|
|
235
|
-
throw new
|
|
108
|
+
throw new edge.HavenSigningError(
|
|
236
109
|
"This x402 binding was retired because its payment window closed before a merchant header could be built \u2014 no header exists to retry with. Re-run the quote tool with the same idempotency_key to get a fresh window, then sign again."
|
|
237
110
|
);
|
|
238
111
|
}
|
|
239
|
-
throw new
|
|
240
|
-
"x402 funding binding is required before signing a merchant header. Sign the hosted funding
|
|
112
|
+
throw new edge.HavenSigningError(
|
|
113
|
+
"x402 funding binding is required before signing a merchant header. Sign the hosted funding payload with x402_expected first (haven_sign returns a binding this tool can use). A binding is also lost when the signer process restarts, since bindings live in memory only \u2014 re-sign to mint a fresh one."
|
|
241
114
|
);
|
|
242
115
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
116
|
+
const assertWindowOpenOrRetire = () => {
|
|
117
|
+
try {
|
|
118
|
+
assertX402PaymentWindowOpen(expected);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
x402Bindings.delete(x402Binding);
|
|
121
|
+
retireX402Binding(x402Binding, "window_expired");
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
assertWindowOpenOrRetire();
|
|
126
|
+
const option = edge.selectStandardPaymentOption(paymentRequired.accepts);
|
|
251
127
|
if (!option) {
|
|
252
|
-
throw new
|
|
128
|
+
throw new edge.HavenApiError(
|
|
253
129
|
"No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
|
|
254
130
|
400
|
|
255
131
|
);
|
|
256
132
|
}
|
|
257
133
|
assertX402MatchesExpected(paymentRequired, option, expected);
|
|
258
134
|
const account = accounts.privateKeyToAccount(delegateKey);
|
|
259
|
-
const requirements =
|
|
260
|
-
const
|
|
135
|
+
const requirements = edge.toStandardPaymentRequirements(paymentRequired, option);
|
|
136
|
+
const { exact } = await loadX402Schemes();
|
|
137
|
+
assertWindowOpenOrRetire();
|
|
138
|
+
const header = await exact.evm.createPaymentHeader(
|
|
261
139
|
account,
|
|
262
140
|
paymentRequired.x402Version,
|
|
263
141
|
requirements
|
|
@@ -268,9 +146,9 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
268
146
|
return { paymentHeader: header, accepted: option };
|
|
269
147
|
}
|
|
270
148
|
try {
|
|
271
|
-
const payment =
|
|
272
|
-
const wrapped =
|
|
273
|
-
|
|
149
|
+
const payment = edge.decodeBase64Json(header);
|
|
150
|
+
const wrapped = edge.encodeBase64Json(
|
|
151
|
+
edge.x402V2PaymentEnvelope(paymentRequired, option, payment.payload)
|
|
274
152
|
);
|
|
275
153
|
return { paymentHeader: wrapped, accepted: option };
|
|
276
154
|
} finally {
|
|
@@ -285,16 +163,16 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
285
163
|
}) {
|
|
286
164
|
assertSweepBinding(authorization, expectedAuth, options.x402BindingSigner);
|
|
287
165
|
if (!sameAddress(authorization.from, delegateAddress)) {
|
|
288
|
-
throw new
|
|
166
|
+
throw new edge.HavenSigningError(
|
|
289
167
|
"Sweep authorization `from` does not match this delegate address."
|
|
290
168
|
);
|
|
291
169
|
}
|
|
292
170
|
if (expectedSafe && !sameAddress(authorization.to, expectedSafe)) {
|
|
293
|
-
throw new
|
|
294
|
-
"Sweep authorization `to` does not match the
|
|
171
|
+
throw new edge.HavenSigningError(
|
|
172
|
+
"Sweep authorization `to` does not match the agent's account (Haven wallet) in the local credential."
|
|
295
173
|
);
|
|
296
174
|
}
|
|
297
|
-
const typedData =
|
|
175
|
+
const typedData = edge.buildSweepTypedData(authorization);
|
|
298
176
|
const viemTypedData = {
|
|
299
177
|
domain: {
|
|
300
178
|
...typedData.domain,
|
|
@@ -313,7 +191,7 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
313
191
|
const signature = await account.signTypedData(viemTypedData);
|
|
314
192
|
const recovered = await viem.recoverTypedDataAddress({ ...viemTypedData, signature });
|
|
315
193
|
if (!sameAddress(recovered, delegateAddress)) {
|
|
316
|
-
throw new
|
|
194
|
+
throw new edge.HavenSigningError(
|
|
317
195
|
"Local sweep signature verification failed \u2014 recovered address does not match the delegate key."
|
|
318
196
|
);
|
|
319
197
|
}
|
|
@@ -323,10 +201,10 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
323
201
|
}
|
|
324
202
|
function assertSweepBinding(authorization, expectedAuth, trustedSigner) {
|
|
325
203
|
if (!expectedAuth || typeof expectedAuth !== "object") {
|
|
326
|
-
throw new
|
|
204
|
+
throw new edge.HavenSigningError("Sweep authorization binding is required before signing.");
|
|
327
205
|
}
|
|
328
206
|
if (!trustedSigner) {
|
|
329
|
-
throw new
|
|
207
|
+
throw new edge.HavenSigningError(
|
|
330
208
|
"Sweep binding verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing sweep authorizations."
|
|
331
209
|
);
|
|
332
210
|
}
|
|
@@ -335,51 +213,51 @@ function assertSweepBinding(authorization, expectedAuth, trustedSigner) {
|
|
|
335
213
|
SUPPORTED_SWEEP_BINDING_VERSIONS,
|
|
336
214
|
"sweep authorization binding"
|
|
337
215
|
);
|
|
338
|
-
const message =
|
|
216
|
+
const message = edge.buildSweepAuthorizationMessage(authorization);
|
|
339
217
|
if (expectedAuth.message !== message) {
|
|
340
|
-
throw new
|
|
218
|
+
throw new edge.HavenSigningError("Sweep authorization binding does not match the authorization being signed.");
|
|
341
219
|
}
|
|
342
220
|
if (!sameAddress(expectedAuth.signer, trustedSigner)) {
|
|
343
|
-
throw new
|
|
221
|
+
throw new edge.HavenSigningError("Sweep authorization binding was not signed by the configured Haven signer.");
|
|
344
222
|
}
|
|
345
|
-
if (!
|
|
346
|
-
throw new
|
|
223
|
+
if (!edge.verifySignature(viem.hashMessage(message), expectedAuth.signature, trustedSigner)) {
|
|
224
|
+
throw new edge.HavenSigningError("Sweep authorization binding signature could not be verified.");
|
|
347
225
|
}
|
|
348
226
|
}
|
|
349
227
|
function assertX402MatchesExpected(paymentRequired, option, expected) {
|
|
350
228
|
assertExpectedShape(expected);
|
|
351
229
|
const headerResource = option.resource ?? paymentRequired.resource.url;
|
|
352
230
|
if (headerResource !== expected.resourceUrl) {
|
|
353
|
-
throw new
|
|
231
|
+
throw new edge.HavenSigningError("x402 payment_required resource does not match the funded intent.");
|
|
354
232
|
}
|
|
355
233
|
if (!sameAddress(option.payTo, expected.merchantTo)) {
|
|
356
|
-
throw new
|
|
234
|
+
throw new edge.HavenSigningError("x402 merchant recipient does not match the funded intent.");
|
|
357
235
|
}
|
|
358
|
-
if (
|
|
359
|
-
throw new
|
|
236
|
+
if (edge.x402AuthorizationAmount(option) !== expected.amount) {
|
|
237
|
+
throw new edge.HavenSigningError("x402 amount does not match the funded intent.");
|
|
360
238
|
}
|
|
361
239
|
if (!sameAddress(option.asset, expected.asset)) {
|
|
362
|
-
throw new
|
|
240
|
+
throw new edge.HavenSigningError("x402 asset does not match the funded intent.");
|
|
363
241
|
}
|
|
364
242
|
if (option.network !== expected.network) {
|
|
365
|
-
throw new
|
|
243
|
+
throw new edge.HavenSigningError("x402 network does not match the funded intent.");
|
|
366
244
|
}
|
|
367
245
|
}
|
|
368
246
|
function assertExpectedShape(expected) {
|
|
369
247
|
if (!expected || typeof expected !== "object") {
|
|
370
|
-
throw new
|
|
248
|
+
throw new edge.HavenSigningError("x402 expected funding context is required before signing a merchant header.");
|
|
371
249
|
}
|
|
372
250
|
}
|
|
373
|
-
var SUPPORTED_X402_EXPECTED_VERSIONS = [
|
|
251
|
+
var SUPPORTED_X402_EXPECTED_VERSIONS = [2, 3];
|
|
374
252
|
var SUPPORTED_SWEEP_BINDING_VERSIONS = [1];
|
|
375
253
|
function assertSupportedBindingVersion(received, supported, context) {
|
|
376
254
|
if (supported.includes(received)) return;
|
|
377
255
|
const highest = Math.max(...supported);
|
|
378
256
|
const outOfDate = received > highest;
|
|
379
|
-
const code = context === "x402 expected context" ?
|
|
380
|
-
const ceiling = outOfDate ? `This signer is out of date: it supports ${context} versions up to ${highest}, and Haven sent version ${received}. Update @haven_ai/signer \u2014 rerun the Haven connector (\`${
|
|
381
|
-
const fallback = outOfDate ?
|
|
382
|
-
throw new
|
|
257
|
+
const code = context === "x402 expected context" ? edge.SignerRefusalCode.UnsupportedExpectedContextVersion : edge.SignerRefusalCode.UnsupportedSweepBindingVersion;
|
|
258
|
+
const ceiling = outOfDate ? `This signer is out of date: it supports ${context} versions up to ${highest}, and Haven sent version ${received}. Update @haven_ai/signer \u2014 rerun the Haven connector (\`${edge.connectorRerunCommand()}\`), which reinstalls the pinned MCP runtime.` : `Unsupported ${context} version ${received}: this signer supports ${supported.join(", ")}.`;
|
|
259
|
+
const fallback = outOfDate ? edge.SIGNER_UPDATE_FALLBACK : `This ${context} version (${received}) is older than what this signer enforces (${supported.join(", ")}) \u2014 updating @haven_ai/signer will not restore it. Nothing was signed or spent; stop and tell the user rather than retrying.`;
|
|
260
|
+
throw new edge.HavenUnsupportedSignerVersionError(
|
|
383
261
|
`${ceiling} Nothing was signed. Do not rewrite the version field to a supported value: it is part of the Haven-signed binding message, so changing it invalidates the signature and would misrepresent what Haven declared.`,
|
|
384
262
|
code,
|
|
385
263
|
supported,
|
|
@@ -387,11 +265,11 @@ function assertSupportedBindingVersion(received, supported, context) {
|
|
|
387
265
|
fallback
|
|
388
266
|
);
|
|
389
267
|
}
|
|
390
|
-
function assertExpectedBinding(
|
|
268
|
+
function assertExpectedBinding(typedDataDigest, expected, trustedSigner) {
|
|
391
269
|
assertExpectedShape(expected);
|
|
392
270
|
if (!trustedSigner) {
|
|
393
|
-
throw new
|
|
394
|
-
"x402 expected-context verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing x402 funding
|
|
271
|
+
throw new edge.HavenSigningError(
|
|
272
|
+
"x402 expected-context verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing x402 funding payloads."
|
|
395
273
|
);
|
|
396
274
|
}
|
|
397
275
|
if (expected.auth) {
|
|
@@ -401,20 +279,17 @@ function assertExpectedBinding(payloadHash, expected, trustedSigner, mode = "has
|
|
|
401
279
|
"x402 expected context"
|
|
402
280
|
);
|
|
403
281
|
}
|
|
404
|
-
if (expected.
|
|
405
|
-
throw new
|
|
406
|
-
|
|
407
|
-
if (mode === "hash" && expected.typedDataHash) {
|
|
408
|
-
throw new sdk.HavenSigningError(
|
|
409
|
-
"This x402 funding intent commits to EIP-712 typed data, so its bare hash must not be raw-signed \u2014 the account would reject that signature on-chain. Sign sign_data.typed_data instead."
|
|
282
|
+
if (!expected.typedDataHash) {
|
|
283
|
+
throw new edge.HavenSigningError(
|
|
284
|
+
"Refusing to sign typed data under an expected context that does not commit to it. Haven must return a v2/v3 x402 expected context (with typedDataHash) for a delegation-rail intent."
|
|
410
285
|
);
|
|
411
286
|
}
|
|
412
|
-
if (
|
|
413
|
-
throw new
|
|
414
|
-
"Refusing to sign
|
|
287
|
+
if (expected.typedDataHash.toLowerCase() !== typedDataDigest.toLowerCase()) {
|
|
288
|
+
throw new edge.HavenSigningError(
|
|
289
|
+
"x402 typed data does not match the digest Haven committed to in the expected context. Refusing to sign \u2014 the payload was altered in transit or Haven declared a different one. The most common cause is the typed data being truncated or reshaped while being copied between tool calls (#1255): re-run the hosted quote and pass its typed_data_b64 string through UNCHANGED instead of re-emitting the nested JSON."
|
|
415
290
|
);
|
|
416
291
|
}
|
|
417
|
-
const message =
|
|
292
|
+
const message = edge.buildX402ExpectedMessage({
|
|
418
293
|
paymentId: expected.paymentId,
|
|
419
294
|
payloadHash: expected.payloadHash,
|
|
420
295
|
resourceUrl: expected.resourceUrl,
|
|
@@ -427,15 +302,15 @@ function assertExpectedBinding(payloadHash, expected, trustedSigner, mode = "has
|
|
|
427
302
|
payerDelegate: expected.payerDelegate,
|
|
428
303
|
payerAgentId: expected.payerAgentId
|
|
429
304
|
});
|
|
430
|
-
const expectedVersion = expected.payerDelegate ? 3 :
|
|
305
|
+
const expectedVersion = expected.payerDelegate ? 3 : 2;
|
|
431
306
|
if (expected.auth?.version !== expectedVersion || expected.auth.message !== message) {
|
|
432
|
-
throw new
|
|
307
|
+
throw new edge.HavenSigningError("x402 expected context authentication message is invalid.");
|
|
433
308
|
}
|
|
434
309
|
if (!sameAddress(expected.auth.signer, trustedSigner)) {
|
|
435
|
-
throw new
|
|
310
|
+
throw new edge.HavenSigningError("x402 expected context was not signed by the configured Haven signer.");
|
|
436
311
|
}
|
|
437
|
-
if (!
|
|
438
|
-
throw new
|
|
312
|
+
if (!edge.verifySignature(viem.hashMessage(message), expected.auth.signature, trustedSigner)) {
|
|
313
|
+
throw new edge.HavenSigningError("x402 expected context signature could not be verified.");
|
|
439
314
|
}
|
|
440
315
|
}
|
|
441
316
|
function assertPayerMatchesDelegate(expected, delegateAddress, localAgentId) {
|
|
@@ -443,20 +318,29 @@ function assertPayerMatchesDelegate(expected, delegateAddress, localAgentId) {
|
|
|
443
318
|
if (sameAddress(expected.payerDelegate, delegateAddress)) return;
|
|
444
319
|
const quoteAgent = expected.payerAgentId ?? "unknown";
|
|
445
320
|
const localAgent = localAgentId ?? "unknown";
|
|
446
|
-
throw new
|
|
321
|
+
throw new edge.HavenSigningError(
|
|
447
322
|
`This quote belongs to a DIFFERENT agent: the quote was created for agent ${quoteAgent} (delegate ${expected.payerDelegate}), but this signer holds agent ${localAgent} (delegate ${delegateAddress}). A long-lived host is holding stale credentials \u2014 its session authenticates as the old agent while the signer on disk belongs to the new one. Nothing was signed. Restart the host so it re-reads its wiring, then re-quote.`
|
|
448
323
|
);
|
|
449
324
|
}
|
|
325
|
+
async function loadX402Schemes() {
|
|
326
|
+
try {
|
|
327
|
+
return await import('x402/schemes');
|
|
328
|
+
} catch (err) {
|
|
329
|
+
throw new edge.HavenSigningError(
|
|
330
|
+
`The x402 scheme module could not be loaded, so no merchant header was built (nothing was sent to the merchant): ${err instanceof Error ? err.message : String(err)}. The signer's runtime install is incomplete \u2014 run: npx @haven_ai/connect --doctor`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
450
334
|
function assertX402PaymentWindowOpen(expected) {
|
|
451
335
|
if (!expected.expiresAt) return;
|
|
452
336
|
const expiresAtMs = Date.parse(expected.expiresAt);
|
|
453
337
|
if (Number.isNaN(expiresAtMs)) {
|
|
454
|
-
throw new
|
|
338
|
+
throw new edge.HavenSigningError("x402 expected context expiresAt is not a valid ISO timestamp.");
|
|
455
339
|
}
|
|
456
340
|
if (expiresAtMs <= Date.now()) {
|
|
457
|
-
throw new
|
|
341
|
+
throw new edge.HavenError(
|
|
458
342
|
"The x402 payment window expired before the merchant header could be signed. Re-quote with haven_pay_mcp_tool using the same idempotency_key before trying again.",
|
|
459
|
-
|
|
343
|
+
edge.AgentPaymentFailureCode.PaymentWindowExpired,
|
|
460
344
|
410,
|
|
461
345
|
expected.paymentId
|
|
462
346
|
);
|
|
@@ -465,11 +349,317 @@ function assertX402PaymentWindowOpen(expected) {
|
|
|
465
349
|
function sameAddress(a, b) {
|
|
466
350
|
return a.toLowerCase() === b.toLowerCase();
|
|
467
351
|
}
|
|
352
|
+
var SIGNER_HOSTED_HANDOFF_SHAPES = {
|
|
353
|
+
haven_get_payment_status: { payment_id: zod.z.string().min(1) }
|
|
354
|
+
};
|
|
355
|
+
function target(shape) {
|
|
356
|
+
const schema = zod.z.object(shape).strict();
|
|
357
|
+
return {
|
|
358
|
+
role: "hosted",
|
|
359
|
+
validate: (input) => {
|
|
360
|
+
const r = schema.safeParse(input ?? {});
|
|
361
|
+
return r.success ? null : r.error.errors.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join("; ");
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
var SIGNER_NEXT_STEP_TARGETS = {
|
|
366
|
+
haven_get_payment_status: target(SIGNER_HOSTED_HANDOFF_SHAPES.haven_get_payment_status)
|
|
367
|
+
};
|
|
368
|
+
var nextStep = edge.createNextStepBuilder(SIGNER_NEXT_STEP_TARGETS);
|
|
369
|
+
function signerRefusalStep(input) {
|
|
370
|
+
return nextStep({ ...input, safeToContinue: false, reason: "" });
|
|
371
|
+
}
|
|
372
|
+
function nextStepWireFields(step) {
|
|
373
|
+
return {
|
|
374
|
+
...step.next_tool ? { next_tool: step.next_tool } : {},
|
|
375
|
+
...step.next_tool_server ? { next_tool_server: step.next_tool_server } : {},
|
|
376
|
+
...step.next_tool_name ? { next_tool_name: step.next_tool_name } : {},
|
|
377
|
+
...step.next_tool_server_role ? { next_tool_server_role: step.next_tool_server_role } : {},
|
|
378
|
+
...step.next_arguments ? { next_arguments: step.next_arguments } : {},
|
|
379
|
+
...step.next_tool_omitted_reason ? { next_tool_omitted_reason: step.next_tool_omitted_reason } : {}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/sign-context.ts
|
|
384
|
+
var DIRECT_RELAY_FALLBACK = "call haven_sign with payload_hash and typed_data_b64 from the haven_send / haven_pay result, passed through unchanged";
|
|
385
|
+
var HavenSignContextError = class extends edge.HavenSigningError {
|
|
386
|
+
/**
|
|
387
|
+
* #3001 / #3010 review: the recovery path, when signing OTHER bytes is
|
|
388
|
+
* actually a remedy — a transport failure (timeout, unreachable) or a
|
|
389
|
+
* body this signer could not read. `typed_data_b64` is NOT in the default
|
|
390
|
+
* quote result since #1272: obtain it by re-running the SAME quote tool
|
|
391
|
+
* with the SAME idempotency_key plus `include_signing_payload: true`.
|
|
392
|
+
* Absent on a backend REFUSAL (an expired, executed or unsignable intent
|
|
393
|
+
* cannot be rescued by re-signing its bytes) — with one #3271 exception: a
|
|
394
|
+
* 404 from the DIRECT fetch (an older backend with no direct route), where
|
|
395
|
+
* the `haven_send` / `haven_pay` result's relay fields still work.
|
|
396
|
+
*/
|
|
397
|
+
fallback;
|
|
398
|
+
/**
|
|
399
|
+
* #3001: `AgentPaymentNextAction` values the signer already emits — the
|
|
400
|
+
* version-mismatch refusal's `stop_and_tell_user` for the classes where
|
|
401
|
+
* retrying the same call cannot help, and `payment_window_expired` (with
|
|
402
|
+
* `retry_with_new_quote`) for the x402 fetch's 410 / `expired`, exactly as
|
|
403
|
+
* the plain `HavenError` branch already does for that code. On the direct
|
|
404
|
+
* fetch (#3271) only `error_code: 'expired'` means an expired window, with
|
|
405
|
+
* no `retry_with_new_quote`; a bare 410 there is a retired-rail tombstone.
|
|
406
|
+
*/
|
|
407
|
+
next_action;
|
|
408
|
+
/**
|
|
409
|
+
* #3103 (epic #3105, decisions 1 and 3): the typed next step beside the
|
|
410
|
+
* action. A refusal Haven made (`SIGN_CONTEXT_REFUSED`, not expired) names
|
|
411
|
+
* the hosted status read with the payment id — except a 404 from the direct
|
|
412
|
+
* fetch (#3271), which names none and points at the `typed_data_b64`
|
|
413
|
+
* relay; a transport failure or a
|
|
414
|
+
* malformed body names no tool — the remedy is re-running the SAME quote
|
|
415
|
+
* tool with `include_signing_payload: true` (x402), or the payment
|
|
416
|
+
* result's `typed_data_b64` relay (direct, #3271) — and an expired window names
|
|
417
|
+
* none either (which quote tool depends on the flow). Never null: a step
|
|
418
|
+
* with no tool carries `next_tool_omitted_reason`. Additive: the class is
|
|
419
|
+
* not exported from the package index — the published surface is the
|
|
420
|
+
* `ToolFailure` envelope `tools.ts` builds from these fields — and every
|
|
421
|
+
* constructor call site is in this file.
|
|
422
|
+
*/
|
|
423
|
+
next_tool;
|
|
424
|
+
next_tool_server;
|
|
425
|
+
next_tool_name;
|
|
426
|
+
next_tool_server_role;
|
|
427
|
+
next_arguments;
|
|
428
|
+
next_tool_omitted_reason;
|
|
429
|
+
retry_with_new_quote;
|
|
430
|
+
/** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
|
|
431
|
+
http_status;
|
|
432
|
+
/** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
|
|
433
|
+
backend_error_code;
|
|
434
|
+
/**
|
|
435
|
+
* #3303: on a 426 `client_outdated` refusal, the backend's update hint —
|
|
436
|
+
* this signer is below the minimum the deployment set, and
|
|
437
|
+
* `client_update.upgrade_command` is what updates it.
|
|
438
|
+
*/
|
|
439
|
+
client_update;
|
|
440
|
+
constructor(message, code, refusal, paymentId, flow = "x402") {
|
|
441
|
+
super(message);
|
|
442
|
+
this.code = code;
|
|
443
|
+
this.name = "HavenSignContextError";
|
|
444
|
+
let step;
|
|
445
|
+
if (code === "SIGN_CONTEXT_REFUSED") {
|
|
446
|
+
this.http_status = refusal?.httpStatus;
|
|
447
|
+
this.backend_error_code = refusal?.errorCode;
|
|
448
|
+
if (refusal?.errorCode === "client_outdated") {
|
|
449
|
+
this.client_update = refusal.clientUpdate;
|
|
450
|
+
this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
|
|
451
|
+
step = signerRefusalStep({
|
|
452
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
453
|
+
nextTool: null,
|
|
454
|
+
nextToolOmittedReason: `this signer is below the minimum version Haven accepts \u2014 tell the user to run ${refusal.clientUpdate?.upgrade_command ?? "the connector again"}, restart the agent runtime, then retry the same haven_sign / haven_sign_x402 call`
|
|
455
|
+
});
|
|
456
|
+
} else if (flow === "direct" && refusal?.httpStatus === 404) {
|
|
457
|
+
this.fallback = "typed_data_b64";
|
|
458
|
+
this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
|
|
459
|
+
step = signerRefusalStep({
|
|
460
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
461
|
+
nextTool: null,
|
|
462
|
+
nextToolOmittedReason: DIRECT_RELAY_FALLBACK
|
|
463
|
+
});
|
|
464
|
+
} else if (flow === "direct" && refusal?.errorCode === "expired") {
|
|
465
|
+
this.next_action = edge.AgentPaymentNextAction.PaymentWindowExpired;
|
|
466
|
+
step = signerRefusalStep({
|
|
467
|
+
nextAction: edge.AgentPaymentNextAction.PaymentWindowExpired,
|
|
468
|
+
nextTool: null,
|
|
469
|
+
nextToolOmittedReason: "call haven_send / haven_pay again with the same idempotency_key \u2014 the expired payment frees it"
|
|
470
|
+
});
|
|
471
|
+
} else if (flow === "x402" && (refusal?.httpStatus === 410 || refusal?.errorCode === "expired")) {
|
|
472
|
+
this.next_action = edge.AgentPaymentNextAction.PaymentWindowExpired;
|
|
473
|
+
this.retry_with_new_quote = true;
|
|
474
|
+
step = signerRefusalStep({
|
|
475
|
+
nextAction: edge.AgentPaymentNextAction.PaymentWindowExpired,
|
|
476
|
+
nextTool: null,
|
|
477
|
+
nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow"
|
|
478
|
+
});
|
|
479
|
+
} else {
|
|
480
|
+
this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
|
|
481
|
+
step = signerRefusalStep({
|
|
482
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
483
|
+
nextTool: "haven_get_payment_status",
|
|
484
|
+
nextArguments: { payment_id: paymentId }
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
this.fallback = "typed_data_b64";
|
|
489
|
+
this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
|
|
490
|
+
step = signerRefusalStep({
|
|
491
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
492
|
+
nextTool: null,
|
|
493
|
+
nextToolOmittedReason: flow === "direct" ? DIRECT_RELAY_FALLBACK : "re-run the SAME hosted quote tool with the same idempotency_key and include_signing_payload: true, then pass its typed_data_b64 to this signer"
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
Object.assign(this, nextStepWireFields(step));
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
async function loadHavenIdentity(credentialsPath) {
|
|
500
|
+
if (!credentialsPath) return null;
|
|
501
|
+
try {
|
|
502
|
+
const raw = JSON.parse(
|
|
503
|
+
await promises.readFile(path.join(path.dirname(credentialsPath), "identity.json"), "utf8")
|
|
504
|
+
);
|
|
505
|
+
const apiKey = typeof raw.api_key === "string" ? raw.api_key : void 0;
|
|
506
|
+
const apiUrl = typeof raw.api_url === "string" ? raw.api_url : void 0;
|
|
507
|
+
if (!apiKey || !apiUrl) return null;
|
|
508
|
+
return { apiKey, apiUrl: apiUrl.replace(/\/+$/, "") };
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
var SIGN_CONTEXT_TIMEOUT_MS = 15e3;
|
|
514
|
+
function signContextHeaders(identity, clientIdentity) {
|
|
515
|
+
return {
|
|
516
|
+
Authorization: `Bearer ${identity.apiKey}`,
|
|
517
|
+
...clientIdentity ? { [edge.HAVEN_CLIENT_HEADER]: clientIdentity } : {}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function refusalOf(status, body) {
|
|
521
|
+
return {
|
|
522
|
+
httpStatus: status,
|
|
523
|
+
errorCode: typeof body.error_code === "string" ? body.error_code : void 0,
|
|
524
|
+
clientUpdate: edge.readClientUpdate(body.client_update)
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
|
|
528
|
+
let response;
|
|
529
|
+
try {
|
|
530
|
+
response = await fetchImpl(
|
|
531
|
+
`${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
|
|
532
|
+
{
|
|
533
|
+
headers: signContextHeaders(identity, clientIdentity),
|
|
534
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
539
|
+
throw new HavenSignContextError(
|
|
540
|
+
timedOut ? `Haven did not answer the signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.` : `Could not reach Haven to fetch the signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or pass typed_data_b64 from the quote result instead.`,
|
|
541
|
+
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
|
|
542
|
+
void 0,
|
|
543
|
+
paymentId
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
let body;
|
|
547
|
+
try {
|
|
548
|
+
body = await response.json();
|
|
549
|
+
} catch (err) {
|
|
550
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
551
|
+
throw new HavenSignContextError(
|
|
552
|
+
`Haven did not finish sending the signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.`,
|
|
553
|
+
"SIGN_CONTEXT_TIMEOUT",
|
|
554
|
+
void 0,
|
|
555
|
+
paymentId
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
body = {};
|
|
559
|
+
}
|
|
560
|
+
if (!response.ok) {
|
|
561
|
+
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
562
|
+
throw new HavenSignContextError(
|
|
563
|
+
`Haven refused the signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 check the payment_id came from this agent\u2019s own quote." : response.status === 410 ? " Re-run the quote with the same idempotency key, then sign the fresh payment_id." : ""),
|
|
564
|
+
"SIGN_CONTEXT_REFUSED",
|
|
565
|
+
refusalOf(response.status, body),
|
|
566
|
+
paymentId
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const signData = body.sign_data;
|
|
570
|
+
const x402Expected = body.x402_expected;
|
|
571
|
+
if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
|
|
572
|
+
throw new HavenSignContextError(
|
|
573
|
+
"The Haven sign-context response is missing sign_data.typed_data or x402_expected \u2014 the backend may predate #1263. Pass typed_data_b64 from the quote result instead.",
|
|
574
|
+
"SIGN_CONTEXT_MALFORMED",
|
|
575
|
+
void 0,
|
|
576
|
+
paymentId
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const paymentRequired = body.payment_required;
|
|
580
|
+
return {
|
|
581
|
+
paymentId: String(body.payment_id ?? paymentId),
|
|
582
|
+
payloadHash: signData.hash,
|
|
583
|
+
typedData: signData.typed_data,
|
|
584
|
+
x402Expected,
|
|
585
|
+
paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null,
|
|
586
|
+
clientUpdate: edge.readClientUpdate(body.client_update)
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
var SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS = [edge.DIRECT_SIGN_CONTEXT_VERSION];
|
|
590
|
+
async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
|
|
591
|
+
let response;
|
|
592
|
+
try {
|
|
593
|
+
response = await fetchImpl(
|
|
594
|
+
`${identity.apiUrl}/payments/${encodeURIComponent(paymentId)}/sign-context`,
|
|
595
|
+
{
|
|
596
|
+
headers: signContextHeaders(identity, clientIdentity),
|
|
597
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
598
|
+
}
|
|
599
|
+
);
|
|
600
|
+
} catch (err) {
|
|
601
|
+
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
602
|
+
throw new HavenSignContextError(
|
|
603
|
+
timedOut ? `Haven did not answer the direct-payment signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the payment result instead.` : `Could not reach Haven to fetch the direct-payment signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or pass typed_data_b64 from the payment result instead.`,
|
|
604
|
+
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
|
|
605
|
+
void 0,
|
|
606
|
+
paymentId,
|
|
607
|
+
"direct"
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
let body;
|
|
611
|
+
try {
|
|
612
|
+
body = await response.json();
|
|
613
|
+
} catch (err) {
|
|
614
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
615
|
+
throw new HavenSignContextError(
|
|
616
|
+
`Haven did not finish sending the direct-payment signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the payment result instead.`,
|
|
617
|
+
"SIGN_CONTEXT_TIMEOUT",
|
|
618
|
+
void 0,
|
|
619
|
+
paymentId,
|
|
620
|
+
"direct"
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
body = {};
|
|
624
|
+
}
|
|
625
|
+
if (!response.ok) {
|
|
626
|
+
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
627
|
+
throw new HavenSignContextError(
|
|
628
|
+
`Haven refused the direct-payment signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 either the payment_id is not from this agent\u2019s own POST /payments call, or this Haven backend predates #3271. Pass payload_hash and typed_data_b64 from the haven_send / haven_pay result, unchanged, instead." : body.error_code === "expired" ? " The payment window has expired; call haven_send / haven_pay again with the same idempotency_key." : ""),
|
|
629
|
+
"SIGN_CONTEXT_REFUSED",
|
|
630
|
+
refusalOf(response.status, body),
|
|
631
|
+
paymentId,
|
|
632
|
+
"direct"
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
const signData = body.sign_data;
|
|
636
|
+
const version = body.direct_sign_context_version;
|
|
637
|
+
if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || signData.signature_scheme !== "eip712_userop" || typeof version !== "number" || !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version)) {
|
|
638
|
+
throw new HavenSignContextError(
|
|
639
|
+
typeof version === "number" && !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version) ? `The Haven direct-payment sign-context response is version ${version}, which this signer does not support (supported: ${SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.` : "The Haven direct-payment sign-context response is missing sign_data.typed_data, or its signature_scheme is not 'eip712_userop' \u2014 the backend may predate #3271. Pass typed_data_b64 from the payment result instead.",
|
|
640
|
+
"SIGN_CONTEXT_MALFORMED",
|
|
641
|
+
void 0,
|
|
642
|
+
paymentId,
|
|
643
|
+
"direct"
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
paymentId: String(body.payment_id ?? paymentId),
|
|
648
|
+
payloadHash: signData.hash,
|
|
649
|
+
typedData: signData.typed_data,
|
|
650
|
+
status: typeof body.status === "string" ? body.status : "pending_signature",
|
|
651
|
+
expiresAt: typeof body.expires_at === "string" ? body.expires_at : void 0,
|
|
652
|
+
clientUpdate: edge.readClientUpdate(body.client_update)
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/capabilities.ts
|
|
468
657
|
var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
|
|
469
658
|
function signerCompatibility() {
|
|
470
659
|
return {
|
|
471
660
|
x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
|
|
472
|
-
sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS]
|
|
661
|
+
sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS],
|
|
662
|
+
direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS]
|
|
473
663
|
};
|
|
474
664
|
}
|
|
475
665
|
function signerCapabilityAdvertisement() {
|
|
@@ -480,25 +670,86 @@ function signerInstructions() {
|
|
|
480
670
|
return [
|
|
481
671
|
"Haven edge signer: sign-only tools bound to the local delegate key. It never emits the",
|
|
482
672
|
"key. Its one network capability is an authenticated READ of a signing context from",
|
|
483
|
-
"Haven by payment_id \u2014 pass payment_id to haven_sign
|
|
484
|
-
"delegation-rail x402) instead of relaying bulky
|
|
673
|
+
"Haven by payment_id \u2014 pass payment_id to haven_sign (preferred for both a direct payment,",
|
|
674
|
+
"#3271, and delegation-rail x402) or haven_sign_x402 (x402 only) instead of relaying bulky",
|
|
675
|
+
"typed-data payloads yourself.",
|
|
485
676
|
"",
|
|
486
677
|
"Version compatibility (check this BEFORE signing, not after):",
|
|
487
678
|
`- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
|
|
488
679
|
`- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
|
|
680
|
+
`- direct-payment (haven_send / haven_pay) sign-context versions supported: ${compatibility.direct_sign_context_versions.join(", ")} \u2014 pass payment_id alone to haven_sign; this signer fetches the exact bytes`,
|
|
489
681
|
"",
|
|
490
682
|
"Haven quote and prepare results report the expected-context version they will emit",
|
|
491
683
|
"(signer_compatibility.x402_expected_context_version). If that version is not in the list",
|
|
492
684
|
"above, this signer is out of date: STOP before signing, and tell the user to update",
|
|
493
|
-
`@haven_ai/signer by rerunning \`${
|
|
685
|
+
`@haven_ai/signer by rerunning \`${edge.connectorRerunCommand()}\`, which reinstalls the pinned`,
|
|
494
686
|
"MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
|
|
495
687
|
"Haven-signed binding message, so changing it invalidates the signature.",
|
|
496
688
|
"",
|
|
497
689
|
"A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
|
|
498
690
|
"machine-readable, not just prose: it carries code, supported_versions, received_version, and",
|
|
499
|
-
"fallback fields alongside the message, so you can branch on it directly."
|
|
691
|
+
"fallback fields alongside the message, so you can branch on it directly. Every signer",
|
|
692
|
+
"refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
|
|
693
|
+
"hosted tool follows (resolve the role against your own server names), else",
|
|
694
|
+
"next_tool_omitted_reason saying why not."
|
|
500
695
|
].join("\n");
|
|
501
696
|
}
|
|
697
|
+
var OWNER_ONLY_MODE = 384;
|
|
698
|
+
var defaultLog = (message) => process.stderr.write(`${message}
|
|
699
|
+
`);
|
|
700
|
+
async function permissiveMode(path, platform = process.platform, stats, follow = "nofollow") {
|
|
701
|
+
if (platform === "win32") return null;
|
|
702
|
+
let info;
|
|
703
|
+
try {
|
|
704
|
+
info = stats ?? (follow === "follow" ? await promises.stat(path) : await promises.lstat(path));
|
|
705
|
+
} catch {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
if ((info.mode & 63) === 0) return null;
|
|
709
|
+
return { octal: (info.mode & 511).toString(8).padStart(4, "0"), regular: info.isFile() };
|
|
710
|
+
}
|
|
711
|
+
async function warnIfFilePermissive(kind, path, log = defaultLog, platform = process.platform) {
|
|
712
|
+
const found = await permissiveMode(path, platform, void 0, "follow");
|
|
713
|
+
if (found === null) return;
|
|
714
|
+
log(
|
|
715
|
+
`haven-signer: warning: ${kind} at ${path} is readable beyond the owner (mode ${found.octal}). Run: chmod 600 ${path}`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
async function tightenIfFilePermissive(kind, path, log = defaultLog, platform = process.platform, stats) {
|
|
719
|
+
if (platform === "win32") return "skipped";
|
|
720
|
+
let info = stats ?? null;
|
|
721
|
+
if (!info) {
|
|
722
|
+
try {
|
|
723
|
+
info = await promises.lstat(path);
|
|
724
|
+
} catch {
|
|
725
|
+
return "owner-only";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (!info.isFile()) {
|
|
729
|
+
log(
|
|
730
|
+
`haven-signer: warning: ${kind} at ${path} is not a regular file (a symlink or similar); audit rows are being written through it and it was not tightened. Remove it: rm ${path}`
|
|
731
|
+
);
|
|
732
|
+
return "warned";
|
|
733
|
+
}
|
|
734
|
+
const found = await permissiveMode(path, platform, info);
|
|
735
|
+
if (found === null) return "owner-only";
|
|
736
|
+
{
|
|
737
|
+
try {
|
|
738
|
+
await promises.chmod(path, OWNER_ONLY_MODE);
|
|
739
|
+
log(
|
|
740
|
+
`haven-signer: ${kind} at ${path} was readable beyond the owner (mode ${found.octal}); tightened to 0600.`
|
|
741
|
+
);
|
|
742
|
+
return "tightened";
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
log(
|
|
747
|
+
`haven-signer: warning: ${kind} at ${path} is readable beyond the owner (mode ${found.octal}) and could not be tightened. Run: chmod 600 ${path}`
|
|
748
|
+
);
|
|
749
|
+
return "warned";
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/credentials.ts
|
|
502
753
|
async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
|
|
503
754
|
if (path) return loadFromFile(path);
|
|
504
755
|
const envKey = stringField(process.env.HAVEN_DELEGATE_KEY);
|
|
@@ -595,28 +846,26 @@ function invalidChainIdError(label) {
|
|
|
595
846
|
}
|
|
596
847
|
async function warnIfCredentialFilePermissive(path, log = (message) => process.stderr.write(`${message}
|
|
597
848
|
`), platform = process.platform) {
|
|
598
|
-
|
|
599
|
-
let mode;
|
|
600
|
-
try {
|
|
601
|
-
mode = (await promises.stat(path)).mode;
|
|
602
|
-
} catch {
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
if ((mode & 63) !== 0) {
|
|
606
|
-
const octal = (mode & 511).toString(8).padStart(4, "0");
|
|
607
|
-
log(
|
|
608
|
-
`haven-signer: warning: credential file at ${path} is readable beyond the owner (mode ${octal}). Run: chmod 600 ${path}`
|
|
609
|
-
);
|
|
610
|
-
}
|
|
849
|
+
await warnIfFilePermissive("credential file", path, log, platform);
|
|
611
850
|
}
|
|
851
|
+
var AUDIT_ROTATE_BYTES = 8 * 1024 * 1024;
|
|
612
852
|
function defaultSigningAuditPath(credentialsPath) {
|
|
613
853
|
if (credentialsPath) return path.resolve(`${credentialsPath}.signer-audit.jsonl`);
|
|
614
854
|
return path.resolve(os.homedir(), ".haven", "signer-audit.jsonl");
|
|
615
855
|
}
|
|
616
|
-
async function appendSigningAuditEntry(entry, path$1) {
|
|
856
|
+
async function appendSigningAuditEntry(entry, path$1, options = {}) {
|
|
617
857
|
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
858
|
+
const rotateAt = options.rotateAtBytes ?? AUDIT_ROTATE_BYTES;
|
|
859
|
+
const existing = await promises.lstat(path$1).catch(() => null);
|
|
860
|
+
if (existing) {
|
|
861
|
+
await tightenIfFilePermissive("audit sidecar", path$1, options.log, options.platform, existing);
|
|
862
|
+
if (existing.size >= rotateAt) {
|
|
863
|
+
await promises.rename(path$1, `${path$1}.1`).catch(() => {
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}
|
|
618
867
|
await promises.appendFile(path$1, `${JSON.stringify(entry)}
|
|
619
|
-
`, "utf8");
|
|
868
|
+
`, { encoding: "utf8", mode: OWNER_ONLY_MODE });
|
|
620
869
|
}
|
|
621
870
|
function createSigningAuditEntry(tool, payloadHash, context, now = /* @__PURE__ */ new Date()) {
|
|
622
871
|
const entry = {
|
|
@@ -642,120 +891,64 @@ function stableStringify(value) {
|
|
|
642
891
|
const object = value;
|
|
643
892
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
|
|
644
893
|
}
|
|
645
|
-
var
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
* `retry_with_new_quote`) for the backend's 410 `expired`, exactly as the
|
|
661
|
-
* plain `HavenError` branch already does for that code.
|
|
662
|
-
*/
|
|
663
|
-
next_action;
|
|
664
|
-
retry_with_new_quote;
|
|
665
|
-
/** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
|
|
666
|
-
http_status;
|
|
667
|
-
/** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
|
|
668
|
-
backend_error_code;
|
|
669
|
-
constructor(message, code, refusal) {
|
|
670
|
-
super(message);
|
|
671
|
-
this.code = code;
|
|
672
|
-
this.name = "HavenSignContextError";
|
|
673
|
-
if (code === "SIGN_CONTEXT_REFUSED") {
|
|
674
|
-
this.http_status = refusal?.httpStatus;
|
|
675
|
-
this.backend_error_code = refusal?.errorCode;
|
|
676
|
-
if (refusal?.httpStatus === 410 || refusal?.errorCode === "expired") {
|
|
677
|
-
this.next_action = sdk.AgentPaymentNextAction.PaymentWindowExpired;
|
|
678
|
-
this.retry_with_new_quote = true;
|
|
679
|
-
} else {
|
|
680
|
-
this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
|
|
681
|
-
}
|
|
682
|
-
} else {
|
|
683
|
-
this.fallback = "typed_data_b64";
|
|
684
|
-
this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
|
|
685
|
-
}
|
|
894
|
+
var BARE_HASH_REFUSED = "BARE_HASH_REFUSED";
|
|
895
|
+
var HavenBareHashRefusedError = class extends edge.HavenSigningError {
|
|
896
|
+
next_action = edge.AgentPaymentNextAction.StopAndTellUser;
|
|
897
|
+
step;
|
|
898
|
+
constructor() {
|
|
899
|
+
super(
|
|
900
|
+
"Refusing to sign a bare payload_hash. A hash carries no shape, amount, merchant or binding, so this signer cannot tell a Haven payment from a transfer out of the delegate wallet or a delegation of its authority \u2014 and the only rail that ever validated a raw hash signature is retired. Call this tool with { payment_id } (the signer fetches and verifies the Haven-signed context itself), or with typed_data / typed_data_b64 (the account validates that EIP-712 payload), or with x402_expected for an x402 funding leg."
|
|
901
|
+
);
|
|
902
|
+
this.code = BARE_HASH_REFUSED;
|
|
903
|
+
this.name = "HavenBareHashRefusedError";
|
|
904
|
+
this.step = signerRefusalStep({
|
|
905
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
906
|
+
nextTool: null,
|
|
907
|
+
nextToolOmittedReason: "call haven_sign again with payment_id (preferred), typed_data / typed_data_b64, or x402_expected \u2014 a bare payload_hash is never signed"
|
|
908
|
+
});
|
|
686
909
|
}
|
|
687
910
|
};
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
911
|
+
|
|
912
|
+
// src/tools.ts
|
|
913
|
+
var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
|
|
914
|
+
var HavenUserOpBindingRefusedError = class extends edge.HavenSigningError {
|
|
915
|
+
next_action = edge.AgentPaymentNextAction.StopAndTellUser;
|
|
916
|
+
step;
|
|
917
|
+
/** `message`: the SDK's `HavenUserOpBindingError.message` verbatim — it already names the mismatch and the remedy. */
|
|
918
|
+
constructor(message) {
|
|
919
|
+
super(message);
|
|
920
|
+
this.code = USEROP_BINDING_MISMATCH;
|
|
921
|
+
this.name = "HavenUserOpBindingRefusedError";
|
|
922
|
+
this.step = signerRefusalStep({
|
|
923
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
924
|
+
nextTool: null,
|
|
925
|
+
nextToolOmittedReason: "call haven_sign again with payment_id alone (preferred), or with typed_data copied unchanged from the payment result"
|
|
926
|
+
});
|
|
700
927
|
}
|
|
701
|
-
}
|
|
702
|
-
var
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
);
|
|
713
|
-
} catch (err) {
|
|
714
|
-
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
715
|
-
throw new HavenSignContextError(
|
|
716
|
-
timedOut ? `Haven did not answer the signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.` : `Could not reach Haven to fetch the signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or pass typed_data_b64 from the quote result instead.`,
|
|
717
|
-
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE"
|
|
718
|
-
);
|
|
928
|
+
};
|
|
929
|
+
var HavenTypedDataNotAllowedError = class extends edge.HavenTypedDataRefusedError {
|
|
930
|
+
next_action = edge.AgentPaymentNextAction.StopAndTellUser;
|
|
931
|
+
step;
|
|
932
|
+
constructor(message) {
|
|
933
|
+
super(message);
|
|
934
|
+
this.name = "HavenTypedDataNotAllowedError";
|
|
935
|
+
this.step = signerRefusalStep({
|
|
936
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
937
|
+
nextTool: null,
|
|
938
|
+
nextToolOmittedReason: "sign only Haven-prepared payloads: call haven_sign with payment_id (preferred), or pass typed_data / typed_data_b64 copied unchanged from a haven_send / haven_pay result, or x402_expected from a Haven quote"
|
|
939
|
+
});
|
|
719
940
|
}
|
|
720
|
-
|
|
941
|
+
};
|
|
942
|
+
function assertBoundDirectPaymentUserOp2(typedData, delegateAddress) {
|
|
721
943
|
try {
|
|
722
|
-
|
|
944
|
+
edge.assertBoundDirectPaymentUserOp(typedData, delegateAddress);
|
|
723
945
|
} catch (err) {
|
|
724
|
-
if (err instanceof
|
|
725
|
-
throw new
|
|
726
|
-
`Haven did not finish sending the signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.`,
|
|
727
|
-
"SIGN_CONTEXT_TIMEOUT"
|
|
728
|
-
);
|
|
946
|
+
if (err instanceof edge.HavenTypedDataRefusedError && !(err instanceof HavenTypedDataNotAllowedError)) {
|
|
947
|
+
throw new HavenTypedDataNotAllowedError(err.message);
|
|
729
948
|
}
|
|
730
|
-
|
|
949
|
+
throw err;
|
|
731
950
|
}
|
|
732
|
-
if (!response.ok) {
|
|
733
|
-
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
734
|
-
throw new HavenSignContextError(
|
|
735
|
-
`Haven refused the signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 check the payment_id came from this agent\u2019s own quote." : response.status === 410 ? " Re-run the quote with the same idempotency key, then sign the fresh payment_id." : ""),
|
|
736
|
-
"SIGN_CONTEXT_REFUSED",
|
|
737
|
-
{ httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 }
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
const signData = body.sign_data;
|
|
741
|
-
const x402Expected = body.x402_expected;
|
|
742
|
-
if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
|
|
743
|
-
throw new HavenSignContextError(
|
|
744
|
-
"The Haven sign-context response is missing sign_data.typed_data or x402_expected \u2014 the backend may predate #1263. Pass typed_data_b64 from the quote result instead.",
|
|
745
|
-
"SIGN_CONTEXT_MALFORMED"
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
const paymentRequired = body.payment_required;
|
|
749
|
-
return {
|
|
750
|
-
paymentId: String(body.payment_id ?? paymentId),
|
|
751
|
-
payloadHash: signData.hash,
|
|
752
|
-
typedData: signData.typed_data,
|
|
753
|
-
x402Expected,
|
|
754
|
-
paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null
|
|
755
|
-
};
|
|
756
951
|
}
|
|
757
|
-
|
|
758
|
-
// src/tools.ts
|
|
759
952
|
var sweepAuthorizationSchema = v3.z.object({
|
|
760
953
|
from: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "from must be a 0x address"),
|
|
761
954
|
to: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "to must be a 0x address"),
|
|
@@ -775,7 +968,7 @@ var sweepExpectedAuthSchema = v3.z.object({
|
|
|
775
968
|
});
|
|
776
969
|
var x402ExpectedShape = {
|
|
777
970
|
payment_id: v3.z.string().min(1),
|
|
778
|
-
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]
|
|
971
|
+
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "payload_hash must be a 0x-prefixed 32-byte hex string"),
|
|
779
972
|
resource_url: v3.z.string().url(),
|
|
780
973
|
merchant_to: v3.z.string().min(1),
|
|
781
974
|
amount: v3.z.string().min(1),
|
|
@@ -789,7 +982,7 @@ var x402ExpectedShape = {
|
|
|
789
982
|
// #1138: present on a delegation-rail (v2) context. It commits to the EIP-712
|
|
790
983
|
// typed data the account validates; the signer refuses to raw-sign the bare
|
|
791
984
|
// hash when it is present, and refuses to sign typed data when it is absent.
|
|
792
|
-
typed_data_hash: v3.z.string().regex(/^0x[0-9a-fA-F]
|
|
985
|
+
typed_data_hash: v3.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "typed_data_hash must be a 0x-prefixed 32-byte hex string").optional(),
|
|
793
986
|
// #1690: present on a v3 context. The delegate this quote was created FOR —
|
|
794
987
|
// the signer refuses to sign when it is not its own. Inside the Haven-signed
|
|
795
988
|
// message, so stripping it here would only break the binding signature.
|
|
@@ -821,7 +1014,7 @@ var toolSchemas = {
|
|
|
821
1014
|
payment_id: v3.z.string().min(1).optional(),
|
|
822
1015
|
// The unsigned hash from haven_pay / haven_pay_x402_quote (payload_hash).
|
|
823
1016
|
// Optional when payment_id is supplied (the fetch carries it).
|
|
824
|
-
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]
|
|
1017
|
+
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "payload_hash must be a 0x-prefixed 32-byte hex string").optional(),
|
|
825
1018
|
// Pass x402.expected from hosted haven_pay_x402_quote when this hash funds
|
|
826
1019
|
// a standard x402 merchant retry. The signer records it locally and returns
|
|
827
1020
|
// an opaque x402_binding for the later header-signing step.
|
|
@@ -853,7 +1046,7 @@ var toolSchemas = {
|
|
|
853
1046
|
payment_id: v3.z.string().min(1).optional(),
|
|
854
1047
|
// One-shot x402 signing: funding hash + merchant header in one local call.
|
|
855
1048
|
// Both optional when payment_id is supplied (the fetch carries them).
|
|
856
|
-
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]
|
|
1049
|
+
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "payload_hash must be a 0x-prefixed 32-byte hex string").optional(),
|
|
857
1050
|
x402_expected: x402ExpectedSchema.optional(),
|
|
858
1051
|
// #1355: optional when payment_id is supplied — the signer's context fetch
|
|
859
1052
|
// carries the stored 402 PaymentRequired, so `{ payment_id }` alone is the
|
|
@@ -866,19 +1059,28 @@ var toolSchemas = {
|
|
|
866
1059
|
// #1255: see haven_sign.typed_data_b64 — the copy-through-safe form.
|
|
867
1060
|
typed_data_b64: v3.z.string().min(1).max(262144).optional()
|
|
868
1061
|
}
|
|
1062
|
+
// #3101: keys survive on the type (see the hosted server's contracts.ts).
|
|
869
1063
|
};
|
|
870
1064
|
var SIGN_DESCRIPTION = [
|
|
871
|
-
"Sign an unsigned Haven payment
|
|
872
|
-
"this process. Pass the payload_hash
|
|
1065
|
+
"Sign an unsigned Haven payment with the local delegate key. The delegate key never leaves",
|
|
1066
|
+
"this process. Pass payment_id (preferred), or the payload_hash from haven_pay / haven_pay_x402_quote",
|
|
1067
|
+
"TOGETHER WITH typed_data / typed_data_b64 or x402_expected \u2014 never a payload_hash alone.",
|
|
873
1068
|
"For x402, also pass x402_expected from haven_pay_x402_quote; the signer records it locally",
|
|
874
1069
|
"and returns { signature, x402_binding }. x402_expected includes expires_at; sign before that",
|
|
875
1070
|
"window closes. DELEGATION-RAIL x402 accounts (#1263): pass payment_id ALONE (preferred) \u2014 this",
|
|
876
1071
|
"signer fetches the exact signing payload and expected context from Haven itself, so nothing",
|
|
877
|
-
"bulky ever crosses your context.
|
|
878
|
-
"
|
|
1072
|
+
"bulky ever crosses your context. DIRECT payments (#3271, haven_send / haven_pay): payment_id",
|
|
1073
|
+
"ALONE also works here \u2014 the signer fetches the exact PackedUserOperation typed data and its",
|
|
1074
|
+
"payload_hash from Haven and checks them against each other before signing, so a payload corrupted",
|
|
1075
|
+
"in transit is refused (USEROP_BINDING_MISMATCH) instead of producing a bad signature. Fallback for",
|
|
1076
|
+
"either flow: pass typed_data_b64 through UNCHANGED (never re-type the nested typed_data JSON); the",
|
|
1077
|
+
"account validates that EIP-712 payload, not payload_hash. On a direct payment the same binding",
|
|
1078
|
+
"check runs on the relayed payload too. This tool signs ONLY Haven-prepared payloads (#3272): a",
|
|
1079
|
+
"direct-payment UserOp from your own account that redeems a delegation granted to it, or an x402",
|
|
1080
|
+
"intent against a Haven-signed context \u2014 never an arbitrary typed_data payload, however it is shaped.",
|
|
879
1081
|
"Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
|
|
880
|
-
"to mcp__haven-signer__haven_x402_sign_header.
|
|
881
|
-
"
|
|
1082
|
+
"to mcp__haven-signer__haven_x402_sign_header. A bare payload_hash with no payment_id, typed_data",
|
|
1083
|
+
"or x402_expected is REFUSED (BARE_HASH_REFUSED): a hash carries nothing this signer can verify."
|
|
882
1084
|
].join(" ");
|
|
883
1085
|
var X402_SIGN_HEADER_DESCRIPTION = [
|
|
884
1086
|
"Build and sign the EIP-3009 merchant payment header for the merchant leg of an x402 payment.",
|
|
@@ -896,7 +1098,7 @@ var X402_SIGN_HEADER_DESCRIPTION = [
|
|
|
896
1098
|
"the funding tx has a confirmed status). Next for paid MCP tools: call mcp__haven__haven_complete_mcp_tool."
|
|
897
1099
|
].join(" ");
|
|
898
1100
|
var SIGN_X402_DESCRIPTION = [
|
|
899
|
-
"One-shot x402 signing for the fast 3-call flow: sign the funding
|
|
1101
|
+
"One-shot x402 signing for the fast 3-call flow: sign the funding payload AND build the EIP-3009",
|
|
900
1102
|
"merchant payment header in a single local call (equivalent to haven_sign followed by",
|
|
901
1103
|
"haven_x402_sign_header). The delegate key never leaves this process. From the haven_pay_mcp_tool",
|
|
902
1104
|
"result pass JUST payment_id \u2014 PREFERRED (#1263, #1355): this signer fetches the exact signing",
|
|
@@ -922,12 +1124,13 @@ var SIGN_X402_DESCRIPTION = [
|
|
|
922
1124
|
].join(" ");
|
|
923
1125
|
var SIGN_SWEEP_DELEGATE_DESCRIPTION = [
|
|
924
1126
|
"Sign a Haven-prepared gasless USDC sweep that recovers stranded funds from the delegate",
|
|
925
|
-
"wallet back to
|
|
926
|
-
"never broadcasts \u2014 it returns only an EIP-3009 signature that Haven's relayer submits and",
|
|
1127
|
+
"wallet back to the agent's account (Haven wallet). The delegate key never leaves this process and this",
|
|
1128
|
+
"tool never broadcasts \u2014 it returns only an EIP-3009 signature that Haven's relayer submits and",
|
|
927
1129
|
"pays gas for. Pass the authorization and expected_auth returned by the hosted",
|
|
928
|
-
"haven_sweep_delegate tool. The signer verifies Haven authored the authorization
|
|
929
|
-
"
|
|
930
|
-
"
|
|
1130
|
+
"haven_sweep_delegate tool. The signer verifies Haven authored the authorization; the",
|
|
1131
|
+
"destination is checked against the agent's account (Haven wallet) from the local credential",
|
|
1132
|
+
"when the credential carries one, and otherwise rests on Haven's binding signature. Then it",
|
|
1133
|
+
"returns { signature } to hand back to mcp__haven__haven_sweep_delegate to complete recovery."
|
|
931
1134
|
].join(" ");
|
|
932
1135
|
var toolDescriptions = {
|
|
933
1136
|
haven_sign: SIGN_DESCRIPTION,
|
|
@@ -935,16 +1138,18 @@ var toolDescriptions = {
|
|
|
935
1138
|
haven_sign_x402: SIGN_X402_DESCRIPTION,
|
|
936
1139
|
haven_sign_sweep_delegate: SIGN_SWEEP_DELEGATE_DESCRIPTION
|
|
937
1140
|
};
|
|
938
|
-
async function signFundingLeg(signer, expected,
|
|
939
|
-
|
|
940
|
-
return signer.
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
)
|
|
1141
|
+
async function signFundingLeg(signer, expected, typedData) {
|
|
1142
|
+
try {
|
|
1143
|
+
return await signer.signX402FundingTypedData(typedData, expected);
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
if (err instanceof edge.HavenTypedDataRefusedError && !(err instanceof HavenTypedDataNotAllowedError)) {
|
|
1146
|
+
throw new HavenTypedDataNotAllowedError(err.message);
|
|
1147
|
+
}
|
|
1148
|
+
if (err instanceof edge.HavenUserOpBindingError) {
|
|
1149
|
+
throw new HavenUserOpBindingRefusedError(err.message);
|
|
1150
|
+
}
|
|
1151
|
+
throw err;
|
|
946
1152
|
}
|
|
947
|
-
return signer.signX402FundingTypedData(typedData, expected);
|
|
948
1153
|
}
|
|
949
1154
|
function toExpectedX402(raw) {
|
|
950
1155
|
return {
|
|
@@ -965,12 +1170,15 @@ function toExpectedX402(raw) {
|
|
|
965
1170
|
function parseFetchedExpected(fetched) {
|
|
966
1171
|
const parsed = v3.z.object(x402ExpectedShape).safeParse(fetched.x402Expected);
|
|
967
1172
|
if (!parsed.success) {
|
|
968
|
-
throw new
|
|
1173
|
+
throw new edge.HavenSigningError(
|
|
969
1174
|
`The Haven sign-context response carried a malformed x402_expected \u2014 the backend may predate #1263. Pass payload_hash + x402_expected from the quote result instead. Underlying: ${parsed.error.issues[0]?.message ?? "schema mismatch"}`
|
|
970
1175
|
);
|
|
971
1176
|
}
|
|
972
1177
|
return parsed.data;
|
|
973
1178
|
}
|
|
1179
|
+
function clientUpdateField(resolved) {
|
|
1180
|
+
return resolved?.ctx.clientUpdate ? { client_update: resolved.ctx.clientUpdate } : {};
|
|
1181
|
+
}
|
|
974
1182
|
function resolveTypedData(args) {
|
|
975
1183
|
if (!args.typed_data_b64) return args.typed_data;
|
|
976
1184
|
try {
|
|
@@ -982,63 +1190,103 @@ function resolveTypedData(args) {
|
|
|
982
1190
|
}
|
|
983
1191
|
return decoded;
|
|
984
1192
|
} catch (err) {
|
|
985
|
-
throw new
|
|
1193
|
+
throw new edge.HavenSigningError(
|
|
986
1194
|
`typed_data_b64 did not decode to a JSON object. Pass the exact string returned by the hosted Haven tool, unchanged \u2014 do not re-encode, trim, or reformat it. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
987
1195
|
);
|
|
988
1196
|
}
|
|
989
1197
|
}
|
|
990
1198
|
function createToolHandlers(signer, options = {}) {
|
|
991
|
-
|
|
1199
|
+
function checkPayloadHashMatch(suppliedHash, fetchedHash, paymentId) {
|
|
1200
|
+
if (suppliedHash && suppliedHash.toLowerCase() !== fetchedHash.toLowerCase()) {
|
|
1201
|
+
throw new edge.HavenSigningError(
|
|
1202
|
+
`The supplied payload_hash does not match the signing context Haven serves for payment ${paymentId}. Pass payment_id alone, or check which quote the hash came from.`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
async function resolveSignContext(args, opts) {
|
|
992
1207
|
if (!args.payment_id) return null;
|
|
993
1208
|
const identity = await options.signContext?.loadIdentity() ?? null;
|
|
994
1209
|
if (!identity) {
|
|
995
|
-
throw new
|
|
996
|
-
`payment_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${
|
|
1210
|
+
throw new edge.HavenSigningError(
|
|
1211
|
+
`payment_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${edge.connectorRerunCommand()}\` to restore it, or pass typed_data_b64 from the quote result instead.`
|
|
997
1212
|
);
|
|
998
1213
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
`The supplied payload_hash does not match the signing context Haven serves for payment ${args.payment_id}. Pass payment_id alone, or check which quote the hash came from.`
|
|
1214
|
+
try {
|
|
1215
|
+
const ctx = await fetchX402SignContext(
|
|
1216
|
+
identity,
|
|
1217
|
+
args.payment_id,
|
|
1218
|
+
options.signContext?.fetchImpl,
|
|
1219
|
+
void 0,
|
|
1220
|
+
options.signContext?.clientIdentity
|
|
1007
1221
|
);
|
|
1222
|
+
checkPayloadHashMatch(args.payload_hash, ctx.payloadHash, args.payment_id);
|
|
1223
|
+
return { kind: "x402", ctx };
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
if (opts.allowDirectFallback && err instanceof HavenSignContextError && err.http_status === 409 && err.backend_error_code === "sign_context_unavailable") {
|
|
1226
|
+
let ctx;
|
|
1227
|
+
try {
|
|
1228
|
+
ctx = await fetchDirectSignContext(
|
|
1229
|
+
identity,
|
|
1230
|
+
args.payment_id,
|
|
1231
|
+
options.signContext?.fetchImpl,
|
|
1232
|
+
void 0,
|
|
1233
|
+
options.signContext?.clientIdentity
|
|
1234
|
+
);
|
|
1235
|
+
} catch (directErr) {
|
|
1236
|
+
if (directErr instanceof HavenSignContextError && directErr.http_status === 409 && directErr.backend_error_code === "sign_context_unavailable") {
|
|
1237
|
+
throw err;
|
|
1238
|
+
}
|
|
1239
|
+
throw directErr;
|
|
1240
|
+
}
|
|
1241
|
+
checkPayloadHashMatch(args.payload_hash, ctx.payloadHash, args.payment_id);
|
|
1242
|
+
return { kind: "direct", ctx };
|
|
1243
|
+
}
|
|
1244
|
+
throw err;
|
|
1008
1245
|
}
|
|
1009
|
-
return ctx;
|
|
1010
1246
|
}
|
|
1011
1247
|
return {
|
|
1012
1248
|
haven_sign: async (input) => runTool(async () => {
|
|
1013
1249
|
const args = parse("haven_sign", coerceX402Expected(input));
|
|
1014
|
-
const
|
|
1015
|
-
const typedData =
|
|
1016
|
-
const payloadHash =
|
|
1250
|
+
const resolved = await resolveSignContext(args, { allowDirectFallback: true });
|
|
1251
|
+
const typedData = resolved?.ctx.typedData ?? resolveTypedData(args);
|
|
1252
|
+
const payloadHash = resolved?.ctx.payloadHash ?? args.payload_hash;
|
|
1017
1253
|
if (!payloadHash) {
|
|
1018
|
-
throw new
|
|
1019
|
-
"Pass payment_id (preferred for delegation-rail x402) or payload_hash."
|
|
1254
|
+
throw new edge.HavenSigningError(
|
|
1255
|
+
"Pass payment_id (preferred for delegation-rail x402 or direct payments) or payload_hash."
|
|
1020
1256
|
);
|
|
1021
1257
|
}
|
|
1022
|
-
const expectedRaw =
|
|
1258
|
+
const expectedRaw = resolved?.kind === "x402" ? parseFetchedExpected(resolved.ctx) : resolved?.kind === "direct" ? null : args.x402_expected;
|
|
1023
1259
|
const x402Expected = expectedRaw ? toExpectedX402(expectedRaw) : null;
|
|
1024
|
-
const result = x402Expected ? await signFundingLeg(signer, x402Expected,
|
|
1260
|
+
const result = x402Expected ? await signFundingLeg(signer, x402Expected, typedData) : null;
|
|
1025
1261
|
if (!result) {
|
|
1026
1262
|
if (typedData) {
|
|
1027
|
-
if (isSettlementChildTypedData(typedData)) {
|
|
1028
|
-
throw new
|
|
1263
|
+
if (edge.isSettlementChildTypedData(typedData)) {
|
|
1264
|
+
throw new edge.HavenSigningError(
|
|
1029
1265
|
"Refusing to sign a delegation payload with no expected context. This typed data is a DELEGATION \u2014 signing it grants a third party authority to move funds \u2014 so it is only ever signed against a Haven-signed context that the caveats are verified against. Call this tool with { payment_id } instead (the signer then fetches and verifies the context itself), or use haven_sign_x402."
|
|
1030
1266
|
);
|
|
1031
1267
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1268
|
+
if (resolved?.kind !== "direct" && !edge.isPackedUserOperationTypedData(typedData)) {
|
|
1269
|
+
throw new HavenTypedDataNotAllowedError(
|
|
1270
|
+
"This signer signs only Haven-prepared payloads: a direct-payment PackedUserOperation from your own account that redeems a delegation granted to it, or a Haven-signed x402 context. Call haven_sign with payment_id (preferred), or pass x402_expected for an x402 funding leg."
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
try {
|
|
1274
|
+
edge.assertUserOpTypedDataBinding(typedData, payloadHash);
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
if (err instanceof edge.HavenUserOpBindingError) {
|
|
1277
|
+
throw new HavenUserOpBindingRefusedError(err.message);
|
|
1278
|
+
}
|
|
1279
|
+
throw err;
|
|
1280
|
+
}
|
|
1281
|
+
assertBoundDirectPaymentUserOp2(typedData, signer.delegateAddress);
|
|
1282
|
+
const signature = await signer.signDelegationTypedData(typedData);
|
|
1283
|
+
await auditSigning("haven_sign", viem.hashTypedData(typedData));
|
|
1284
|
+
return { signature, ...clientUpdateField(resolved) };
|
|
1035
1285
|
}
|
|
1036
|
-
|
|
1037
|
-
await auditSigning("haven_sign", payloadHash);
|
|
1038
|
-
return { signature };
|
|
1286
|
+
throw new HavenBareHashRefusedError();
|
|
1039
1287
|
}
|
|
1040
|
-
await auditSigning("haven_sign",
|
|
1041
|
-
return { signature: result.signature, x402_binding: result.x402Binding };
|
|
1288
|
+
await auditSigning("haven_sign", viem.hashTypedData(typedData));
|
|
1289
|
+
return { signature: result.signature, x402_binding: result.x402Binding, ...clientUpdateField(resolved) };
|
|
1042
1290
|
}),
|
|
1043
1291
|
haven_x402_sign_header: async (input) => runTool(async () => {
|
|
1044
1292
|
const args = parse("haven_x402_sign_header", coercePaymentRequired(input));
|
|
@@ -1054,23 +1302,20 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1054
1302
|
}),
|
|
1055
1303
|
haven_sign_x402: async (input) => runTool(async () => {
|
|
1056
1304
|
const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
|
|
1057
|
-
const
|
|
1305
|
+
const resolved = await resolveSignContext(args, { allowDirectFallback: false });
|
|
1306
|
+
const fetched = resolved?.kind === "x402" ? resolved.ctx : void 0;
|
|
1058
1307
|
const payloadHash = fetched?.payloadHash ?? args.payload_hash;
|
|
1059
1308
|
const expectedRaw = fetched ? parseFetchedExpected(fetched) : args.x402_expected;
|
|
1060
1309
|
if (!payloadHash || !expectedRaw) {
|
|
1061
|
-
throw new
|
|
1310
|
+
throw new edge.HavenSigningError(
|
|
1062
1311
|
"Pass payment_id (preferred \u2014 the signer fetches the signing context itself) or payload_hash + x402_expected from the quote result."
|
|
1063
1312
|
);
|
|
1064
1313
|
}
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
toExpectedX402(expectedRaw),
|
|
1068
|
-
payloadHash,
|
|
1069
|
-
fetched?.typedData ?? resolveTypedData(args)
|
|
1070
|
-
);
|
|
1314
|
+
const fundingTypedData = fetched?.typedData ?? resolveTypedData(args);
|
|
1315
|
+
const funding = await signFundingLeg(signer, toExpectedX402(expectedRaw), fundingTypedData);
|
|
1071
1316
|
const paymentRequired = fetched?.paymentRequired ?? args.payment_required;
|
|
1072
1317
|
if (!paymentRequired) {
|
|
1073
|
-
throw new
|
|
1318
|
+
throw new edge.HavenSigningError(
|
|
1074
1319
|
fetched ? "The Haven sign-context for this payment_id carried no payment_required (backend predates #1355). Pass payment_required from the pay result explicitly, verbatim." : "payment_required is required on the quote-based path (no payment_id to fetch it by). Pass it verbatim from the quote result."
|
|
1075
1320
|
);
|
|
1076
1321
|
}
|
|
@@ -1078,13 +1323,17 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1078
1323
|
paymentRequired,
|
|
1079
1324
|
funding.x402Binding
|
|
1080
1325
|
);
|
|
1081
|
-
await auditSigning(
|
|
1326
|
+
await auditSigning(
|
|
1327
|
+
"haven_sign_x402",
|
|
1328
|
+
viem.hashTypedData(fundingTypedData)
|
|
1329
|
+
);
|
|
1082
1330
|
await auditSigning("haven_sign_x402", hashPayloadForAudit(paymentRequired));
|
|
1083
1331
|
return {
|
|
1084
1332
|
signature: funding.signature,
|
|
1085
1333
|
x402_binding: funding.x402Binding,
|
|
1086
1334
|
payment_header: header.paymentHeader,
|
|
1087
|
-
accepted: header.accepted
|
|
1335
|
+
accepted: header.accepted,
|
|
1336
|
+
...clientUpdateField(resolved)
|
|
1088
1337
|
};
|
|
1089
1338
|
}),
|
|
1090
1339
|
haven_sign_sweep_delegate: async (input) => runTool(async () => {
|
|
@@ -1092,7 +1341,8 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1092
1341
|
const result = await signer.signSweepAuthorization({
|
|
1093
1342
|
authorization: args.authorization,
|
|
1094
1343
|
expectedAuth: args.expected_auth,
|
|
1095
|
-
// Cross-check `to` against the
|
|
1344
|
+
// Cross-check `to` against the agent's account (Haven wallet) from the
|
|
1345
|
+
// local credential, when the credential carries one.
|
|
1096
1346
|
expectedSafe: options.audit?.accountAddress
|
|
1097
1347
|
});
|
|
1098
1348
|
await auditSigning(
|
|
@@ -1105,13 +1355,19 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1105
1355
|
async function auditSigning(tool, payloadHash) {
|
|
1106
1356
|
if (!options.audit) return;
|
|
1107
1357
|
const { auditPath, ...context } = options.audit;
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1358
|
+
try {
|
|
1359
|
+
await appendSigningAuditEntry(
|
|
1360
|
+
createSigningAuditEntry(tool, payloadHash, {
|
|
1361
|
+
...context,
|
|
1362
|
+
delegateAddress: signer.delegateAddress
|
|
1363
|
+
}),
|
|
1364
|
+
auditPath
|
|
1365
|
+
);
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1368
|
+
process.stderr.write(`haven-signer: warning: audit entry for ${tool} could not be written to ${auditPath}: ${message}
|
|
1369
|
+
`);
|
|
1370
|
+
}
|
|
1115
1371
|
}
|
|
1116
1372
|
}
|
|
1117
1373
|
function parse(name, input) {
|
|
@@ -1162,7 +1418,7 @@ function normalizeError(err) {
|
|
|
1162
1418
|
statusCode: 400
|
|
1163
1419
|
};
|
|
1164
1420
|
}
|
|
1165
|
-
if (err instanceof
|
|
1421
|
+
if (err instanceof edge.HavenUnsupportedSignerVersionError) {
|
|
1166
1422
|
return {
|
|
1167
1423
|
success: false,
|
|
1168
1424
|
code: err.code,
|
|
@@ -1170,7 +1426,40 @@ function normalizeError(err) {
|
|
|
1170
1426
|
supported_versions: [...err.supportedVersions],
|
|
1171
1427
|
received_version: err.receivedVersion,
|
|
1172
1428
|
fallback: err.fallback,
|
|
1173
|
-
next_action:
|
|
1429
|
+
next_action: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
1430
|
+
// #3103: no tool can fix a version skew from inside the call.
|
|
1431
|
+
...nextStepWireFields(signerRefusalStep({
|
|
1432
|
+
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
1433
|
+
nextTool: null,
|
|
1434
|
+
nextToolOmittedReason: "update @haven_ai/signer by re-running the connector, then repeat the same call"
|
|
1435
|
+
}))
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
if (err instanceof HavenBareHashRefusedError) {
|
|
1439
|
+
return {
|
|
1440
|
+
success: false,
|
|
1441
|
+
code: err.code,
|
|
1442
|
+
message: err.message,
|
|
1443
|
+
next_action: err.next_action,
|
|
1444
|
+
...nextStepWireFields(err.step)
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
if (err instanceof HavenUserOpBindingRefusedError) {
|
|
1448
|
+
return {
|
|
1449
|
+
success: false,
|
|
1450
|
+
code: err.code,
|
|
1451
|
+
message: err.message,
|
|
1452
|
+
next_action: err.next_action,
|
|
1453
|
+
...nextStepWireFields(err.step)
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
if (err instanceof HavenTypedDataNotAllowedError) {
|
|
1457
|
+
return {
|
|
1458
|
+
success: false,
|
|
1459
|
+
code: err.code,
|
|
1460
|
+
message: err.message,
|
|
1461
|
+
next_action: err.next_action,
|
|
1462
|
+
...nextStepWireFields(err.step)
|
|
1174
1463
|
};
|
|
1175
1464
|
}
|
|
1176
1465
|
if (err instanceof HavenSignContextError) {
|
|
@@ -1182,26 +1471,40 @@ function normalizeError(err) {
|
|
|
1182
1471
|
...err.fallback !== void 0 ? { fallback: err.fallback } : {},
|
|
1183
1472
|
...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
|
|
1184
1473
|
...err.http_status !== void 0 ? { http_status: err.http_status } : {},
|
|
1185
|
-
...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {}
|
|
1474
|
+
...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {},
|
|
1475
|
+
...err.client_update ? { client_update: err.client_update } : {},
|
|
1476
|
+
// #3103: the typed step the error decided beside its action.
|
|
1477
|
+
...err.next_tool ? { next_tool: err.next_tool } : {},
|
|
1478
|
+
...err.next_tool_server ? { next_tool_server: err.next_tool_server } : {},
|
|
1479
|
+
...err.next_tool_name ? { next_tool_name: err.next_tool_name } : {},
|
|
1480
|
+
...err.next_tool_server_role ? { next_tool_server_role: err.next_tool_server_role } : {},
|
|
1481
|
+
...err.next_arguments ? { next_arguments: err.next_arguments } : {},
|
|
1482
|
+
...err.next_tool_omitted_reason ? { next_tool_omitted_reason: err.next_tool_omitted_reason } : {}
|
|
1186
1483
|
};
|
|
1187
1484
|
}
|
|
1188
|
-
if (err instanceof
|
|
1485
|
+
if (err instanceof edge.HavenSigningError) {
|
|
1189
1486
|
return { success: false, code: err.code, message: err.message };
|
|
1190
1487
|
}
|
|
1191
|
-
if (err instanceof
|
|
1488
|
+
if (err instanceof edge.HavenApiError) {
|
|
1192
1489
|
return { success: false, code: err.code, message: err.message, statusCode: err.statusCode };
|
|
1193
1490
|
}
|
|
1194
|
-
if (err instanceof
|
|
1491
|
+
if (err instanceof edge.HavenError) {
|
|
1195
1492
|
return {
|
|
1196
1493
|
success: false,
|
|
1197
1494
|
code: err.code,
|
|
1198
1495
|
message: err.message,
|
|
1199
1496
|
statusCode: err.statusCode,
|
|
1200
1497
|
paymentId: err.paymentId,
|
|
1201
|
-
...err.code ===
|
|
1202
|
-
next_action:
|
|
1498
|
+
...err.code === edge.AgentPaymentFailureCode.PaymentWindowExpired ? {
|
|
1499
|
+
next_action: edge.AgentPaymentNextAction.PaymentWindowExpired,
|
|
1203
1500
|
retry_with_new_quote: true,
|
|
1204
|
-
suggested_tool: "haven_pay_mcp_tool"
|
|
1501
|
+
suggested_tool: "haven_pay_mcp_tool",
|
|
1502
|
+
// #3103: same omission as the hosted window-expired helper.
|
|
1503
|
+
...nextStepWireFields(signerRefusalStep({
|
|
1504
|
+
nextAction: edge.AgentPaymentNextAction.PaymentWindowExpired,
|
|
1505
|
+
nextTool: null,
|
|
1506
|
+
nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow (suggested_tool names the MCP one)"
|
|
1507
|
+
}))
|
|
1205
1508
|
} : {}
|
|
1206
1509
|
};
|
|
1207
1510
|
}
|
|
@@ -1213,6 +1516,17 @@ function normalizeError(err) {
|
|
|
1213
1516
|
}
|
|
1214
1517
|
|
|
1215
1518
|
// src/consent.ts
|
|
1519
|
+
var toolSummaries = {
|
|
1520
|
+
haven_sign: "signs one Haven-prepared payment (a direct-payment UserOp from your own account whose only call redeems a delegation made to it, or an x402 funding intent against a Haven-signed context)",
|
|
1521
|
+
haven_x402_sign_header: "builds and signs the EIP-3009 merchant payment header for an x402 payment",
|
|
1522
|
+
haven_sign_x402: "the two above in one call: funding payload + merchant header",
|
|
1523
|
+
haven_sign_sweep_delegate: "signs a Haven-prepared gasless sweep of stranded funds back to your own account"
|
|
1524
|
+
};
|
|
1525
|
+
var CONNECTOR_DOCTOR_HINT = [
|
|
1526
|
+
"Wired by the Haven connector (Claude Desktop, Codex, ...)? Diagnose and repair with: npx @haven_ai/connect --doctor",
|
|
1527
|
+
" (the doctor shows this as a failed 'Signer stdio handshake' check; the connector's setup outcome reports it as",
|
|
1528
|
+
" local_signer_ack_required; the repair is the connector's --ack-local-tools)."
|
|
1529
|
+
].join("\n");
|
|
1216
1530
|
var SIGNER_ACK_ENV = "HAVEN_SIGNER_ACK";
|
|
1217
1531
|
var SIGNER_CONSENT_SURFACE_VERSION = 2;
|
|
1218
1532
|
function computeSignerConsentHash(input) {
|
|
@@ -1248,13 +1562,13 @@ function renderSignerConsentBlock(input, hash) {
|
|
|
1248
1562
|
lines.push("payload from Haven, authenticated with the agent credential stored next to");
|
|
1249
1563
|
lines.push("this signer's key file - it never sends the key, a signature, or anything");
|
|
1250
1564
|
lines.push("else outbound, and it cannot show a live allowance summary.");
|
|
1251
|
-
lines.push("
|
|
1252
|
-
lines.push("pause or revoke agent authority outside this signer.");
|
|
1565
|
+
lines.push("The agent's signed budget delegation is the real spend gate, and the wallet");
|
|
1566
|
+
lines.push("owner can pause or revoke agent authority outside this signer.");
|
|
1253
1567
|
lines.push("");
|
|
1254
|
-
lines.push("Tools this signer will expose to your agent runtime
|
|
1568
|
+
lines.push("Tools this signer will expose to your agent runtime (one line each; the");
|
|
1569
|
+
lines.push("full agent-facing descriptions are what the runtime sees):");
|
|
1255
1570
|
for (const name of input.toolNames) {
|
|
1256
|
-
lines.push(` - ${name}`);
|
|
1257
|
-
lines.push(` ${toolDescriptions[name]}`);
|
|
1571
|
+
lines.push(` - ${name}: ${toolSummaries[name]}`);
|
|
1258
1572
|
}
|
|
1259
1573
|
lines.push("");
|
|
1260
1574
|
lines.push("A local audit entry is appended for every signing operation. Audit entries");
|
|
@@ -1268,6 +1582,8 @@ function renderSignerConsentBlock(input, hash) {
|
|
|
1268
1582
|
lines.push(" - re-run with --ack to write the acknowledgement next to your");
|
|
1269
1583
|
lines.push(" credential file (sidecar <credentials>.signer-ack.json).");
|
|
1270
1584
|
lines.push("");
|
|
1585
|
+
lines.push(CONNECTOR_DOCTOR_HINT);
|
|
1586
|
+
lines.push("");
|
|
1271
1587
|
lines.push("------------------------------------------------------------");
|
|
1272
1588
|
lines.push("");
|
|
1273
1589
|
return lines.join("\n");
|
|
@@ -1331,7 +1647,7 @@ async function writeAckFile(path$1, hash) {
|
|
|
1331
1647
|
);
|
|
1332
1648
|
}
|
|
1333
1649
|
var SIGNER_NAME = "@haven_ai/signer";
|
|
1334
|
-
var SIGNER_VERSION = "0.
|
|
1650
|
+
var SIGNER_VERSION = "0.5.0-alpha.1";
|
|
1335
1651
|
async function resolveEdgeSigner(options = {}) {
|
|
1336
1652
|
const { signer } = await resolveSignerRuntime(options);
|
|
1337
1653
|
return signer;
|
|
@@ -1373,13 +1689,17 @@ function buildSignerMcpServer(signer, options = {}) {
|
|
|
1373
1689
|
accountAddress: options.credentials?.accountAddress,
|
|
1374
1690
|
chainId: options.credentials?.chainId
|
|
1375
1691
|
},
|
|
1376
|
-
// #1263: the payment_id signing path — the ONLY network
|
|
1377
|
-
//
|
|
1692
|
+
// #1263: the payment_id signing path — the ONLY network path this server
|
|
1693
|
+
// has: up to two authenticated reads of a signing context from Haven (x402,
|
|
1694
|
+
// then direct for haven_sign, #3271), using
|
|
1378
1695
|
// the agent identity the connector stores next to the signer credential.
|
|
1379
1696
|
// The signer CORE stays network-free; fetched bytes still pass the same
|
|
1380
1697
|
// binding verification + digest re-derivation as tool-argument bytes.
|
|
1381
1698
|
signContext: {
|
|
1382
|
-
loadIdentity: () => loadHavenIdentity(credentialsPath)
|
|
1699
|
+
loadIdentity: () => loadHavenIdentity(credentialsPath),
|
|
1700
|
+
// #3303: the backend refuses a signer below a minimum it has set, at
|
|
1701
|
+
// sign-context — the one place this signer meets it.
|
|
1702
|
+
clientIdentity: edge.havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
|
|
1383
1703
|
}
|
|
1384
1704
|
});
|
|
1385
1705
|
const registerTool = server.tool.bind(server);
|
|
@@ -1394,9 +1714,9 @@ function buildSignerMcpServer(signer, options = {}) {
|
|
|
1394
1714
|
return server;
|
|
1395
1715
|
}
|
|
1396
1716
|
function assertSupportedNodeVersion(nodeVersion = process.versions.node) {
|
|
1397
|
-
if (
|
|
1717
|
+
if (edge.isSupportedNodeVersion(nodeVersion)) return;
|
|
1398
1718
|
const err = new Error(
|
|
1399
|
-
|
|
1719
|
+
edge.unsupportedNodeVersionMessage({ subject: "The Haven signer", nodeVersion })
|
|
1400
1720
|
);
|
|
1401
1721
|
err.code = "HAVEN_SIGNER_UNSUPPORTED_NODE";
|
|
1402
1722
|
throw err;
|
|
@@ -1407,7 +1727,7 @@ async function runSignerStdioServer(options = {}) {
|
|
|
1407
1727
|
const decision = await runSignerConsentGate(signer, credentials, options);
|
|
1408
1728
|
if (!decision.ok) {
|
|
1409
1729
|
const err = new Error(
|
|
1410
|
-
decision.reason === "env_var_mismatch" ? "Haven edge signer consent acknowledgement does not match the current configuration." : "Haven edge signer requires a one-time consent acknowledgement before starting."
|
|
1730
|
+
(decision.reason === "env_var_mismatch" ? "Haven edge signer consent acknowledgement does not match the current configuration." : "Haven edge signer requires a one-time consent acknowledgement before starting.") + " See the consent block above; if the Haven connector wired this signer, run: npx @haven_ai/connect --doctor"
|
|
1411
1731
|
);
|
|
1412
1732
|
err.code = "HAVEN_SIGNER_NO_CONSENT";
|
|
1413
1733
|
throw err;
|
|
@@ -1441,8 +1761,15 @@ function toMcpResult(payload) {
|
|
|
1441
1761
|
};
|
|
1442
1762
|
}
|
|
1443
1763
|
|
|
1764
|
+
Object.defineProperty(exports, "deriveDelegateAccountAddress", {
|
|
1765
|
+
enumerable: true,
|
|
1766
|
+
get: function () { return edge.deriveDelegateAccountAddress; }
|
|
1767
|
+
});
|
|
1768
|
+
exports.AUDIT_ROTATE_BYTES = AUDIT_ROTATE_BYTES;
|
|
1769
|
+
exports.OWNER_ONLY_MODE = OWNER_ONLY_MODE;
|
|
1444
1770
|
exports.SIGNER_ACK_ENV = SIGNER_ACK_ENV;
|
|
1445
1771
|
exports.SIGNER_CAPABILITY_KEY = SIGNER_CAPABILITY_KEY;
|
|
1772
|
+
exports.SIGNER_HOSTED_HANDOFF_SHAPES = SIGNER_HOSTED_HANDOFF_SHAPES;
|
|
1446
1773
|
exports.SIGNER_NAME = SIGNER_NAME;
|
|
1447
1774
|
exports.SIGNER_VERSION = SIGNER_VERSION;
|
|
1448
1775
|
exports.SUPPORTED_SWEEP_BINDING_VERSIONS = SUPPORTED_SWEEP_BINDING_VERSIONS;
|
|
@@ -1460,6 +1787,7 @@ exports.defaultSigningAuditPath = defaultSigningAuditPath;
|
|
|
1460
1787
|
exports.ensureSignerConsent = ensureSignerConsent;
|
|
1461
1788
|
exports.hashPayloadForAudit = hashPayloadForAudit;
|
|
1462
1789
|
exports.loadSignerCredentials = loadSignerCredentials;
|
|
1790
|
+
exports.permissiveMode = permissiveMode;
|
|
1463
1791
|
exports.readAccountAddressEnv = readAccountAddressEnv;
|
|
1464
1792
|
exports.readAccountAddressField = readAccountAddressField;
|
|
1465
1793
|
exports.renderSignerConsentBlock = renderSignerConsentBlock;
|
|
@@ -1470,8 +1798,10 @@ exports.runSignerStdioServer = runSignerStdioServer;
|
|
|
1470
1798
|
exports.signerCapabilityAdvertisement = signerCapabilityAdvertisement;
|
|
1471
1799
|
exports.signerCompatibility = signerCompatibility;
|
|
1472
1800
|
exports.signerInstructions = signerInstructions;
|
|
1801
|
+
exports.tightenIfFilePermissive = tightenIfFilePermissive;
|
|
1473
1802
|
exports.toolDescriptions = toolDescriptions;
|
|
1474
1803
|
exports.toolSchemas = toolSchemas;
|
|
1475
1804
|
exports.warnIfCredentialFilePermissive = warnIfCredentialFilePermissive;
|
|
1805
|
+
exports.warnIfFilePermissive = warnIfFilePermissive;
|
|
1476
1806
|
//# sourceMappingURL=index.cjs.map
|
|
1477
1807
|
//# sourceMappingURL=index.cjs.map
|