@haven_ai/signer 0.4.0-alpha.0 → 0.6.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -35
- package/dist/cli.cjs +1516 -1055
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1681 -1220
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +963 -520
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +216 -128
- package/dist/index.d.ts +216 -128
- package/dist/index.js +886 -451
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,145 +1,17 @@
|
|
|
1
1
|
import { randomUUID, createHash } from 'crypto';
|
|
2
2
|
import { recoverTypedDataAddress, hashTypedData, hashMessage } from 'viem';
|
|
3
3
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import {
|
|
4
|
+
import { createNextStepBuilder, addressFromKey, HavenSigningError, buildSweepTypedData, selectStandardPaymentOption, HavenApiError, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, isSettlementChildTypedData, chainIdForNetwork, assertOwnSettlementChild, isPackedUserOperationTypedData, assertUserOpTypedDataBinding, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, buildSweepAuthorizationMessage, verifySignature, x402AuthorizationAmount, SignerRefusalCode, connectorRerunCommand, SIGNER_UPDATE_FALLBACK, HavenUnsupportedSignerVersionError, buildX402ExpectedMessage, DIRECT_SIGN_CONTEXT_VERSION, AgentPaymentNextAction, HavenError, AgentPaymentFailureCode, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, HavenUserOpBindingError, assertOwnTaskChild, assertOwnTaskBudgetCloseUserOp, readClientUpdate, HAVEN_CLIENT_HEADER } from '@haven_ai/sdk/edge';
|
|
5
|
+
export { deriveDelegateAccountAddress } from '@haven_ai/sdk/edge';
|
|
6
|
+
import { stat, lstat, chmod, readFile, mkdir, rename, appendFile, writeFile } from 'fs/promises';
|
|
7
|
+
import { resolve, dirname, join } from 'path';
|
|
8
|
+
import { z } from 'zod';
|
|
7
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
11
|
import { homedir } from 'os';
|
|
10
|
-
import { resolve, dirname, join } from 'path';
|
|
11
12
|
import { z as z$1 } from 'zod/v3';
|
|
12
|
-
import { z } from 'zod';
|
|
13
13
|
|
|
14
14
|
// src/core.ts
|
|
15
|
-
var DELEGATION_MANAGER = "0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3";
|
|
16
|
-
var CAVEAT_ENFORCERS = {
|
|
17
|
-
erc20TransferAmount: "0xf100b0819427117EcF76Ed94B358B1A5b5C6D2Fc",
|
|
18
|
-
allowedCalldata: "0xc2b0d624c1c4319760C96503BA27C347F3260f55",
|
|
19
|
-
timestamp: "0x1046bb45C8d673d4ea75321280DB34899413c069"};
|
|
20
|
-
var MAX_SETTLEMENT_WINDOW_SECONDS = 600;
|
|
21
|
-
function isSettlementChildTypedData(value) {
|
|
22
|
-
const td = value;
|
|
23
|
-
return !!td && td.primaryType === "Delegation" && typeof td.domain?.verifyingContract === "string" && Array.isArray(td.message?.caveats);
|
|
24
|
-
}
|
|
25
|
-
function same(a, b) {
|
|
26
|
-
return !!a && !!b && a.toLowerCase() === b.toLowerCase();
|
|
27
|
-
}
|
|
28
|
-
function hex(value) {
|
|
29
|
-
return value.startsWith("0x") ? value.slice(2).toLowerCase() : value.toLowerCase();
|
|
30
|
-
}
|
|
31
|
-
function slice(terms, start, end) {
|
|
32
|
-
const body = hex(terms);
|
|
33
|
-
return end === void 0 ? body.slice(start * 2) : body.slice(start * 2, end * 2);
|
|
34
|
-
}
|
|
35
|
-
function findCaveat(caveats, enforcer) {
|
|
36
|
-
return caveats.find((c) => same(c.enforcer, enforcer));
|
|
37
|
-
}
|
|
38
|
-
function refuse(what, detail) {
|
|
39
|
-
throw new HavenSigningError(
|
|
40
|
-
`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.`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
function chainIdForNetwork(network) {
|
|
44
|
-
if (!network) return void 0;
|
|
45
|
-
const caip = /^eip155:(\d+)$/.exec(network);
|
|
46
|
-
if (caip) return Number(caip[1]);
|
|
47
|
-
if (network === "base") return 8453;
|
|
48
|
-
if (network === "base-sepolia") return 84532;
|
|
49
|
-
return void 0;
|
|
50
|
-
}
|
|
51
|
-
function verifySettlementChild(typedData, expected, now = Date.now()) {
|
|
52
|
-
if (typedData.primaryType !== "Delegation") {
|
|
53
|
-
refuse("it is not a Delegation payload", `primaryType was '${typedData.primaryType}'.`);
|
|
54
|
-
}
|
|
55
|
-
if (!same(typedData.domain?.verifyingContract, DELEGATION_MANAGER)) {
|
|
56
|
-
refuse(
|
|
57
|
-
"the EIP-712 domain names an unknown DelegationManager",
|
|
58
|
-
`Expected ${DELEGATION_MANAGER}, got ${typedData.domain?.verifyingContract}.`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
const domainChain = Number(typedData.domain?.chainId);
|
|
62
|
-
if (!Number.isFinite(domainChain) || domainChain !== expected.chainId) {
|
|
63
|
-
refuse(
|
|
64
|
-
"it is scoped to the wrong chain",
|
|
65
|
-
`The expected context says chain ${expected.chainId}; the child says ${typedData.domain?.chainId}.`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
const caveats = typedData.message?.caveats ?? [];
|
|
69
|
-
if (caveats.length === 0) {
|
|
70
|
-
refuse("it carries no caveats at all", "An unconstrained delegation is not a payment.");
|
|
71
|
-
}
|
|
72
|
-
const transfer = findCaveat(caveats, CAVEAT_ENFORCERS.erc20TransferAmount);
|
|
73
|
-
if (!transfer) {
|
|
74
|
-
refuse(
|
|
75
|
-
"it has no ERC-20 transfer-amount caveat",
|
|
76
|
-
"Without it the delegation is not bounded to an amount."
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
const token = `0x${slice(transfer.terms, 0, 20)}`;
|
|
80
|
-
if (!same(token, expected.asset)) {
|
|
81
|
-
refuse("it spends a different token", `Expected ${expected.asset}, child pins ${token}.`);
|
|
82
|
-
}
|
|
83
|
-
const amount = BigInt(`0x${slice(transfer.terms, 20, 52)}`);
|
|
84
|
-
if (amount !== BigInt(expected.amount)) {
|
|
85
|
-
refuse(
|
|
86
|
-
"the amount does not match",
|
|
87
|
-
`The expected context says ${expected.amount}; the child allows ${amount.toString()}.`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
const calldata = findCaveat(caveats, CAVEAT_ENFORCERS.allowedCalldata);
|
|
91
|
-
if (!calldata) {
|
|
92
|
-
refuse(
|
|
93
|
-
"it has no payee pin",
|
|
94
|
-
"Without an allowed-calldata caveat the merchant could redeem it to any address."
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
const startIndex = BigInt(`0x${slice(calldata.terms, 0, 32)}`);
|
|
98
|
-
if (startIndex !== 4n) {
|
|
99
|
-
refuse(
|
|
100
|
-
"the payee pin points at the wrong calldata offset",
|
|
101
|
-
`Expected offset 4 (the transfer's \`to\` word), got ${startIndex.toString()}.`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
const paddedPayee = slice(calldata.terms, 32, 64);
|
|
105
|
-
if (paddedPayee.slice(0, 24) !== "0".repeat(24)) {
|
|
106
|
-
refuse("the pinned payee is not a plain address", "Its 32-byte word is not a padded address.");
|
|
107
|
-
}
|
|
108
|
-
const payee = `0x${paddedPayee.slice(24)}`;
|
|
109
|
-
if (!same(payee, expected.merchantTo)) {
|
|
110
|
-
refuse(
|
|
111
|
-
"it pays a different address than Haven declared",
|
|
112
|
-
`The expected context says ${expected.merchantTo}; the child pins ${payee}.`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
const timestamp = findCaveat(caveats, CAVEAT_ENFORCERS.timestamp);
|
|
116
|
-
if (!timestamp) {
|
|
117
|
-
refuse("it never expires", "A settlement child without a timestamp caveat is open-ended.");
|
|
118
|
-
}
|
|
119
|
-
const beforeThreshold = Number(BigInt(`0x${slice(timestamp.terms, 16, 32)}`));
|
|
120
|
-
if (beforeThreshold <= 0) {
|
|
121
|
-
refuse("its expiry is unset", "The timestamp caveat has no upper bound.");
|
|
122
|
-
}
|
|
123
|
-
const nowSec = Math.floor(now / 1e3);
|
|
124
|
-
if (beforeThreshold <= nowSec) {
|
|
125
|
-
refuse("it has already expired", `Expiry ${beforeThreshold} is not in the future.`);
|
|
126
|
-
}
|
|
127
|
-
if (beforeThreshold > nowSec + MAX_SETTLEMENT_WINDOW_SECONDS) {
|
|
128
|
-
refuse(
|
|
129
|
-
"its window is longer than a settlement may live",
|
|
130
|
-
`Expiry is ${beforeThreshold - nowSec}s out; the ceiling is ${MAX_SETTLEMENT_WINDOW_SECONDS}s.`
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
if (expected.expiresAt) {
|
|
134
|
-
const declared = Math.floor(Date.parse(expected.expiresAt) / 1e3);
|
|
135
|
-
if (Number.isFinite(declared) && beforeThreshold > declared) {
|
|
136
|
-
refuse(
|
|
137
|
-
"it outlives the payment window Haven declared",
|
|
138
|
-
`The expected context expires at ${expected.expiresAt}; the child lives to ${beforeThreshold}.`
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
15
|
function createEdgeSigner(delegateKey, options = {}) {
|
|
144
16
|
let delegateAddress;
|
|
145
17
|
try {
|
|
@@ -159,41 +31,28 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
159
31
|
if (!oldest.done) retiredX402Bindings.delete(oldest.value);
|
|
160
32
|
}
|
|
161
33
|
}
|
|
162
|
-
function signAndVerify(hash) {
|
|
163
|
-
const signature = signHash(delegateKey, hash);
|
|
164
|
-
if (!verifySignature(hash, signature, delegateAddress)) {
|
|
165
|
-
throw new HavenSigningError(
|
|
166
|
-
"Local signature verification failed \u2014 recovered address does not match the delegate key."
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
return signature;
|
|
170
|
-
}
|
|
171
34
|
return {
|
|
172
35
|
delegateAddress,
|
|
173
|
-
signPaymentHash(hash) {
|
|
174
|
-
return signAndVerify(hash);
|
|
175
|
-
},
|
|
176
36
|
async signDelegationTypedData(typedData) {
|
|
177
37
|
const account = privateKeyToAccount(delegateKey);
|
|
178
38
|
return account.signTypedData(typedData);
|
|
179
39
|
},
|
|
180
|
-
signX402FundingHash(hash, expected) {
|
|
181
|
-
assertExpectedBinding(hash, expected, options.x402BindingSigner, "hash");
|
|
182
|
-
assertPayerMatchesDelegate(expected, delegateAddress, options.agentId);
|
|
183
|
-
const signature = signAndVerify(hash);
|
|
184
|
-
const x402Binding = randomUUID();
|
|
185
|
-
x402Bindings.set(x402Binding, { ...expected });
|
|
186
|
-
return { signature, x402Binding };
|
|
187
|
-
},
|
|
188
40
|
async signX402FundingTypedData(typedData, expected) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
41
|
+
if (expected?.auth) {
|
|
42
|
+
assertSupportedBindingVersion(
|
|
43
|
+
expected.auth.version,
|
|
44
|
+
SUPPORTED_X402_EXPECTED_VERSIONS,
|
|
45
|
+
"x402 expected context"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (!typedData) {
|
|
193
49
|
throw new HavenSigningError(
|
|
194
|
-
"x402
|
|
50
|
+
"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."
|
|
195
51
|
);
|
|
196
52
|
}
|
|
53
|
+
const digest = hashTypedData(typedData);
|
|
54
|
+
assertExpectedBinding(digest, expected, options.x402BindingSigner);
|
|
55
|
+
assertPayerMatchesDelegate(expected, delegateAddress, options.agentId);
|
|
197
56
|
if (isSettlementChildTypedData(typedData)) {
|
|
198
57
|
const settlementChainId = chainIdForNetwork(expected.network);
|
|
199
58
|
if (settlementChainId === void 0) {
|
|
@@ -201,7 +60,7 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
201
60
|
`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.`
|
|
202
61
|
);
|
|
203
62
|
}
|
|
204
|
-
|
|
63
|
+
assertOwnSettlementChild(typedData, {
|
|
205
64
|
merchantTo: expected.merchantTo,
|
|
206
65
|
amount: expected.amount,
|
|
207
66
|
asset: expected.asset,
|
|
@@ -211,7 +70,21 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
211
70
|
// signed another" class this file exists to catch (#1455 review).
|
|
212
71
|
chainId: settlementChainId,
|
|
213
72
|
expiresAt: expected.expiresAt
|
|
73
|
+
}, delegateAddress);
|
|
74
|
+
} else if (isPackedUserOperationTypedData(typedData)) {
|
|
75
|
+
assertUserOpTypedDataBinding(typedData, expected.payloadHash);
|
|
76
|
+
assertBoundDirectPaymentUserOp(typedData, delegateAddress);
|
|
77
|
+
assertFundingLegPaysDelegate(typedData, {
|
|
78
|
+
delegateAddress,
|
|
79
|
+
asset: expected.asset,
|
|
80
|
+
amount: expected.amount
|
|
214
81
|
});
|
|
82
|
+
} else {
|
|
83
|
+
throw new HavenTypedDataRefusedError(
|
|
84
|
+
`This signer's x402 arm signs only a funding-leg PackedUserOperation or an erc7710 settlement Delegation; this typed data is '${String(
|
|
85
|
+
typedData.primaryType
|
|
86
|
+
)}'. Refusing, even though its digest is bound by the expected context.`
|
|
87
|
+
);
|
|
215
88
|
}
|
|
216
89
|
const account = privateKeyToAccount(delegateKey);
|
|
217
90
|
const signature = await account.signTypedData(
|
|
@@ -236,16 +109,19 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
236
109
|
);
|
|
237
110
|
}
|
|
238
111
|
throw new HavenSigningError(
|
|
239
|
-
"x402 funding binding is required before signing a merchant header. Sign the hosted funding
|
|
112
|
+
"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."
|
|
240
113
|
);
|
|
241
114
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
115
|
+
const assertWindowOpenOrRetire = () => {
|
|
116
|
+
try {
|
|
117
|
+
assertX402PaymentWindowOpen(expected);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
x402Bindings.delete(x402Binding);
|
|
120
|
+
retireX402Binding(x402Binding, "window_expired");
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
assertWindowOpenOrRetire();
|
|
249
125
|
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
250
126
|
if (!option) {
|
|
251
127
|
throw new HavenApiError(
|
|
@@ -256,6 +132,8 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
256
132
|
assertX402MatchesExpected(paymentRequired, option, expected);
|
|
257
133
|
const account = privateKeyToAccount(delegateKey);
|
|
258
134
|
const requirements = toStandardPaymentRequirements(paymentRequired, option);
|
|
135
|
+
const { exact } = await loadX402Schemes();
|
|
136
|
+
assertWindowOpenOrRetire();
|
|
259
137
|
const header = await exact.evm.createPaymentHeader(
|
|
260
138
|
account,
|
|
261
139
|
paymentRequired.x402Version,
|
|
@@ -290,7 +168,7 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
290
168
|
}
|
|
291
169
|
if (expectedSafe && !sameAddress(authorization.to, expectedSafe)) {
|
|
292
170
|
throw new HavenSigningError(
|
|
293
|
-
"Sweep authorization `to` does not match the
|
|
171
|
+
"Sweep authorization `to` does not match the agent's account (Haven wallet) in the local credential."
|
|
294
172
|
);
|
|
295
173
|
}
|
|
296
174
|
const typedData = buildSweepTypedData(authorization);
|
|
@@ -369,7 +247,7 @@ function assertExpectedShape(expected) {
|
|
|
369
247
|
throw new HavenSigningError("x402 expected funding context is required before signing a merchant header.");
|
|
370
248
|
}
|
|
371
249
|
}
|
|
372
|
-
var SUPPORTED_X402_EXPECTED_VERSIONS = [
|
|
250
|
+
var SUPPORTED_X402_EXPECTED_VERSIONS = [2, 3];
|
|
373
251
|
var SUPPORTED_SWEEP_BINDING_VERSIONS = [1];
|
|
374
252
|
function assertSupportedBindingVersion(received, supported, context) {
|
|
375
253
|
if (supported.includes(received)) return;
|
|
@@ -386,11 +264,11 @@ function assertSupportedBindingVersion(received, supported, context) {
|
|
|
386
264
|
fallback
|
|
387
265
|
);
|
|
388
266
|
}
|
|
389
|
-
function assertExpectedBinding(
|
|
267
|
+
function assertExpectedBinding(typedDataDigest, expected, trustedSigner) {
|
|
390
268
|
assertExpectedShape(expected);
|
|
391
269
|
if (!trustedSigner) {
|
|
392
270
|
throw new HavenSigningError(
|
|
393
|
-
"x402 expected-context verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing x402 funding
|
|
271
|
+
"x402 expected-context verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing x402 funding payloads."
|
|
394
272
|
);
|
|
395
273
|
}
|
|
396
274
|
if (expected.auth) {
|
|
@@ -400,17 +278,14 @@ function assertExpectedBinding(payloadHash, expected, trustedSigner, mode = "has
|
|
|
400
278
|
"x402 expected context"
|
|
401
279
|
);
|
|
402
280
|
}
|
|
403
|
-
if (expected.
|
|
404
|
-
throw new HavenSigningError("x402 expected context does not match the funding hash being signed.");
|
|
405
|
-
}
|
|
406
|
-
if (mode === "hash" && expected.typedDataHash) {
|
|
281
|
+
if (!expected.typedDataHash) {
|
|
407
282
|
throw new HavenSigningError(
|
|
408
|
-
"
|
|
283
|
+
"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."
|
|
409
284
|
);
|
|
410
285
|
}
|
|
411
|
-
if (
|
|
286
|
+
if (expected.typedDataHash.toLowerCase() !== typedDataDigest.toLowerCase()) {
|
|
412
287
|
throw new HavenSigningError(
|
|
413
|
-
"Refusing to sign
|
|
288
|
+
"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."
|
|
414
289
|
);
|
|
415
290
|
}
|
|
416
291
|
const message = buildX402ExpectedMessage({
|
|
@@ -426,7 +301,7 @@ function assertExpectedBinding(payloadHash, expected, trustedSigner, mode = "has
|
|
|
426
301
|
payerDelegate: expected.payerDelegate,
|
|
427
302
|
payerAgentId: expected.payerAgentId
|
|
428
303
|
});
|
|
429
|
-
const expectedVersion = expected.payerDelegate ? 3 :
|
|
304
|
+
const expectedVersion = expected.payerDelegate ? 3 : 2;
|
|
430
305
|
if (expected.auth?.version !== expectedVersion || expected.auth.message !== message) {
|
|
431
306
|
throw new HavenSigningError("x402 expected context authentication message is invalid.");
|
|
432
307
|
}
|
|
@@ -446,6 +321,15 @@ function assertPayerMatchesDelegate(expected, delegateAddress, localAgentId) {
|
|
|
446
321
|
`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.`
|
|
447
322
|
);
|
|
448
323
|
}
|
|
324
|
+
async function loadX402Schemes() {
|
|
325
|
+
try {
|
|
326
|
+
return await import('x402/schemes');
|
|
327
|
+
} catch (err) {
|
|
328
|
+
throw new HavenSigningError(
|
|
329
|
+
`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`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
449
333
|
function assertX402PaymentWindowOpen(expected) {
|
|
450
334
|
if (!expected.expiresAt) return;
|
|
451
335
|
const expiresAtMs = Date.parse(expected.expiresAt);
|
|
@@ -464,11 +348,442 @@ function assertX402PaymentWindowOpen(expected) {
|
|
|
464
348
|
function sameAddress(a, b) {
|
|
465
349
|
return a.toLowerCase() === b.toLowerCase();
|
|
466
350
|
}
|
|
351
|
+
var SIGNER_HOSTED_HANDOFF_SHAPES = {
|
|
352
|
+
haven_get_payment_status: { payment_id: z.string().min(1) }
|
|
353
|
+
};
|
|
354
|
+
function target(shape) {
|
|
355
|
+
const schema = z.object(shape).strict();
|
|
356
|
+
return {
|
|
357
|
+
role: "hosted",
|
|
358
|
+
validate: (input) => {
|
|
359
|
+
const r = schema.safeParse(input ?? {});
|
|
360
|
+
return r.success ? null : r.error.errors.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join("; ");
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
var SIGNER_NEXT_STEP_TARGETS = {
|
|
365
|
+
haven_get_payment_status: target(SIGNER_HOSTED_HANDOFF_SHAPES.haven_get_payment_status)
|
|
366
|
+
};
|
|
367
|
+
var nextStep = createNextStepBuilder(SIGNER_NEXT_STEP_TARGETS);
|
|
368
|
+
function signerRefusalStep(input) {
|
|
369
|
+
return nextStep({ ...input, safeToContinue: false, reason: "" });
|
|
370
|
+
}
|
|
371
|
+
function nextStepWireFields(step) {
|
|
372
|
+
return {
|
|
373
|
+
...step.next_tool ? { next_tool: step.next_tool } : {},
|
|
374
|
+
...step.next_tool_server ? { next_tool_server: step.next_tool_server } : {},
|
|
375
|
+
...step.next_tool_name ? { next_tool_name: step.next_tool_name } : {},
|
|
376
|
+
...step.next_tool_server_role ? { next_tool_server_role: step.next_tool_server_role } : {},
|
|
377
|
+
...step.next_arguments ? { next_arguments: step.next_arguments } : {},
|
|
378
|
+
...step.next_tool_omitted_reason ? { next_tool_omitted_reason: step.next_tool_omitted_reason } : {}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/sign-context.ts
|
|
383
|
+
var DIRECT_RELAY_FALLBACK = "call haven_sign with payload_hash and typed_data_b64 from the haven_send / haven_pay result, passed through unchanged";
|
|
384
|
+
var HavenSignContextError = class extends HavenSigningError {
|
|
385
|
+
/**
|
|
386
|
+
* #3001 / #3010 review: the recovery path, when signing OTHER bytes is
|
|
387
|
+
* actually a remedy — a transport failure (timeout, unreachable) or a
|
|
388
|
+
* body this signer could not read. `typed_data_b64` is NOT in the default
|
|
389
|
+
* quote result since #1272: obtain it by re-running the SAME quote tool
|
|
390
|
+
* with the SAME idempotency_key plus `include_signing_payload: true`.
|
|
391
|
+
* Absent on a backend REFUSAL (an expired, executed or unsignable intent
|
|
392
|
+
* cannot be rescued by re-signing its bytes) — with one #3271 exception: a
|
|
393
|
+
* 404 from the DIRECT fetch (an older backend with no direct route), where
|
|
394
|
+
* the `haven_send` / `haven_pay` result's relay fields still work.
|
|
395
|
+
*/
|
|
396
|
+
fallback;
|
|
397
|
+
/**
|
|
398
|
+
* #3001: `AgentPaymentNextAction` values the signer already emits — the
|
|
399
|
+
* version-mismatch refusal's `stop_and_tell_user` for the classes where
|
|
400
|
+
* retrying the same call cannot help, and `payment_window_expired` (with
|
|
401
|
+
* `retry_with_new_quote`) for the x402 fetch's 410 / `expired`, exactly as
|
|
402
|
+
* the plain `HavenError` branch already does for that code. On the direct
|
|
403
|
+
* fetch (#3271) only `error_code: 'expired'` means an expired window, with
|
|
404
|
+
* no `retry_with_new_quote`; a bare 410 there is a retired-rail tombstone.
|
|
405
|
+
*/
|
|
406
|
+
next_action;
|
|
407
|
+
/**
|
|
408
|
+
* #3103 (epic #3105, decisions 1 and 3): the typed next step beside the
|
|
409
|
+
* action. A refusal Haven made (`SIGN_CONTEXT_REFUSED`, not expired) names
|
|
410
|
+
* the hosted status read with the payment id — except a 404 from the direct
|
|
411
|
+
* fetch (#3271), which names none and points at the `typed_data_b64`
|
|
412
|
+
* relay; a transport failure or a
|
|
413
|
+
* malformed body names no tool — the remedy is re-running the SAME quote
|
|
414
|
+
* tool with `include_signing_payload: true` (x402), or the payment
|
|
415
|
+
* result's `typed_data_b64` relay (direct, #3271) — and an expired window names
|
|
416
|
+
* none either (which quote tool depends on the flow). Never null: a step
|
|
417
|
+
* with no tool carries `next_tool_omitted_reason`. Additive: the class is
|
|
418
|
+
* not exported from the package index — the published surface is the
|
|
419
|
+
* `ToolFailure` envelope `tools.ts` builds from these fields — and every
|
|
420
|
+
* constructor call site is in this file.
|
|
421
|
+
*/
|
|
422
|
+
next_tool;
|
|
423
|
+
next_tool_server;
|
|
424
|
+
next_tool_name;
|
|
425
|
+
next_tool_server_role;
|
|
426
|
+
next_arguments;
|
|
427
|
+
next_tool_omitted_reason;
|
|
428
|
+
retry_with_new_quote;
|
|
429
|
+
/** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
|
|
430
|
+
http_status;
|
|
431
|
+
/** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
|
|
432
|
+
backend_error_code;
|
|
433
|
+
/**
|
|
434
|
+
* #3303: on a 426 `client_outdated` refusal, the backend's update hint —
|
|
435
|
+
* this signer is below the minimum the deployment set, and
|
|
436
|
+
* `client_update.upgrade_command` is what updates it.
|
|
437
|
+
*/
|
|
438
|
+
client_update;
|
|
439
|
+
constructor(message, code, refusal, paymentId, flow = "x402") {
|
|
440
|
+
super(message);
|
|
441
|
+
this.code = code;
|
|
442
|
+
this.name = "HavenSignContextError";
|
|
443
|
+
let step;
|
|
444
|
+
if (code === "SIGN_CONTEXT_REFUSED") {
|
|
445
|
+
this.http_status = refusal?.httpStatus;
|
|
446
|
+
this.backend_error_code = refusal?.errorCode;
|
|
447
|
+
if (refusal?.errorCode === "client_outdated") {
|
|
448
|
+
this.client_update = refusal.clientUpdate;
|
|
449
|
+
this.next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
450
|
+
step = signerRefusalStep({
|
|
451
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
452
|
+
nextTool: null,
|
|
453
|
+
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`
|
|
454
|
+
});
|
|
455
|
+
} else if (flow === "direct" && refusal?.httpStatus === 404) {
|
|
456
|
+
this.fallback = "typed_data_b64";
|
|
457
|
+
this.next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
458
|
+
step = signerRefusalStep({
|
|
459
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
460
|
+
nextTool: null,
|
|
461
|
+
nextToolOmittedReason: DIRECT_RELAY_FALLBACK
|
|
462
|
+
});
|
|
463
|
+
} else if (flow === "direct" && refusal?.errorCode === "expired") {
|
|
464
|
+
this.next_action = AgentPaymentNextAction.PaymentWindowExpired;
|
|
465
|
+
step = signerRefusalStep({
|
|
466
|
+
nextAction: AgentPaymentNextAction.PaymentWindowExpired,
|
|
467
|
+
nextTool: null,
|
|
468
|
+
nextToolOmittedReason: "call haven_send / haven_pay again with the same idempotency_key \u2014 the expired payment frees it"
|
|
469
|
+
});
|
|
470
|
+
} else if (flow === "x402" && (refusal?.httpStatus === 410 || refusal?.errorCode === "expired")) {
|
|
471
|
+
this.next_action = AgentPaymentNextAction.PaymentWindowExpired;
|
|
472
|
+
this.retry_with_new_quote = true;
|
|
473
|
+
step = signerRefusalStep({
|
|
474
|
+
nextAction: AgentPaymentNextAction.PaymentWindowExpired,
|
|
475
|
+
nextTool: null,
|
|
476
|
+
nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow"
|
|
477
|
+
});
|
|
478
|
+
} else if (flow === "task") {
|
|
479
|
+
this.next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
480
|
+
step = signerRefusalStep({
|
|
481
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
482
|
+
nextTool: null,
|
|
483
|
+
nextToolOmittedReason: `re-check this task budget's status (id ${paymentId}) with the hosted Haven server before retrying`
|
|
484
|
+
});
|
|
485
|
+
} else {
|
|
486
|
+
this.next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
487
|
+
step = signerRefusalStep({
|
|
488
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
489
|
+
nextTool: "haven_get_payment_status",
|
|
490
|
+
nextArguments: { payment_id: paymentId }
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
} else if (flow === "task") {
|
|
494
|
+
this.next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
495
|
+
step = signerRefusalStep({
|
|
496
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
497
|
+
nextTool: null,
|
|
498
|
+
nextToolOmittedReason: "retry the same haven_sign call with the same task_budget_id"
|
|
499
|
+
});
|
|
500
|
+
} else {
|
|
501
|
+
this.fallback = "typed_data_b64";
|
|
502
|
+
this.next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
503
|
+
step = signerRefusalStep({
|
|
504
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
505
|
+
nextTool: null,
|
|
506
|
+
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"
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
Object.assign(this, nextStepWireFields(step));
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
async function loadHavenIdentity(credentialsPath) {
|
|
513
|
+
if (!credentialsPath) return null;
|
|
514
|
+
try {
|
|
515
|
+
const raw = JSON.parse(
|
|
516
|
+
await readFile(join(dirname(credentialsPath), "identity.json"), "utf8")
|
|
517
|
+
);
|
|
518
|
+
const apiKey = typeof raw.api_key === "string" ? raw.api_key : void 0;
|
|
519
|
+
const apiUrl = typeof raw.api_url === "string" ? raw.api_url : void 0;
|
|
520
|
+
if (!apiKey || !apiUrl) return null;
|
|
521
|
+
return { apiKey, apiUrl: apiUrl.replace(/\/+$/, "") };
|
|
522
|
+
} catch {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
var SIGN_CONTEXT_TIMEOUT_MS = 15e3;
|
|
527
|
+
function signContextHeaders(identity, clientIdentity) {
|
|
528
|
+
return {
|
|
529
|
+
Authorization: `Bearer ${identity.apiKey}`,
|
|
530
|
+
...clientIdentity ? { [HAVEN_CLIENT_HEADER]: clientIdentity } : {}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function refusalOf(status, body) {
|
|
534
|
+
return {
|
|
535
|
+
httpStatus: status,
|
|
536
|
+
errorCode: typeof body.error_code === "string" ? body.error_code : void 0,
|
|
537
|
+
clientUpdate: readClientUpdate(body.client_update)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
|
|
541
|
+
let response;
|
|
542
|
+
try {
|
|
543
|
+
response = await fetchImpl(
|
|
544
|
+
`${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
|
|
545
|
+
{
|
|
546
|
+
headers: signContextHeaders(identity, clientIdentity),
|
|
547
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
552
|
+
throw new HavenSignContextError(
|
|
553
|
+
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.`,
|
|
554
|
+
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
|
|
555
|
+
void 0,
|
|
556
|
+
paymentId
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
let body;
|
|
560
|
+
try {
|
|
561
|
+
body = await response.json();
|
|
562
|
+
} catch (err) {
|
|
563
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
564
|
+
throw new HavenSignContextError(
|
|
565
|
+
`Haven did not finish sending the signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.`,
|
|
566
|
+
"SIGN_CONTEXT_TIMEOUT",
|
|
567
|
+
void 0,
|
|
568
|
+
paymentId
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
body = {};
|
|
572
|
+
}
|
|
573
|
+
if (!response.ok) {
|
|
574
|
+
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
575
|
+
throw new HavenSignContextError(
|
|
576
|
+
`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." : ""),
|
|
577
|
+
"SIGN_CONTEXT_REFUSED",
|
|
578
|
+
refusalOf(response.status, body),
|
|
579
|
+
paymentId
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
const signData = body.sign_data;
|
|
583
|
+
const x402Expected = body.x402_expected;
|
|
584
|
+
if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
|
|
585
|
+
throw new HavenSignContextError(
|
|
586
|
+
"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.",
|
|
587
|
+
"SIGN_CONTEXT_MALFORMED",
|
|
588
|
+
void 0,
|
|
589
|
+
paymentId
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
const paymentRequired = body.payment_required;
|
|
593
|
+
return {
|
|
594
|
+
paymentId: String(body.payment_id ?? paymentId),
|
|
595
|
+
payloadHash: signData.hash,
|
|
596
|
+
typedData: signData.typed_data,
|
|
597
|
+
x402Expected,
|
|
598
|
+
paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null,
|
|
599
|
+
clientUpdate: readClientUpdate(body.client_update)
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
var SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS = [DIRECT_SIGN_CONTEXT_VERSION];
|
|
603
|
+
async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
|
|
604
|
+
let response;
|
|
605
|
+
try {
|
|
606
|
+
response = await fetchImpl(
|
|
607
|
+
`${identity.apiUrl}/payments/${encodeURIComponent(paymentId)}/sign-context`,
|
|
608
|
+
{
|
|
609
|
+
headers: signContextHeaders(identity, clientIdentity),
|
|
610
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
} catch (err) {
|
|
614
|
+
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
615
|
+
throw new HavenSignContextError(
|
|
616
|
+
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.`,
|
|
617
|
+
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
|
|
618
|
+
void 0,
|
|
619
|
+
paymentId,
|
|
620
|
+
"direct"
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
let body;
|
|
624
|
+
try {
|
|
625
|
+
body = await response.json();
|
|
626
|
+
} catch (err) {
|
|
627
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
628
|
+
throw new HavenSignContextError(
|
|
629
|
+
`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.`,
|
|
630
|
+
"SIGN_CONTEXT_TIMEOUT",
|
|
631
|
+
void 0,
|
|
632
|
+
paymentId,
|
|
633
|
+
"direct"
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
body = {};
|
|
637
|
+
}
|
|
638
|
+
if (!response.ok) {
|
|
639
|
+
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
640
|
+
throw new HavenSignContextError(
|
|
641
|
+
`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." : ""),
|
|
642
|
+
"SIGN_CONTEXT_REFUSED",
|
|
643
|
+
refusalOf(response.status, body),
|
|
644
|
+
paymentId,
|
|
645
|
+
"direct"
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
const signData = body.sign_data;
|
|
649
|
+
const version = body.direct_sign_context_version;
|
|
650
|
+
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)) {
|
|
651
|
+
throw new HavenSignContextError(
|
|
652
|
+
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.",
|
|
653
|
+
"SIGN_CONTEXT_MALFORMED",
|
|
654
|
+
void 0,
|
|
655
|
+
paymentId,
|
|
656
|
+
"direct"
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
paymentId: String(body.payment_id ?? paymentId),
|
|
661
|
+
payloadHash: signData.hash,
|
|
662
|
+
typedData: signData.typed_data,
|
|
663
|
+
status: typeof body.status === "string" ? body.status : "pending_signature",
|
|
664
|
+
expiresAt: typeof body.expires_at === "string" ? body.expires_at : void 0,
|
|
665
|
+
clientUpdate: readClientUpdate(body.client_update)
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
var SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS = [1];
|
|
669
|
+
async function fetchTaskBudgetSignContext(identity, taskBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
|
|
670
|
+
let response;
|
|
671
|
+
try {
|
|
672
|
+
response = await fetchImpl(
|
|
673
|
+
`${identity.apiUrl}/task-budgets/${encodeURIComponent(taskBudgetId)}/sign-context`,
|
|
674
|
+
{
|
|
675
|
+
headers: signContextHeaders(identity, clientIdentity),
|
|
676
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
677
|
+
}
|
|
678
|
+
);
|
|
679
|
+
} catch (err) {
|
|
680
|
+
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
681
|
+
throw new HavenSignContextError(
|
|
682
|
+
timedOut ? `Haven did not answer the task-budget signing-context fetch for ${taskBudgetId} within ${timeoutMs} ms. Retry.` : `Could not reach Haven to fetch the task-budget signing context for ${taskBudgetId}: ${err instanceof Error ? err.message : String(err)}.`,
|
|
683
|
+
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
|
|
684
|
+
void 0,
|
|
685
|
+
taskBudgetId,
|
|
686
|
+
"task"
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
let body;
|
|
690
|
+
try {
|
|
691
|
+
body = await response.json();
|
|
692
|
+
} catch (err) {
|
|
693
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
694
|
+
throw new HavenSignContextError(
|
|
695
|
+
`Haven did not finish sending the task-budget signing context for ${taskBudgetId} within ${timeoutMs} ms. Retry.`,
|
|
696
|
+
"SIGN_CONTEXT_TIMEOUT",
|
|
697
|
+
void 0,
|
|
698
|
+
taskBudgetId,
|
|
699
|
+
"task"
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
body = {};
|
|
703
|
+
}
|
|
704
|
+
if (!response.ok) {
|
|
705
|
+
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
706
|
+
throw new HavenSignContextError(
|
|
707
|
+
`Haven refused the task-budget signing-context fetch for ${taskBudgetId}: ${detail}`,
|
|
708
|
+
"SIGN_CONTEXT_REFUSED",
|
|
709
|
+
refusalOf(response.status, body),
|
|
710
|
+
taskBudgetId,
|
|
711
|
+
"task"
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
const purpose = body.purpose;
|
|
715
|
+
const typedData = body.typed_data;
|
|
716
|
+
const version = body.task_sign_context_version;
|
|
717
|
+
const expected = body.expected;
|
|
718
|
+
const malformed = (detail) => {
|
|
719
|
+
throw new HavenSignContextError(
|
|
720
|
+
`The Haven task-budget sign-context response for ${taskBudgetId} is malformed: ${detail}.`,
|
|
721
|
+
"SIGN_CONTEXT_MALFORMED",
|
|
722
|
+
void 0,
|
|
723
|
+
taskBudgetId,
|
|
724
|
+
"task"
|
|
725
|
+
);
|
|
726
|
+
};
|
|
727
|
+
if (typeof version !== "number" || !SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS.includes(version)) {
|
|
728
|
+
throw new HavenSignContextError(
|
|
729
|
+
`The Haven task-budget sign-context response is version ${String(version)}, which this signer does not support (supported: ${SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.`,
|
|
730
|
+
"SIGN_CONTEXT_MALFORMED",
|
|
731
|
+
void 0,
|
|
732
|
+
taskBudgetId,
|
|
733
|
+
"task"
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
if (!typedData || typeof typedData !== "object") return malformed("missing typed_data");
|
|
737
|
+
if (!expected || typeof expected !== "object") return malformed("missing expected");
|
|
738
|
+
const e = expected;
|
|
739
|
+
if (purpose === "open") {
|
|
740
|
+
if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.token_address !== "string" || typeof e.max_amount_atomic !== "string" || e.recipient_address !== null && typeof e.recipient_address !== "string" || typeof e.expires_at !== "number" || typeof e.parent_delegation_hash !== "string") {
|
|
741
|
+
return malformed("expected is missing a required field for purpose open");
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
purpose: "open",
|
|
745
|
+
taskBudgetId: String(body.task_budget_id ?? taskBudgetId),
|
|
746
|
+
typedData,
|
|
747
|
+
expected: {
|
|
748
|
+
delegateAccount: e.delegate_account,
|
|
749
|
+
chainId: e.chain_id,
|
|
750
|
+
tokenAddress: e.token_address,
|
|
751
|
+
maxAmountAtomic: e.max_amount_atomic,
|
|
752
|
+
recipientAddress: e.recipient_address ?? null,
|
|
753
|
+
expiresAt: e.expires_at,
|
|
754
|
+
parentDelegationHash: e.parent_delegation_hash
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (purpose === "close") {
|
|
759
|
+
const userOpHash = body.user_op_hash;
|
|
760
|
+
if (typeof userOpHash !== "string") return malformed("missing user_op_hash for purpose close");
|
|
761
|
+
if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.delegation_hash !== "string") {
|
|
762
|
+
return malformed("expected is missing a required field for purpose close");
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
purpose: "close",
|
|
766
|
+
taskBudgetId: String(body.task_budget_id ?? taskBudgetId),
|
|
767
|
+
typedData,
|
|
768
|
+
userOpHash,
|
|
769
|
+
expected: {
|
|
770
|
+
delegateAccount: e.delegate_account,
|
|
771
|
+
chainId: e.chain_id,
|
|
772
|
+
delegationHash: e.delegation_hash
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
return malformed(`unknown purpose '${String(purpose)}'`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/capabilities.ts
|
|
467
780
|
var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
|
|
468
781
|
function signerCompatibility() {
|
|
469
782
|
return {
|
|
470
783
|
x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
|
|
471
|
-
sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS]
|
|
784
|
+
sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS],
|
|
785
|
+
direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS],
|
|
786
|
+
task_sign_context_versions: [...SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS]
|
|
472
787
|
};
|
|
473
788
|
}
|
|
474
789
|
function signerCapabilityAdvertisement() {
|
|
@@ -479,12 +794,15 @@ function signerInstructions() {
|
|
|
479
794
|
return [
|
|
480
795
|
"Haven edge signer: sign-only tools bound to the local delegate key. It never emits the",
|
|
481
796
|
"key. Its one network capability is an authenticated READ of a signing context from",
|
|
482
|
-
"Haven by payment_id \u2014 pass payment_id to haven_sign
|
|
483
|
-
"delegation-rail x402) instead of relaying bulky
|
|
797
|
+
"Haven by payment_id \u2014 pass payment_id to haven_sign (preferred for both a direct payment,",
|
|
798
|
+
"#3271, and delegation-rail x402) or haven_sign_x402 (x402 only) instead of relaying bulky",
|
|
799
|
+
"typed-data payloads yourself.",
|
|
484
800
|
"",
|
|
485
801
|
"Version compatibility (check this BEFORE signing, not after):",
|
|
486
802
|
`- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
|
|
487
803
|
`- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
|
|
804
|
+
`- 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`,
|
|
805
|
+
`- task-budget sign-context versions supported: ${compatibility.task_sign_context_versions.join(", ")} \u2014 pass task_budget_id alone to haven_sign; this signer fetches the exact bytes`,
|
|
488
806
|
"",
|
|
489
807
|
"Haven quote and prepare results report the expected-context version they will emit",
|
|
490
808
|
"(signer_compatibility.x402_expected_context_version). If that version is not in the list",
|
|
@@ -501,6 +819,62 @@ function signerInstructions() {
|
|
|
501
819
|
"next_tool_omitted_reason saying why not."
|
|
502
820
|
].join("\n");
|
|
503
821
|
}
|
|
822
|
+
var OWNER_ONLY_MODE = 384;
|
|
823
|
+
var defaultLog = (message) => process.stderr.write(`${message}
|
|
824
|
+
`);
|
|
825
|
+
async function permissiveMode(path, platform = process.platform, stats, follow = "nofollow") {
|
|
826
|
+
if (platform === "win32") return null;
|
|
827
|
+
let info;
|
|
828
|
+
try {
|
|
829
|
+
info = stats ?? (follow === "follow" ? await stat(path) : await lstat(path));
|
|
830
|
+
} catch {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
if ((info.mode & 63) === 0) return null;
|
|
834
|
+
return { octal: (info.mode & 511).toString(8).padStart(4, "0"), regular: info.isFile() };
|
|
835
|
+
}
|
|
836
|
+
async function warnIfFilePermissive(kind, path, log = defaultLog, platform = process.platform) {
|
|
837
|
+
const found = await permissiveMode(path, platform, void 0, "follow");
|
|
838
|
+
if (found === null) return;
|
|
839
|
+
log(
|
|
840
|
+
`haven-signer: warning: ${kind} at ${path} is readable beyond the owner (mode ${found.octal}). Run: chmod 600 ${path}`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
async function tightenIfFilePermissive(kind, path, log = defaultLog, platform = process.platform, stats) {
|
|
844
|
+
if (platform === "win32") return "skipped";
|
|
845
|
+
let info = stats ?? null;
|
|
846
|
+
if (!info) {
|
|
847
|
+
try {
|
|
848
|
+
info = await lstat(path);
|
|
849
|
+
} catch {
|
|
850
|
+
return "owner-only";
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (!info.isFile()) {
|
|
854
|
+
log(
|
|
855
|
+
`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}`
|
|
856
|
+
);
|
|
857
|
+
return "warned";
|
|
858
|
+
}
|
|
859
|
+
const found = await permissiveMode(path, platform, info);
|
|
860
|
+
if (found === null) return "owner-only";
|
|
861
|
+
{
|
|
862
|
+
try {
|
|
863
|
+
await chmod(path, OWNER_ONLY_MODE);
|
|
864
|
+
log(
|
|
865
|
+
`haven-signer: ${kind} at ${path} was readable beyond the owner (mode ${found.octal}); tightened to 0600.`
|
|
866
|
+
);
|
|
867
|
+
return "tightened";
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
log(
|
|
872
|
+
`haven-signer: warning: ${kind} at ${path} is readable beyond the owner (mode ${found.octal}) and could not be tightened. Run: chmod 600 ${path}`
|
|
873
|
+
);
|
|
874
|
+
return "warned";
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/credentials.ts
|
|
504
878
|
async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
|
|
505
879
|
if (path) return loadFromFile(path);
|
|
506
880
|
const envKey = stringField(process.env.HAVEN_DELEGATE_KEY);
|
|
@@ -597,28 +971,26 @@ function invalidChainIdError(label) {
|
|
|
597
971
|
}
|
|
598
972
|
async function warnIfCredentialFilePermissive(path, log = (message) => process.stderr.write(`${message}
|
|
599
973
|
`), platform = process.platform) {
|
|
600
|
-
|
|
601
|
-
let mode;
|
|
602
|
-
try {
|
|
603
|
-
mode = (await stat(path)).mode;
|
|
604
|
-
} catch {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
if ((mode & 63) !== 0) {
|
|
608
|
-
const octal = (mode & 511).toString(8).padStart(4, "0");
|
|
609
|
-
log(
|
|
610
|
-
`haven-signer: warning: credential file at ${path} is readable beyond the owner (mode ${octal}). Run: chmod 600 ${path}`
|
|
611
|
-
);
|
|
612
|
-
}
|
|
974
|
+
await warnIfFilePermissive("credential file", path, log, platform);
|
|
613
975
|
}
|
|
976
|
+
var AUDIT_ROTATE_BYTES = 8 * 1024 * 1024;
|
|
614
977
|
function defaultSigningAuditPath(credentialsPath) {
|
|
615
978
|
if (credentialsPath) return resolve(`${credentialsPath}.signer-audit.jsonl`);
|
|
616
979
|
return resolve(homedir(), ".haven", "signer-audit.jsonl");
|
|
617
980
|
}
|
|
618
|
-
async function appendSigningAuditEntry(entry, path) {
|
|
981
|
+
async function appendSigningAuditEntry(entry, path, options = {}) {
|
|
619
982
|
await mkdir(dirname(path), { recursive: true });
|
|
983
|
+
const rotateAt = options.rotateAtBytes ?? AUDIT_ROTATE_BYTES;
|
|
984
|
+
const existing = await lstat(path).catch(() => null);
|
|
985
|
+
if (existing) {
|
|
986
|
+
await tightenIfFilePermissive("audit sidecar", path, options.log, options.platform, existing);
|
|
987
|
+
if (existing.size >= rotateAt) {
|
|
988
|
+
await rename(path, `${path}.1`).catch(() => {
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
620
992
|
await appendFile(path, `${JSON.stringify(entry)}
|
|
621
|
-
`, "utf8");
|
|
993
|
+
`, { encoding: "utf8", mode: OWNER_ONLY_MODE });
|
|
622
994
|
}
|
|
623
995
|
function createSigningAuditEntry(tool, payloadHash, context, now = /* @__PURE__ */ new Date()) {
|
|
624
996
|
const entry = {
|
|
@@ -644,194 +1016,64 @@ function stableStringify(value) {
|
|
|
644
1016
|
const object = value;
|
|
645
1017
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
|
|
646
1018
|
}
|
|
647
|
-
var
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1019
|
+
var BARE_HASH_REFUSED = "BARE_HASH_REFUSED";
|
|
1020
|
+
var HavenBareHashRefusedError = class extends HavenSigningError {
|
|
1021
|
+
next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
1022
|
+
step;
|
|
1023
|
+
constructor() {
|
|
1024
|
+
super(
|
|
1025
|
+
"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."
|
|
1026
|
+
);
|
|
1027
|
+
this.code = BARE_HASH_REFUSED;
|
|
1028
|
+
this.name = "HavenBareHashRefusedError";
|
|
1029
|
+
this.step = signerRefusalStep({
|
|
1030
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
1031
|
+
nextTool: null,
|
|
1032
|
+
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"
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
662
1035
|
};
|
|
663
|
-
var nextStep = createNextStepBuilder(SIGNER_NEXT_STEP_TARGETS);
|
|
664
|
-
function signerRefusalStep(input) {
|
|
665
|
-
return nextStep({ ...input, safeToContinue: false, reason: "" });
|
|
666
|
-
}
|
|
667
|
-
function nextStepWireFields(step) {
|
|
668
|
-
return {
|
|
669
|
-
...step.next_tool ? { next_tool: step.next_tool } : {},
|
|
670
|
-
...step.next_tool_server ? { next_tool_server: step.next_tool_server } : {},
|
|
671
|
-
...step.next_tool_name ? { next_tool_name: step.next_tool_name } : {},
|
|
672
|
-
...step.next_tool_server_role ? { next_tool_server_role: step.next_tool_server_role } : {},
|
|
673
|
-
...step.next_arguments ? { next_arguments: step.next_arguments } : {},
|
|
674
|
-
...step.next_tool_omitted_reason ? { next_tool_omitted_reason: step.next_tool_omitted_reason } : {}
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
1036
|
|
|
678
|
-
// src/
|
|
679
|
-
var
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
* with the SAME idempotency_key plus `include_signing_payload: true`.
|
|
686
|
-
* Absent on a backend REFUSAL: an expired, executed or unsignable intent
|
|
687
|
-
* cannot be rescued by re-signing its bytes.
|
|
688
|
-
*/
|
|
689
|
-
fallback;
|
|
690
|
-
/**
|
|
691
|
-
* #3001: `AgentPaymentNextAction` values the signer already emits — the
|
|
692
|
-
* version-mismatch refusal's `stop_and_tell_user` for the classes where
|
|
693
|
-
* retrying the same call cannot help, and `payment_window_expired` (with
|
|
694
|
-
* `retry_with_new_quote`) for the backend's 410 `expired`, exactly as the
|
|
695
|
-
* plain `HavenError` branch already does for that code.
|
|
696
|
-
*/
|
|
697
|
-
next_action;
|
|
698
|
-
/**
|
|
699
|
-
* #3103 (epic #3105, decisions 1 and 3): the typed next step beside the
|
|
700
|
-
* action. A refusal Haven made (`SIGN_CONTEXT_REFUSED`, not expired) names
|
|
701
|
-
* the hosted status read with the payment id; a transport failure or a
|
|
702
|
-
* malformed body names no tool — the remedy is re-running the SAME quote
|
|
703
|
-
* tool with `include_signing_payload: true` — and an expired window names
|
|
704
|
-
* none either (which quote tool depends on the flow). Never null: a step
|
|
705
|
-
* with no tool carries `next_tool_omitted_reason`. Additive: the class is
|
|
706
|
-
* not exported from the package index — the published surface is the
|
|
707
|
-
* `ToolFailure` envelope `tools.ts` builds from these fields — and every
|
|
708
|
-
* constructor call site is in this file.
|
|
709
|
-
*/
|
|
710
|
-
next_tool;
|
|
711
|
-
next_tool_server;
|
|
712
|
-
next_tool_name;
|
|
713
|
-
next_tool_server_role;
|
|
714
|
-
next_arguments;
|
|
715
|
-
next_tool_omitted_reason;
|
|
716
|
-
retry_with_new_quote;
|
|
717
|
-
/** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
|
|
718
|
-
http_status;
|
|
719
|
-
/** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
|
|
720
|
-
backend_error_code;
|
|
721
|
-
constructor(message, code, refusal, paymentId) {
|
|
1037
|
+
// src/tools.ts
|
|
1038
|
+
var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
|
|
1039
|
+
var HavenUserOpBindingRefusedError = class extends HavenSigningError {
|
|
1040
|
+
next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
1041
|
+
step;
|
|
1042
|
+
/** `message`: the SDK's `HavenUserOpBindingError.message` verbatim — it already names the mismatch and the remedy. */
|
|
1043
|
+
constructor(message) {
|
|
722
1044
|
super(message);
|
|
723
|
-
this.code =
|
|
724
|
-
this.name = "
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
this.next_action = AgentPaymentNextAction.PaymentWindowExpired;
|
|
731
|
-
this.retry_with_new_quote = true;
|
|
732
|
-
step = signerRefusalStep({
|
|
733
|
-
nextAction: AgentPaymentNextAction.PaymentWindowExpired,
|
|
734
|
-
nextTool: null,
|
|
735
|
-
nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow"
|
|
736
|
-
});
|
|
737
|
-
} else {
|
|
738
|
-
this.next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
739
|
-
step = signerRefusalStep({
|
|
740
|
-
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
741
|
-
nextTool: "haven_get_payment_status",
|
|
742
|
-
nextArguments: { payment_id: paymentId }
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
} else {
|
|
746
|
-
this.fallback = "typed_data_b64";
|
|
747
|
-
this.next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
748
|
-
step = signerRefusalStep({
|
|
749
|
-
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
750
|
-
nextTool: null,
|
|
751
|
-
nextToolOmittedReason: "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"
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
Object.assign(this, nextStepWireFields(step));
|
|
1045
|
+
this.code = USEROP_BINDING_MISMATCH;
|
|
1046
|
+
this.name = "HavenUserOpBindingRefusedError";
|
|
1047
|
+
this.step = signerRefusalStep({
|
|
1048
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
1049
|
+
nextTool: null,
|
|
1050
|
+
nextToolOmittedReason: "call haven_sign again with payment_id alone (preferred), or with typed_data copied unchanged from the payment result"
|
|
1051
|
+
});
|
|
755
1052
|
}
|
|
756
1053
|
};
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
return null;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
var SIGN_CONTEXT_TIMEOUT_MS = 15e3;
|
|
772
|
-
async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS) {
|
|
773
|
-
let response;
|
|
774
|
-
try {
|
|
775
|
-
response = await fetchImpl(
|
|
776
|
-
`${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
|
|
777
|
-
{
|
|
778
|
-
headers: { Authorization: `Bearer ${identity.apiKey}` },
|
|
779
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
780
|
-
}
|
|
781
|
-
);
|
|
782
|
-
} catch (err) {
|
|
783
|
-
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
784
|
-
throw new HavenSignContextError(
|
|
785
|
-
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.`,
|
|
786
|
-
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
|
|
787
|
-
void 0,
|
|
788
|
-
paymentId
|
|
789
|
-
);
|
|
1054
|
+
var HavenTypedDataNotAllowedError = class extends HavenTypedDataRefusedError {
|
|
1055
|
+
next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
1056
|
+
step;
|
|
1057
|
+
constructor(message) {
|
|
1058
|
+
super(message);
|
|
1059
|
+
this.name = "HavenTypedDataNotAllowedError";
|
|
1060
|
+
this.step = signerRefusalStep({
|
|
1061
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
1062
|
+
nextTool: null,
|
|
1063
|
+
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"
|
|
1064
|
+
});
|
|
790
1065
|
}
|
|
791
|
-
|
|
1066
|
+
};
|
|
1067
|
+
function assertBoundDirectPaymentUserOp2(typedData, delegateAddress) {
|
|
792
1068
|
try {
|
|
793
|
-
|
|
1069
|
+
assertBoundDirectPaymentUserOp(typedData, delegateAddress);
|
|
794
1070
|
} catch (err) {
|
|
795
|
-
if (err instanceof
|
|
796
|
-
throw new
|
|
797
|
-
`Haven did not finish sending the signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.`,
|
|
798
|
-
"SIGN_CONTEXT_TIMEOUT",
|
|
799
|
-
void 0,
|
|
800
|
-
paymentId
|
|
801
|
-
);
|
|
1071
|
+
if (err instanceof HavenTypedDataRefusedError && !(err instanceof HavenTypedDataNotAllowedError)) {
|
|
1072
|
+
throw new HavenTypedDataNotAllowedError(err.message);
|
|
802
1073
|
}
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
if (!response.ok) {
|
|
806
|
-
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
807
|
-
throw new HavenSignContextError(
|
|
808
|
-
`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." : ""),
|
|
809
|
-
"SIGN_CONTEXT_REFUSED",
|
|
810
|
-
{ httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 },
|
|
811
|
-
paymentId
|
|
812
|
-
);
|
|
813
|
-
}
|
|
814
|
-
const signData = body.sign_data;
|
|
815
|
-
const x402Expected = body.x402_expected;
|
|
816
|
-
if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
|
|
817
|
-
throw new HavenSignContextError(
|
|
818
|
-
"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.",
|
|
819
|
-
"SIGN_CONTEXT_MALFORMED",
|
|
820
|
-
void 0,
|
|
821
|
-
paymentId
|
|
822
|
-
);
|
|
1074
|
+
throw err;
|
|
823
1075
|
}
|
|
824
|
-
const paymentRequired = body.payment_required;
|
|
825
|
-
return {
|
|
826
|
-
paymentId: String(body.payment_id ?? paymentId),
|
|
827
|
-
payloadHash: signData.hash,
|
|
828
|
-
typedData: signData.typed_data,
|
|
829
|
-
x402Expected,
|
|
830
|
-
paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null
|
|
831
|
-
};
|
|
832
1076
|
}
|
|
833
|
-
|
|
834
|
-
// src/tools.ts
|
|
835
1077
|
var sweepAuthorizationSchema = z$1.object({
|
|
836
1078
|
from: z$1.string().regex(/^0x[0-9a-fA-F]{40}$/, "from must be a 0x address"),
|
|
837
1079
|
to: z$1.string().regex(/^0x[0-9a-fA-F]{40}$/, "to must be a 0x address"),
|
|
@@ -851,7 +1093,7 @@ var sweepExpectedAuthSchema = z$1.object({
|
|
|
851
1093
|
});
|
|
852
1094
|
var x402ExpectedShape = {
|
|
853
1095
|
payment_id: z$1.string().min(1),
|
|
854
|
-
payload_hash: z$1.string().regex(/^0x[0-9a-fA-F]
|
|
1096
|
+
payload_hash: z$1.string().regex(/^0x[0-9a-fA-F]{64}$/, "payload_hash must be a 0x-prefixed 32-byte hex string"),
|
|
855
1097
|
resource_url: z$1.string().url(),
|
|
856
1098
|
merchant_to: z$1.string().min(1),
|
|
857
1099
|
amount: z$1.string().min(1),
|
|
@@ -865,7 +1107,7 @@ var x402ExpectedShape = {
|
|
|
865
1107
|
// #1138: present on a delegation-rail (v2) context. It commits to the EIP-712
|
|
866
1108
|
// typed data the account validates; the signer refuses to raw-sign the bare
|
|
867
1109
|
// hash when it is present, and refuses to sign typed data when it is absent.
|
|
868
|
-
typed_data_hash: z$1.string().regex(/^0x[0-9a-fA-F]
|
|
1110
|
+
typed_data_hash: z$1.string().regex(/^0x[0-9a-fA-F]{64}$/, "typed_data_hash must be a 0x-prefixed 32-byte hex string").optional(),
|
|
869
1111
|
// #1690: present on a v3 context. The delegate this quote was created FOR —
|
|
870
1112
|
// the signer refuses to sign when it is not its own. Inside the Haven-signed
|
|
871
1113
|
// message, so stripping it here would only break the binding signature.
|
|
@@ -897,7 +1139,7 @@ var toolSchemas = {
|
|
|
897
1139
|
payment_id: z$1.string().min(1).optional(),
|
|
898
1140
|
// The unsigned hash from haven_pay / haven_pay_x402_quote (payload_hash).
|
|
899
1141
|
// Optional when payment_id is supplied (the fetch carries it).
|
|
900
|
-
payload_hash: z$1.string().regex(/^0x[0-9a-fA-F]
|
|
1142
|
+
payload_hash: z$1.string().regex(/^0x[0-9a-fA-F]{64}$/, "payload_hash must be a 0x-prefixed 32-byte hex string").optional(),
|
|
901
1143
|
// Pass x402.expected from hosted haven_pay_x402_quote when this hash funds
|
|
902
1144
|
// a standard x402 merchant retry. The signer records it locally and returns
|
|
903
1145
|
// an opaque x402_binding for the later header-signing step.
|
|
@@ -913,7 +1155,14 @@ var toolSchemas = {
|
|
|
913
1155
|
// Bounded: a realistic redemption payload is ~10KB encoded; 256KB is
|
|
914
1156
|
// generous headroom while keeping the offline signer from materializing
|
|
915
1157
|
// arbitrarily large caller input.
|
|
916
|
-
typed_data_b64: z$1.string().min(1).max(262144).optional()
|
|
1158
|
+
typed_data_b64: z$1.string().min(1).max(262144).optional(),
|
|
1159
|
+
// #3329: sign a task budget's own child delegation (purpose `open`) or
|
|
1160
|
+
// its early-close UserOp (purpose `close`) — the signer fetches the
|
|
1161
|
+
// exact typed data and expectation itself, exactly like payment_id.
|
|
1162
|
+
// Mutually exclusive with payment_id / payload_hash (checked in the
|
|
1163
|
+
// handler, since Zod's object-level refine on a raw shape used by
|
|
1164
|
+
// several call sites is not the shape this schema is composed as).
|
|
1165
|
+
task_budget_id: z$1.string().min(1).optional()
|
|
917
1166
|
},
|
|
918
1167
|
haven_x402_sign_header: {
|
|
919
1168
|
// The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
|
|
@@ -929,7 +1178,7 @@ var toolSchemas = {
|
|
|
929
1178
|
payment_id: z$1.string().min(1).optional(),
|
|
930
1179
|
// One-shot x402 signing: funding hash + merchant header in one local call.
|
|
931
1180
|
// Both optional when payment_id is supplied (the fetch carries them).
|
|
932
|
-
payload_hash: z$1.string().regex(/^0x[0-9a-fA-F]
|
|
1181
|
+
payload_hash: z$1.string().regex(/^0x[0-9a-fA-F]{64}$/, "payload_hash must be a 0x-prefixed 32-byte hex string").optional(),
|
|
933
1182
|
x402_expected: x402ExpectedSchema.optional(),
|
|
934
1183
|
// #1355: optional when payment_id is supplied — the signer's context fetch
|
|
935
1184
|
// carries the stored 402 PaymentRequired, so `{ payment_id }` alone is the
|
|
@@ -945,17 +1194,29 @@ var toolSchemas = {
|
|
|
945
1194
|
// #3101: keys survive on the type (see the hosted server's contracts.ts).
|
|
946
1195
|
};
|
|
947
1196
|
var SIGN_DESCRIPTION = [
|
|
948
|
-
"Sign an unsigned Haven payment
|
|
949
|
-
"this process. Pass the payload_hash
|
|
1197
|
+
"Sign an unsigned Haven payment with the local delegate key. The delegate key never leaves",
|
|
1198
|
+
"this process. Pass payment_id (preferred), or the payload_hash from haven_pay / haven_pay_x402_quote",
|
|
1199
|
+
"TOGETHER WITH typed_data / typed_data_b64 or x402_expected \u2014 never a payload_hash alone.",
|
|
950
1200
|
"For x402, also pass x402_expected from haven_pay_x402_quote; the signer records it locally",
|
|
951
1201
|
"and returns { signature, x402_binding }. x402_expected includes expires_at; sign before that",
|
|
952
1202
|
"window closes. DELEGATION-RAIL x402 accounts (#1263): pass payment_id ALONE (preferred) \u2014 this",
|
|
953
1203
|
"signer fetches the exact signing payload and expected context from Haven itself, so nothing",
|
|
954
|
-
"bulky ever crosses your context.
|
|
955
|
-
"
|
|
1204
|
+
"bulky ever crosses your context. DIRECT payments (#3271, haven_send / haven_pay): payment_id",
|
|
1205
|
+
"ALONE also works here \u2014 the signer fetches the exact PackedUserOperation typed data and its",
|
|
1206
|
+
"payload_hash from Haven and checks them against each other before signing, so a payload corrupted",
|
|
1207
|
+
"in transit is refused (USEROP_BINDING_MISMATCH) instead of producing a bad signature. Fallback for",
|
|
1208
|
+
"either flow: pass typed_data_b64 through UNCHANGED (never re-type the nested typed_data JSON); the",
|
|
1209
|
+
"account validates that EIP-712 payload, not payload_hash. On a direct payment the same binding",
|
|
1210
|
+
"check runs on the relayed payload too. This tool signs ONLY Haven-prepared payloads (#3272): a",
|
|
1211
|
+
"direct-payment UserOp from your own account that redeems a delegation granted to it, or an x402",
|
|
1212
|
+
"intent against a Haven-signed context \u2014 never an arbitrary typed_data payload, however it is shaped.",
|
|
956
1213
|
"Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
|
|
957
|
-
"to mcp__haven-signer__haven_x402_sign_header.
|
|
958
|
-
"
|
|
1214
|
+
"to mcp__haven-signer__haven_x402_sign_header. A bare payload_hash with no payment_id, typed_data",
|
|
1215
|
+
"or x402_expected is REFUSED (BARE_HASH_REFUSED): a hash carries nothing this signer can verify.",
|
|
1216
|
+
"TASK BUDGETS (#3329): pass task_budget_id ALONE \u2014 mutually exclusive with payment_id and",
|
|
1217
|
+
"payload_hash \u2014 to sign a task budget\u2019s own child delegation (opening it) or its early-close",
|
|
1218
|
+
"UserOp; this signer fetches the exact typed data itself. Result then carries next_tool_name",
|
|
1219
|
+
"(haven_submit) and next_arguments { task_budget_id, signature } instead of x402_binding."
|
|
959
1220
|
].join(" ");
|
|
960
1221
|
var X402_SIGN_HEADER_DESCRIPTION = [
|
|
961
1222
|
"Build and sign the EIP-3009 merchant payment header for the merchant leg of an x402 payment.",
|
|
@@ -973,7 +1234,7 @@ var X402_SIGN_HEADER_DESCRIPTION = [
|
|
|
973
1234
|
"the funding tx has a confirmed status). Next for paid MCP tools: call mcp__haven__haven_complete_mcp_tool."
|
|
974
1235
|
].join(" ");
|
|
975
1236
|
var SIGN_X402_DESCRIPTION = [
|
|
976
|
-
"One-shot x402 signing for the fast 3-call flow: sign the funding
|
|
1237
|
+
"One-shot x402 signing for the fast 3-call flow: sign the funding payload AND build the EIP-3009",
|
|
977
1238
|
"merchant payment header in a single local call (equivalent to haven_sign followed by",
|
|
978
1239
|
"haven_x402_sign_header). The delegate key never leaves this process. From the haven_pay_mcp_tool",
|
|
979
1240
|
"result pass JUST payment_id \u2014 PREFERRED (#1263, #1355): this signer fetches the exact signing",
|
|
@@ -999,12 +1260,13 @@ var SIGN_X402_DESCRIPTION = [
|
|
|
999
1260
|
].join(" ");
|
|
1000
1261
|
var SIGN_SWEEP_DELEGATE_DESCRIPTION = [
|
|
1001
1262
|
"Sign a Haven-prepared gasless USDC sweep that recovers stranded funds from the delegate",
|
|
1002
|
-
"wallet back to
|
|
1003
|
-
"never broadcasts \u2014 it returns only an EIP-3009 signature that Haven's relayer submits and",
|
|
1263
|
+
"wallet back to the agent's account (Haven wallet). The delegate key never leaves this process and this",
|
|
1264
|
+
"tool never broadcasts \u2014 it returns only an EIP-3009 signature that Haven's relayer submits and",
|
|
1004
1265
|
"pays gas for. Pass the authorization and expected_auth returned by the hosted",
|
|
1005
|
-
"haven_sweep_delegate tool. The signer verifies Haven authored the authorization
|
|
1006
|
-
"
|
|
1007
|
-
"
|
|
1266
|
+
"haven_sweep_delegate tool. The signer verifies Haven authored the authorization; the",
|
|
1267
|
+
"destination is checked against the agent's account (Haven wallet) from the local credential",
|
|
1268
|
+
"when the credential carries one, and otherwise rests on Haven's binding signature. Then it",
|
|
1269
|
+
"returns { signature } to hand back to mcp__haven__haven_sweep_delegate to complete recovery."
|
|
1008
1270
|
].join(" ");
|
|
1009
1271
|
var toolDescriptions = {
|
|
1010
1272
|
haven_sign: SIGN_DESCRIPTION,
|
|
@@ -1012,16 +1274,18 @@ var toolDescriptions = {
|
|
|
1012
1274
|
haven_sign_x402: SIGN_X402_DESCRIPTION,
|
|
1013
1275
|
haven_sign_sweep_delegate: SIGN_SWEEP_DELEGATE_DESCRIPTION
|
|
1014
1276
|
};
|
|
1015
|
-
async function signFundingLeg(signer, expected,
|
|
1016
|
-
|
|
1017
|
-
return signer.
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
)
|
|
1277
|
+
async function signFundingLeg(signer, expected, typedData) {
|
|
1278
|
+
try {
|
|
1279
|
+
return await signer.signX402FundingTypedData(typedData, expected);
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
if (err instanceof HavenTypedDataRefusedError && !(err instanceof HavenTypedDataNotAllowedError)) {
|
|
1282
|
+
throw new HavenTypedDataNotAllowedError(err.message);
|
|
1283
|
+
}
|
|
1284
|
+
if (err instanceof HavenUserOpBindingError) {
|
|
1285
|
+
throw new HavenUserOpBindingRefusedError(err.message);
|
|
1286
|
+
}
|
|
1287
|
+
throw err;
|
|
1023
1288
|
}
|
|
1024
|
-
return signer.signX402FundingTypedData(typedData, expected);
|
|
1025
1289
|
}
|
|
1026
1290
|
function toExpectedX402(raw) {
|
|
1027
1291
|
return {
|
|
@@ -1048,6 +1312,9 @@ function parseFetchedExpected(fetched) {
|
|
|
1048
1312
|
}
|
|
1049
1313
|
return parsed.data;
|
|
1050
1314
|
}
|
|
1315
|
+
function clientUpdateField(resolved) {
|
|
1316
|
+
return resolved?.ctx.clientUpdate ? { client_update: resolved.ctx.clientUpdate } : {};
|
|
1317
|
+
}
|
|
1051
1318
|
function resolveTypedData(args) {
|
|
1052
1319
|
if (!args.typed_data_b64) return args.typed_data;
|
|
1053
1320
|
try {
|
|
@@ -1065,7 +1332,14 @@ function resolveTypedData(args) {
|
|
|
1065
1332
|
}
|
|
1066
1333
|
}
|
|
1067
1334
|
function createToolHandlers(signer, options = {}) {
|
|
1068
|
-
|
|
1335
|
+
function checkPayloadHashMatch(suppliedHash, fetchedHash, paymentId) {
|
|
1336
|
+
if (suppliedHash && suppliedHash.toLowerCase() !== fetchedHash.toLowerCase()) {
|
|
1337
|
+
throw new HavenSigningError(
|
|
1338
|
+
`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.`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async function resolveSignContext(args, opts) {
|
|
1069
1343
|
if (!args.payment_id) return null;
|
|
1070
1344
|
const identity = await options.signContext?.loadIdentity() ?? null;
|
|
1071
1345
|
if (!identity) {
|
|
@@ -1073,32 +1347,128 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1073
1347
|
`payment_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${connectorRerunCommand()}\` to restore it, or pass typed_data_b64 from the quote result instead.`
|
|
1074
1348
|
);
|
|
1075
1349
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1350
|
+
try {
|
|
1351
|
+
const ctx = await fetchX402SignContext(
|
|
1352
|
+
identity,
|
|
1353
|
+
args.payment_id,
|
|
1354
|
+
options.signContext?.fetchImpl,
|
|
1355
|
+
void 0,
|
|
1356
|
+
options.signContext?.clientIdentity
|
|
1357
|
+
);
|
|
1358
|
+
checkPayloadHashMatch(args.payload_hash, ctx.payloadHash, args.payment_id);
|
|
1359
|
+
return { kind: "x402", ctx };
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
if (opts.allowDirectFallback && err instanceof HavenSignContextError && err.http_status === 409 && err.backend_error_code === "sign_context_unavailable") {
|
|
1362
|
+
let ctx;
|
|
1363
|
+
try {
|
|
1364
|
+
ctx = await fetchDirectSignContext(
|
|
1365
|
+
identity,
|
|
1366
|
+
args.payment_id,
|
|
1367
|
+
options.signContext?.fetchImpl,
|
|
1368
|
+
void 0,
|
|
1369
|
+
options.signContext?.clientIdentity
|
|
1370
|
+
);
|
|
1371
|
+
} catch (directErr) {
|
|
1372
|
+
if (directErr instanceof HavenSignContextError && directErr.http_status === 409 && directErr.backend_error_code === "sign_context_unavailable") {
|
|
1373
|
+
throw err;
|
|
1374
|
+
}
|
|
1375
|
+
throw directErr;
|
|
1376
|
+
}
|
|
1377
|
+
checkPayloadHashMatch(args.payload_hash, ctx.payloadHash, args.payment_id);
|
|
1378
|
+
return { kind: "direct", ctx };
|
|
1379
|
+
}
|
|
1380
|
+
throw err;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
async function signTaskBudget(taskBudgetId) {
|
|
1384
|
+
const identity = await options.signContext?.loadIdentity() ?? null;
|
|
1385
|
+
if (!identity) {
|
|
1082
1386
|
throw new HavenSigningError(
|
|
1083
|
-
`
|
|
1387
|
+
`task_budget_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${connectorRerunCommand()}\` to restore it.`
|
|
1084
1388
|
);
|
|
1085
1389
|
}
|
|
1086
|
-
|
|
1390
|
+
const ctx = await fetchTaskBudgetSignContext(
|
|
1391
|
+
identity,
|
|
1392
|
+
taskBudgetId,
|
|
1393
|
+
options.signContext?.fetchImpl,
|
|
1394
|
+
void 0,
|
|
1395
|
+
options.signContext?.clientIdentity
|
|
1396
|
+
);
|
|
1397
|
+
let signature;
|
|
1398
|
+
if (ctx.purpose === "open") {
|
|
1399
|
+
try {
|
|
1400
|
+
assertOwnTaskChild(
|
|
1401
|
+
ctx.typedData,
|
|
1402
|
+
{
|
|
1403
|
+
chainId: ctx.expected.chainId,
|
|
1404
|
+
tokenAddress: ctx.expected.tokenAddress,
|
|
1405
|
+
maxAmountAtomic: ctx.expected.maxAmountAtomic,
|
|
1406
|
+
recipientAddress: ctx.expected.recipientAddress,
|
|
1407
|
+
expiresAt: ctx.expected.expiresAt,
|
|
1408
|
+
parentDelegationHash: ctx.expected.parentDelegationHash
|
|
1409
|
+
},
|
|
1410
|
+
signer.delegateAddress
|
|
1411
|
+
);
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
if (err instanceof HavenTypedDataRefusedError) throw err;
|
|
1414
|
+
throw new HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
|
|
1415
|
+
}
|
|
1416
|
+
signature = await signer.signDelegationTypedData(ctx.typedData);
|
|
1417
|
+
} else {
|
|
1418
|
+
try {
|
|
1419
|
+
assertUserOpTypedDataBinding(ctx.typedData, ctx.userOpHash);
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
if (err instanceof HavenUserOpBindingError) throw new HavenUserOpBindingRefusedError(err.message);
|
|
1422
|
+
throw err;
|
|
1423
|
+
}
|
|
1424
|
+
try {
|
|
1425
|
+
assertOwnTaskBudgetCloseUserOp(
|
|
1426
|
+
ctx.typedData,
|
|
1427
|
+
{ delegationHash: ctx.expected.delegationHash },
|
|
1428
|
+
signer.delegateAddress
|
|
1429
|
+
);
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
if (err instanceof HavenTypedDataRefusedError) throw err;
|
|
1432
|
+
throw new HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
|
|
1433
|
+
}
|
|
1434
|
+
signature = await signer.signDelegationTypedData(ctx.typedData);
|
|
1435
|
+
}
|
|
1436
|
+
await auditSigning("haven_sign", hashTypedData(ctx.typedData));
|
|
1437
|
+
return {
|
|
1438
|
+
signature,
|
|
1439
|
+
task_budget_id: ctx.taskBudgetId,
|
|
1440
|
+
purpose: ctx.purpose,
|
|
1441
|
+
next_tool_name: "haven_submit",
|
|
1442
|
+
next_tool_server_role: "hosted",
|
|
1443
|
+
next_arguments: { task_budget_id: ctx.taskBudgetId, signature }
|
|
1444
|
+
};
|
|
1087
1445
|
}
|
|
1088
1446
|
return {
|
|
1089
1447
|
haven_sign: async (input) => runTool(async () => {
|
|
1090
1448
|
const args = parse("haven_sign", coerceX402Expected(input));
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1449
|
+
if (args.task_budget_id) {
|
|
1450
|
+
if (args.payment_id || args.payload_hash) {
|
|
1451
|
+
throw new z$1.ZodError([
|
|
1452
|
+
{
|
|
1453
|
+
code: z$1.ZodIssueCode.custom,
|
|
1454
|
+
path: ["task_budget_id"],
|
|
1455
|
+
message: "task_budget_id is mutually exclusive with payment_id / payload_hash \u2014 pass exactly one."
|
|
1456
|
+
}
|
|
1457
|
+
]);
|
|
1458
|
+
}
|
|
1459
|
+
return signTaskBudget(args.task_budget_id);
|
|
1460
|
+
}
|
|
1461
|
+
const resolved = await resolveSignContext(args, { allowDirectFallback: true });
|
|
1462
|
+
const typedData = resolved?.ctx.typedData ?? resolveTypedData(args);
|
|
1463
|
+
const payloadHash = resolved?.ctx.payloadHash ?? args.payload_hash;
|
|
1094
1464
|
if (!payloadHash) {
|
|
1095
1465
|
throw new HavenSigningError(
|
|
1096
|
-
"Pass payment_id (preferred for delegation-rail x402) or payload_hash."
|
|
1466
|
+
"Pass payment_id (preferred for delegation-rail x402 or direct payments) or payload_hash."
|
|
1097
1467
|
);
|
|
1098
1468
|
}
|
|
1099
|
-
const expectedRaw =
|
|
1469
|
+
const expectedRaw = resolved?.kind === "x402" ? parseFetchedExpected(resolved.ctx) : resolved?.kind === "direct" ? null : args.x402_expected;
|
|
1100
1470
|
const x402Expected = expectedRaw ? toExpectedX402(expectedRaw) : null;
|
|
1101
|
-
const result = x402Expected ? await signFundingLeg(signer, x402Expected,
|
|
1471
|
+
const result = x402Expected ? await signFundingLeg(signer, x402Expected, typedData) : null;
|
|
1102
1472
|
if (!result) {
|
|
1103
1473
|
if (typedData) {
|
|
1104
1474
|
if (isSettlementChildTypedData(typedData)) {
|
|
@@ -1106,16 +1476,28 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1106
1476
|
"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."
|
|
1107
1477
|
);
|
|
1108
1478
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1479
|
+
if (resolved?.kind !== "direct" && !isPackedUserOperationTypedData(typedData)) {
|
|
1480
|
+
throw new HavenTypedDataNotAllowedError(
|
|
1481
|
+
"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."
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
try {
|
|
1485
|
+
assertUserOpTypedDataBinding(typedData, payloadHash);
|
|
1486
|
+
} catch (err) {
|
|
1487
|
+
if (err instanceof HavenUserOpBindingError) {
|
|
1488
|
+
throw new HavenUserOpBindingRefusedError(err.message);
|
|
1489
|
+
}
|
|
1490
|
+
throw err;
|
|
1491
|
+
}
|
|
1492
|
+
assertBoundDirectPaymentUserOp2(typedData, signer.delegateAddress);
|
|
1493
|
+
const signature = await signer.signDelegationTypedData(typedData);
|
|
1494
|
+
await auditSigning("haven_sign", hashTypedData(typedData));
|
|
1495
|
+
return { signature, ...clientUpdateField(resolved) };
|
|
1112
1496
|
}
|
|
1113
|
-
|
|
1114
|
-
await auditSigning("haven_sign", payloadHash);
|
|
1115
|
-
return { signature };
|
|
1497
|
+
throw new HavenBareHashRefusedError();
|
|
1116
1498
|
}
|
|
1117
|
-
await auditSigning("haven_sign",
|
|
1118
|
-
return { signature: result.signature, x402_binding: result.x402Binding };
|
|
1499
|
+
await auditSigning("haven_sign", hashTypedData(typedData));
|
|
1500
|
+
return { signature: result.signature, x402_binding: result.x402Binding, ...clientUpdateField(resolved) };
|
|
1119
1501
|
}),
|
|
1120
1502
|
haven_x402_sign_header: async (input) => runTool(async () => {
|
|
1121
1503
|
const args = parse("haven_x402_sign_header", coercePaymentRequired(input));
|
|
@@ -1131,7 +1513,8 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1131
1513
|
}),
|
|
1132
1514
|
haven_sign_x402: async (input) => runTool(async () => {
|
|
1133
1515
|
const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
|
|
1134
|
-
const
|
|
1516
|
+
const resolved = await resolveSignContext(args, { allowDirectFallback: false });
|
|
1517
|
+
const fetched = resolved?.kind === "x402" ? resolved.ctx : void 0;
|
|
1135
1518
|
const payloadHash = fetched?.payloadHash ?? args.payload_hash;
|
|
1136
1519
|
const expectedRaw = fetched ? parseFetchedExpected(fetched) : args.x402_expected;
|
|
1137
1520
|
if (!payloadHash || !expectedRaw) {
|
|
@@ -1139,12 +1522,8 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1139
1522
|
"Pass payment_id (preferred \u2014 the signer fetches the signing context itself) or payload_hash + x402_expected from the quote result."
|
|
1140
1523
|
);
|
|
1141
1524
|
}
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
toExpectedX402(expectedRaw),
|
|
1145
|
-
payloadHash,
|
|
1146
|
-
fetched?.typedData ?? resolveTypedData(args)
|
|
1147
|
-
);
|
|
1525
|
+
const fundingTypedData = fetched?.typedData ?? resolveTypedData(args);
|
|
1526
|
+
const funding = await signFundingLeg(signer, toExpectedX402(expectedRaw), fundingTypedData);
|
|
1148
1527
|
const paymentRequired = fetched?.paymentRequired ?? args.payment_required;
|
|
1149
1528
|
if (!paymentRequired) {
|
|
1150
1529
|
throw new HavenSigningError(
|
|
@@ -1155,13 +1534,17 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1155
1534
|
paymentRequired,
|
|
1156
1535
|
funding.x402Binding
|
|
1157
1536
|
);
|
|
1158
|
-
await auditSigning(
|
|
1537
|
+
await auditSigning(
|
|
1538
|
+
"haven_sign_x402",
|
|
1539
|
+
hashTypedData(fundingTypedData)
|
|
1540
|
+
);
|
|
1159
1541
|
await auditSigning("haven_sign_x402", hashPayloadForAudit(paymentRequired));
|
|
1160
1542
|
return {
|
|
1161
1543
|
signature: funding.signature,
|
|
1162
1544
|
x402_binding: funding.x402Binding,
|
|
1163
1545
|
payment_header: header.paymentHeader,
|
|
1164
|
-
accepted: header.accepted
|
|
1546
|
+
accepted: header.accepted,
|
|
1547
|
+
...clientUpdateField(resolved)
|
|
1165
1548
|
};
|
|
1166
1549
|
}),
|
|
1167
1550
|
haven_sign_sweep_delegate: async (input) => runTool(async () => {
|
|
@@ -1169,7 +1552,8 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1169
1552
|
const result = await signer.signSweepAuthorization({
|
|
1170
1553
|
authorization: args.authorization,
|
|
1171
1554
|
expectedAuth: args.expected_auth,
|
|
1172
|
-
// Cross-check `to` against the
|
|
1555
|
+
// Cross-check `to` against the agent's account (Haven wallet) from the
|
|
1556
|
+
// local credential, when the credential carries one.
|
|
1173
1557
|
expectedSafe: options.audit?.accountAddress
|
|
1174
1558
|
});
|
|
1175
1559
|
await auditSigning(
|
|
@@ -1182,13 +1566,19 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1182
1566
|
async function auditSigning(tool, payloadHash) {
|
|
1183
1567
|
if (!options.audit) return;
|
|
1184
1568
|
const { auditPath, ...context } = options.audit;
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1569
|
+
try {
|
|
1570
|
+
await appendSigningAuditEntry(
|
|
1571
|
+
createSigningAuditEntry(tool, payloadHash, {
|
|
1572
|
+
...context,
|
|
1573
|
+
delegateAddress: signer.delegateAddress
|
|
1574
|
+
}),
|
|
1575
|
+
auditPath
|
|
1576
|
+
);
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1579
|
+
process.stderr.write(`haven-signer: warning: audit entry for ${tool} could not be written to ${auditPath}: ${message}
|
|
1580
|
+
`);
|
|
1581
|
+
}
|
|
1192
1582
|
}
|
|
1193
1583
|
}
|
|
1194
1584
|
function parse(name, input) {
|
|
@@ -1256,6 +1646,33 @@ function normalizeError(err) {
|
|
|
1256
1646
|
}))
|
|
1257
1647
|
};
|
|
1258
1648
|
}
|
|
1649
|
+
if (err instanceof HavenBareHashRefusedError) {
|
|
1650
|
+
return {
|
|
1651
|
+
success: false,
|
|
1652
|
+
code: err.code,
|
|
1653
|
+
message: err.message,
|
|
1654
|
+
next_action: err.next_action,
|
|
1655
|
+
...nextStepWireFields(err.step)
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
if (err instanceof HavenUserOpBindingRefusedError) {
|
|
1659
|
+
return {
|
|
1660
|
+
success: false,
|
|
1661
|
+
code: err.code,
|
|
1662
|
+
message: err.message,
|
|
1663
|
+
next_action: err.next_action,
|
|
1664
|
+
...nextStepWireFields(err.step)
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
if (err instanceof HavenTypedDataNotAllowedError) {
|
|
1668
|
+
return {
|
|
1669
|
+
success: false,
|
|
1670
|
+
code: err.code,
|
|
1671
|
+
message: err.message,
|
|
1672
|
+
next_action: err.next_action,
|
|
1673
|
+
...nextStepWireFields(err.step)
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1259
1676
|
if (err instanceof HavenSignContextError) {
|
|
1260
1677
|
return {
|
|
1261
1678
|
success: false,
|
|
@@ -1266,6 +1683,7 @@ function normalizeError(err) {
|
|
|
1266
1683
|
...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
|
|
1267
1684
|
...err.http_status !== void 0 ? { http_status: err.http_status } : {},
|
|
1268
1685
|
...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {},
|
|
1686
|
+
...err.client_update ? { client_update: err.client_update } : {},
|
|
1269
1687
|
// #3103: the typed step the error decided beside its action.
|
|
1270
1688
|
...err.next_tool ? { next_tool: err.next_tool } : {},
|
|
1271
1689
|
...err.next_tool_server ? { next_tool_server: err.next_tool_server } : {},
|
|
@@ -1309,6 +1727,17 @@ function normalizeError(err) {
|
|
|
1309
1727
|
}
|
|
1310
1728
|
|
|
1311
1729
|
// src/consent.ts
|
|
1730
|
+
var toolSummaries = {
|
|
1731
|
+
haven_sign: "signs a Haven-prepared payment, task-budget open, or task-budget close (a UserOp from your own account whose only call redeems a delegation made to it, directly or through a self-delegated task-budget child, or an x402 funding intent against a Haven-signed context)",
|
|
1732
|
+
haven_x402_sign_header: "builds and signs the EIP-3009 merchant payment header for an x402 payment",
|
|
1733
|
+
haven_sign_x402: "the two above in one call: funding payload + merchant header",
|
|
1734
|
+
haven_sign_sweep_delegate: "signs a Haven-prepared gasless sweep of stranded funds back to your own account"
|
|
1735
|
+
};
|
|
1736
|
+
var CONNECTOR_DOCTOR_HINT = [
|
|
1737
|
+
"Wired by the Haven connector (Claude Desktop, Codex, ...)? Diagnose and repair with: npx @haven_ai/connect --doctor",
|
|
1738
|
+
" (the doctor shows this as a failed 'Signer stdio handshake' check; the connector's setup outcome reports it as",
|
|
1739
|
+
" local_signer_ack_required; the repair is the connector's --ack-local-tools)."
|
|
1740
|
+
].join("\n");
|
|
1312
1741
|
var SIGNER_ACK_ENV = "HAVEN_SIGNER_ACK";
|
|
1313
1742
|
var SIGNER_CONSENT_SURFACE_VERSION = 2;
|
|
1314
1743
|
function computeSignerConsentHash(input) {
|
|
@@ -1344,13 +1773,13 @@ function renderSignerConsentBlock(input, hash) {
|
|
|
1344
1773
|
lines.push("payload from Haven, authenticated with the agent credential stored next to");
|
|
1345
1774
|
lines.push("this signer's key file - it never sends the key, a signature, or anything");
|
|
1346
1775
|
lines.push("else outbound, and it cannot show a live allowance summary.");
|
|
1347
|
-
lines.push("
|
|
1348
|
-
lines.push("pause or revoke agent authority outside this signer.");
|
|
1776
|
+
lines.push("The agent's signed budget delegation is the real spend gate, and the wallet");
|
|
1777
|
+
lines.push("owner can pause or revoke agent authority outside this signer.");
|
|
1349
1778
|
lines.push("");
|
|
1350
|
-
lines.push("Tools this signer will expose to your agent runtime
|
|
1779
|
+
lines.push("Tools this signer will expose to your agent runtime (one line each; the");
|
|
1780
|
+
lines.push("full agent-facing descriptions are what the runtime sees):");
|
|
1351
1781
|
for (const name of input.toolNames) {
|
|
1352
|
-
lines.push(` - ${name}`);
|
|
1353
|
-
lines.push(` ${toolDescriptions[name]}`);
|
|
1782
|
+
lines.push(` - ${name}: ${toolSummaries[name]}`);
|
|
1354
1783
|
}
|
|
1355
1784
|
lines.push("");
|
|
1356
1785
|
lines.push("A local audit entry is appended for every signing operation. Audit entries");
|
|
@@ -1364,6 +1793,8 @@ function renderSignerConsentBlock(input, hash) {
|
|
|
1364
1793
|
lines.push(" - re-run with --ack to write the acknowledgement next to your");
|
|
1365
1794
|
lines.push(" credential file (sidecar <credentials>.signer-ack.json).");
|
|
1366
1795
|
lines.push("");
|
|
1796
|
+
lines.push(CONNECTOR_DOCTOR_HINT);
|
|
1797
|
+
lines.push("");
|
|
1367
1798
|
lines.push("------------------------------------------------------------");
|
|
1368
1799
|
lines.push("");
|
|
1369
1800
|
return lines.join("\n");
|
|
@@ -1427,7 +1858,7 @@ async function writeAckFile(path, hash) {
|
|
|
1427
1858
|
);
|
|
1428
1859
|
}
|
|
1429
1860
|
var SIGNER_NAME = "@haven_ai/signer";
|
|
1430
|
-
var SIGNER_VERSION = "0.
|
|
1861
|
+
var SIGNER_VERSION = "0.6.0-alpha.0";
|
|
1431
1862
|
async function resolveEdgeSigner(options = {}) {
|
|
1432
1863
|
const { signer } = await resolveSignerRuntime(options);
|
|
1433
1864
|
return signer;
|
|
@@ -1469,13 +1900,17 @@ function buildSignerMcpServer(signer, options = {}) {
|
|
|
1469
1900
|
accountAddress: options.credentials?.accountAddress,
|
|
1470
1901
|
chainId: options.credentials?.chainId
|
|
1471
1902
|
},
|
|
1472
|
-
// #1263: the payment_id signing path — the ONLY network
|
|
1473
|
-
//
|
|
1903
|
+
// #1263: the payment_id signing path — the ONLY network path this server
|
|
1904
|
+
// has: up to two authenticated reads of a signing context from Haven (x402,
|
|
1905
|
+
// then direct for haven_sign, #3271), using
|
|
1474
1906
|
// the agent identity the connector stores next to the signer credential.
|
|
1475
1907
|
// The signer CORE stays network-free; fetched bytes still pass the same
|
|
1476
1908
|
// binding verification + digest re-derivation as tool-argument bytes.
|
|
1477
1909
|
signContext: {
|
|
1478
|
-
loadIdentity: () => loadHavenIdentity(credentialsPath)
|
|
1910
|
+
loadIdentity: () => loadHavenIdentity(credentialsPath),
|
|
1911
|
+
// #3303: the backend refuses a signer below a minimum it has set, at
|
|
1912
|
+
// sign-context — the one place this signer meets it.
|
|
1913
|
+
clientIdentity: havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
|
|
1479
1914
|
}
|
|
1480
1915
|
});
|
|
1481
1916
|
const registerTool = server.tool.bind(server);
|
|
@@ -1503,7 +1938,7 @@ async function runSignerStdioServer(options = {}) {
|
|
|
1503
1938
|
const decision = await runSignerConsentGate(signer, credentials, options);
|
|
1504
1939
|
if (!decision.ok) {
|
|
1505
1940
|
const err = new Error(
|
|
1506
|
-
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."
|
|
1941
|
+
(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"
|
|
1507
1942
|
);
|
|
1508
1943
|
err.code = "HAVEN_SIGNER_NO_CONSENT";
|
|
1509
1944
|
throw err;
|
|
@@ -1537,6 +1972,6 @@ function toMcpResult(payload) {
|
|
|
1537
1972
|
};
|
|
1538
1973
|
}
|
|
1539
1974
|
|
|
1540
|
-
export { SIGNER_ACK_ENV, SIGNER_CAPABILITY_KEY, SIGNER_HOSTED_HANDOFF_SHAPES, SIGNER_NAME, SIGNER_VERSION, SUPPORTED_SWEEP_BINDING_VERSIONS, SUPPORTED_X402_EXPECTED_VERSIONS, appendSigningAuditEntry, assertSupportedBindingVersion, assertSupportedNodeVersion, assertX402MatchesExpected, buildSignerMcpServer, computeSignerConsentHash, createEdgeSigner, createSigningAuditEntry, createToolHandlers, defaultSigningAuditPath, ensureSignerConsent, hashPayloadForAudit, loadSignerCredentials, readAccountAddressEnv, readAccountAddressField, renderSignerConsentBlock, resolveEdgeSigner, resolveSignerRuntime, runSignerConsentGate, runSignerStdioServer, signerCapabilityAdvertisement, signerCompatibility, signerInstructions, toolDescriptions, toolSchemas, warnIfCredentialFilePermissive };
|
|
1975
|
+
export { AUDIT_ROTATE_BYTES, OWNER_ONLY_MODE, SIGNER_ACK_ENV, SIGNER_CAPABILITY_KEY, SIGNER_HOSTED_HANDOFF_SHAPES, SIGNER_NAME, SIGNER_VERSION, SUPPORTED_SWEEP_BINDING_VERSIONS, SUPPORTED_X402_EXPECTED_VERSIONS, appendSigningAuditEntry, assertSupportedBindingVersion, assertSupportedNodeVersion, assertX402MatchesExpected, buildSignerMcpServer, computeSignerConsentHash, createEdgeSigner, createSigningAuditEntry, createToolHandlers, defaultSigningAuditPath, ensureSignerConsent, hashPayloadForAudit, loadSignerCredentials, permissiveMode, readAccountAddressEnv, readAccountAddressField, renderSignerConsentBlock, resolveEdgeSigner, resolveSignerRuntime, runSignerConsentGate, runSignerStdioServer, signerCapabilityAdvertisement, signerCompatibility, signerInstructions, tightenIfFilePermissive, toolDescriptions, toolSchemas, warnIfCredentialFilePermissive, warnIfFilePermissive };
|
|
1541
1976
|
//# sourceMappingURL=index.js.map
|
|
1542
1977
|
//# sourceMappingURL=index.js.map
|