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