@haven_ai/signer 0.1.17-alpha.0 → 0.1.19-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -7,11 +7,11 @@ var crypto = require('crypto');
7
7
  var promises = require('fs/promises');
8
8
  var os = require('os');
9
9
  var path = require('path');
10
- var sdk = require('@haven_ai/sdk');
11
- var v3 = require('zod/v3');
12
10
  var viem = require('viem');
13
11
  var accounts = require('viem/accounts');
14
12
  var schemes = require('x402/schemes');
13
+ var sdk = require('@haven_ai/sdk');
14
+ var v3 = require('zod/v3');
15
15
 
16
16
  function defaultSigningAuditPath(credentialsPath) {
17
17
  if (credentialsPath) return path.resolve(`${credentialsPath}.signer-audit.jsonl`);
@@ -46,6 +46,300 @@ function stableStringify(value) {
46
46
  const object = value;
47
47
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
48
48
  }
49
+ function createEdgeSigner(delegateKey, options = {}) {
50
+ let delegateAddress;
51
+ try {
52
+ delegateAddress = sdk.addressFromKey(delegateKey);
53
+ } catch (err) {
54
+ throw new sdk.HavenSigningError(
55
+ `Invalid delegate key: ${err instanceof Error ? err.message : String(err)}`
56
+ );
57
+ }
58
+ const x402Bindings = /* @__PURE__ */ new Map();
59
+ function signAndVerify(hash) {
60
+ const signature = sdk.signHash(delegateKey, hash);
61
+ if (!sdk.verifySignature(hash, signature, delegateAddress)) {
62
+ throw new sdk.HavenSigningError(
63
+ "Local signature verification failed \u2014 recovered address does not match the delegate key."
64
+ );
65
+ }
66
+ return signature;
67
+ }
68
+ return {
69
+ delegateAddress,
70
+ signPaymentHash(hash) {
71
+ return signAndVerify(hash);
72
+ },
73
+ async signDelegationTypedData(typedData) {
74
+ const account = accounts.privateKeyToAccount(delegateKey);
75
+ return account.signTypedData(typedData);
76
+ },
77
+ signX402FundingHash(hash, expected) {
78
+ assertExpectedBinding(hash, expected, options.x402BindingSigner, "hash");
79
+ const signature = signAndVerify(hash);
80
+ const x402Binding = crypto.randomUUID();
81
+ x402Bindings.set(x402Binding, { ...expected });
82
+ return { signature, x402Binding };
83
+ },
84
+ async signX402FundingTypedData(typedData, expected) {
85
+ assertExpectedBinding(expected.payloadHash, expected, options.x402BindingSigner, "typed-data");
86
+ const digest = viem.hashTypedData(typedData);
87
+ if (digest.toLowerCase() !== expected.typedDataHash?.toLowerCase()) {
88
+ throw new sdk.HavenSigningError(
89
+ "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."
90
+ );
91
+ }
92
+ const account = accounts.privateKeyToAccount(delegateKey);
93
+ const signature = await account.signTypedData(
94
+ typedData
95
+ );
96
+ const x402Binding = crypto.randomUUID();
97
+ x402Bindings.set(x402Binding, { ...expected });
98
+ return { signature, x402Binding };
99
+ },
100
+ async buildX402PaymentHeader(paymentRequired, x402Binding) {
101
+ const expected = x402Bindings.get(x402Binding);
102
+ if (!expected) {
103
+ throw new sdk.HavenSigningError(
104
+ "x402 funding binding is required before signing a merchant header. Sign the hosted funding hash with x402_expected first."
105
+ );
106
+ }
107
+ try {
108
+ assertX402PaymentWindowOpen(expected);
109
+ } catch (err) {
110
+ x402Bindings.delete(x402Binding);
111
+ throw err;
112
+ }
113
+ const option = sdk.selectStandardPaymentOption(paymentRequired.accepts);
114
+ if (!option) {
115
+ throw new sdk.HavenApiError(
116
+ "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
117
+ 400
118
+ );
119
+ }
120
+ assertX402MatchesExpected(paymentRequired, option, expected);
121
+ const account = accounts.privateKeyToAccount(delegateKey);
122
+ const requirements = sdk.toStandardPaymentRequirements(paymentRequired, option);
123
+ const header = await schemes.exact.evm.createPaymentHeader(
124
+ account,
125
+ paymentRequired.x402Version,
126
+ requirements
127
+ );
128
+ if (paymentRequired.x402Version < 2) {
129
+ x402Bindings.delete(x402Binding);
130
+ return { paymentHeader: header, accepted: option };
131
+ }
132
+ try {
133
+ const payment = sdk.decodeBase64Json(header);
134
+ const wrapped = sdk.encodeBase64Json({
135
+ x402Version: paymentRequired.x402Version,
136
+ accepted: option,
137
+ payload: payment.payload
138
+ });
139
+ return { paymentHeader: wrapped, accepted: option };
140
+ } finally {
141
+ x402Bindings.delete(x402Binding);
142
+ }
143
+ },
144
+ async signSweepAuthorization({
145
+ authorization,
146
+ expectedAuth,
147
+ expectedSafe
148
+ }) {
149
+ assertSweepBinding(authorization, expectedAuth, options.x402BindingSigner);
150
+ if (!sameAddress(authorization.from, delegateAddress)) {
151
+ throw new sdk.HavenSigningError(
152
+ "Sweep authorization `from` does not match this delegate address."
153
+ );
154
+ }
155
+ if (expectedSafe && !sameAddress(authorization.to, expectedSafe)) {
156
+ throw new sdk.HavenSigningError(
157
+ "Sweep authorization `to` does not match the Safe in the local credential."
158
+ );
159
+ }
160
+ const typedData = sdk.buildSweepTypedData(authorization);
161
+ const viemTypedData = {
162
+ domain: {
163
+ ...typedData.domain,
164
+ verifyingContract: typedData.domain.verifyingContract
165
+ },
166
+ types: typedData.types,
167
+ primaryType: typedData.primaryType,
168
+ message: {
169
+ ...typedData.message,
170
+ from: typedData.message.from,
171
+ to: typedData.message.to,
172
+ nonce: typedData.message.nonce
173
+ }
174
+ };
175
+ const account = accounts.privateKeyToAccount(delegateKey);
176
+ const signature = await account.signTypedData(viemTypedData);
177
+ const recovered = await viem.recoverTypedDataAddress({ ...viemTypedData, signature });
178
+ if (!sameAddress(recovered, delegateAddress)) {
179
+ throw new sdk.HavenSigningError(
180
+ "Local sweep signature verification failed \u2014 recovered address does not match the delegate key."
181
+ );
182
+ }
183
+ return { signature };
184
+ }
185
+ };
186
+ }
187
+ function assertSweepBinding(authorization, expectedAuth, trustedSigner) {
188
+ if (!expectedAuth || typeof expectedAuth !== "object") {
189
+ throw new sdk.HavenSigningError("Sweep authorization binding is required before signing.");
190
+ }
191
+ if (!trustedSigner) {
192
+ throw new sdk.HavenSigningError(
193
+ "Sweep binding verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing sweep authorizations."
194
+ );
195
+ }
196
+ assertSupportedBindingVersion(
197
+ expectedAuth.version,
198
+ SUPPORTED_SWEEP_BINDING_VERSIONS,
199
+ "sweep authorization binding"
200
+ );
201
+ const message = sdk.buildSweepAuthorizationMessage(authorization);
202
+ if (expectedAuth.message !== message) {
203
+ throw new sdk.HavenSigningError("Sweep authorization binding does not match the authorization being signed.");
204
+ }
205
+ if (!sameAddress(expectedAuth.signer, trustedSigner)) {
206
+ throw new sdk.HavenSigningError("Sweep authorization binding was not signed by the configured Haven signer.");
207
+ }
208
+ if (!sdk.verifySignature(viem.hashMessage(message), expectedAuth.signature, trustedSigner)) {
209
+ throw new sdk.HavenSigningError("Sweep authorization binding signature could not be verified.");
210
+ }
211
+ }
212
+ function assertX402MatchesExpected(paymentRequired, option, expected) {
213
+ assertExpectedShape(expected);
214
+ const headerResource = option.resource ?? paymentRequired.resource.url;
215
+ if (headerResource !== expected.resourceUrl) {
216
+ throw new sdk.HavenSigningError("x402 payment_required resource does not match the funded intent.");
217
+ }
218
+ if (!sameAddress(option.payTo, expected.merchantTo)) {
219
+ throw new sdk.HavenSigningError("x402 merchant recipient does not match the funded intent.");
220
+ }
221
+ if (sdk.x402AuthorizationAmount(option) !== expected.amount) {
222
+ throw new sdk.HavenSigningError("x402 amount does not match the funded intent.");
223
+ }
224
+ if (!sameAddress(option.asset, expected.asset)) {
225
+ throw new sdk.HavenSigningError("x402 asset does not match the funded intent.");
226
+ }
227
+ if (option.network !== expected.network) {
228
+ throw new sdk.HavenSigningError("x402 network does not match the funded intent.");
229
+ }
230
+ }
231
+ function assertExpectedShape(expected) {
232
+ if (!expected || typeof expected !== "object") {
233
+ throw new sdk.HavenSigningError("x402 expected funding context is required before signing a merchant header.");
234
+ }
235
+ }
236
+ var SUPPORTED_X402_EXPECTED_VERSIONS = [1, 2];
237
+ var SUPPORTED_SWEEP_BINDING_VERSIONS = [1];
238
+ function assertSupportedBindingVersion(received, supported, context) {
239
+ if (supported.includes(received)) return;
240
+ const highest = Math.max(...supported);
241
+ const ceiling = received > highest ? `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 (\`npx @haven_ai/connect@alpha\`), which reinstalls the pinned MCP runtime.` : `Unsupported ${context} version ${received}: this signer supports ${supported.join(", ")}.`;
242
+ throw new sdk.HavenSigningError(
243
+ `${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 authorised.`
244
+ );
245
+ }
246
+ function assertExpectedBinding(payloadHash, expected, trustedSigner, mode = "hash") {
247
+ assertExpectedShape(expected);
248
+ if (!trustedSigner) {
249
+ throw new sdk.HavenSigningError(
250
+ "x402 expected-context verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing x402 funding hashes."
251
+ );
252
+ }
253
+ if (expected.auth) {
254
+ assertSupportedBindingVersion(
255
+ expected.auth.version,
256
+ SUPPORTED_X402_EXPECTED_VERSIONS,
257
+ "x402 expected context"
258
+ );
259
+ }
260
+ if (expected.payloadHash.toLowerCase() !== payloadHash.toLowerCase()) {
261
+ throw new sdk.HavenSigningError("x402 expected context does not match the funding hash being signed.");
262
+ }
263
+ if (mode === "hash" && expected.typedDataHash) {
264
+ throw new sdk.HavenSigningError(
265
+ "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."
266
+ );
267
+ }
268
+ if (mode === "typed-data" && !expected.typedDataHash) {
269
+ throw new sdk.HavenSigningError(
270
+ "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."
271
+ );
272
+ }
273
+ const message = sdk.buildX402ExpectedMessage({
274
+ paymentId: expected.paymentId,
275
+ payloadHash: expected.payloadHash,
276
+ resourceUrl: expected.resourceUrl,
277
+ merchantTo: expected.merchantTo,
278
+ amount: expected.amount,
279
+ asset: expected.asset,
280
+ network: expected.network,
281
+ expiresAt: expected.expiresAt,
282
+ typedDataHash: expected.typedDataHash
283
+ });
284
+ const expectedVersion = expected.typedDataHash ? 2 : 1;
285
+ if (expected.auth?.version !== expectedVersion || expected.auth.message !== message) {
286
+ throw new sdk.HavenSigningError("x402 expected context authentication message is invalid.");
287
+ }
288
+ if (!sameAddress(expected.auth.signer, trustedSigner)) {
289
+ throw new sdk.HavenSigningError("x402 expected context was not signed by the configured Haven signer.");
290
+ }
291
+ if (!sdk.verifySignature(viem.hashMessage(message), expected.auth.signature, trustedSigner)) {
292
+ throw new sdk.HavenSigningError("x402 expected context signature could not be verified.");
293
+ }
294
+ }
295
+ function assertX402PaymentWindowOpen(expected) {
296
+ if (!expected.expiresAt) return;
297
+ const expiresAtMs = Date.parse(expected.expiresAt);
298
+ if (Number.isNaN(expiresAtMs)) {
299
+ throw new sdk.HavenSigningError("x402 expected context expiresAt is not a valid ISO timestamp.");
300
+ }
301
+ if (expiresAtMs <= Date.now()) {
302
+ throw new sdk.HavenError(
303
+ "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.",
304
+ sdk.AgentPaymentFailureCode.PaymentWindowExpired,
305
+ 410,
306
+ expected.paymentId
307
+ );
308
+ }
309
+ }
310
+ function sameAddress(a, b) {
311
+ return a.toLowerCase() === b.toLowerCase();
312
+ }
313
+
314
+ // src/capabilities.ts
315
+ var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
316
+ function signerCompatibility() {
317
+ return {
318
+ x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
319
+ sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS]
320
+ };
321
+ }
322
+ function signerCapabilityAdvertisement() {
323
+ return { experimental: { [SIGNER_CAPABILITY_KEY]: signerCompatibility() } };
324
+ }
325
+ function signerInstructions() {
326
+ const compatibility = signerCompatibility();
327
+ return [
328
+ "Haven edge signer: sign-only tools bound to the local delegate key. It performs no",
329
+ "network I/O and never emits the key.",
330
+ "",
331
+ "Version compatibility (check this BEFORE signing, not after):",
332
+ `- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
333
+ `- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
334
+ "",
335
+ "Haven quote and prepare results report the expected-context version they will emit",
336
+ "(signer_compatibility.x402_expected_context_version). If that version is not in the list",
337
+ "above, this signer is out of date: STOP before signing, and tell the user to update",
338
+ "@haven_ai/signer by rerunning `npx @haven_ai/connect@alpha`, which reinstalls the pinned",
339
+ "MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
340
+ "Haven-signed binding message, so changing it invalidates the signature."
341
+ ].join("\n");
342
+ }
49
343
  var sweepAuthorizationSchema = v3.z.object({
50
344
  from: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "from must be a 0x address"),
51
345
  to: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "to must be a 0x address"),
@@ -56,8 +350,9 @@ var sweepAuthorizationSchema = v3.z.object({
56
350
  token: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "token must be a 0x address"),
57
351
  chainId: v3.z.number().int().positive()
58
352
  });
353
+ var bindingVersionSchema = v3.z.number().int().positive();
59
354
  var sweepExpectedAuthSchema = v3.z.object({
60
- version: v3.z.literal(1),
355
+ version: bindingVersionSchema,
61
356
  message: v3.z.string().min(1),
62
357
  signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
63
358
  signer: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "signer must be a 0x address")
@@ -75,8 +370,13 @@ var x402ExpectedSchema = v3.z.object({
75
370
  // Omitting it used to fail downstream with a cryptic "authentication message is
76
371
  // invalid" — making it required surfaces a clear INVALID_INPUT at the boundary.
77
372
  expires_at: v3.z.string().min(1),
373
+ // #1138: present on a delegation-rail (v2) context. It commits to the EIP-712
374
+ // typed data the account validates; the signer refuses to raw-sign the bare
375
+ // hash when it is present, and refuses to sign typed data when it is absent.
376
+ typed_data_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "typed_data_hash must be a 0x-prefixed hex string").optional(),
78
377
  auth: v3.z.object({
79
- version: v3.z.literal(1),
378
+ // Open at the boundary, enforced in the signer — see bindingVersionSchema.
379
+ version: bindingVersionSchema,
80
380
  message: v3.z.string().min(1),
81
381
  signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
82
382
  signer: v3.z.string().min(1)
@@ -97,7 +397,19 @@ var toolSchemas = {
97
397
  // Pass x402.expected from hosted haven_pay_x402_quote when this hash funds
98
398
  // a standard x402 merchant retry. The signer records it locally and returns
99
399
  // an opaque x402_binding for the later header-signing step.
100
- x402_expected: x402ExpectedSchema.optional()
400
+ x402_expected: x402ExpectedSchema.optional(),
401
+ // #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
402
+ // (not z.unknown()) so MCP clients embed it as JSON rather than a string.
403
+ typed_data: v3.z.record(v3.z.string(), v3.z.unknown()).optional(),
404
+ // #1255: the same payload as ONE opaque base64 string, exactly as returned
405
+ // by the hosted tools. Preferred over typed_data when both are present —
406
+ // an agent re-emitting multi-KB nested JSON between tool calls is the
407
+ // failure mode this field removes (a truncated/reshaped payload fails the
408
+ // digest check and the payment refuses, correctly but pointlessly).
409
+ // Bounded: a realistic redemption payload is ~10KB encoded; 256KB is
410
+ // generous headroom while keeping the offline signer from materializing
411
+ // arbitrarily large caller input.
412
+ typed_data_b64: v3.z.string().min(1).max(262144).optional()
101
413
  },
102
414
  haven_x402_sign_header: {
103
415
  // The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
@@ -111,7 +423,12 @@ var toolSchemas = {
111
423
  // One-shot x402 signing: funding hash + merchant header in one local call.
112
424
  payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
113
425
  x402_expected: x402ExpectedSchema,
114
- payment_required: v3.z.record(v3.z.string(), v3.z.unknown())
426
+ payment_required: v3.z.record(v3.z.string(), v3.z.unknown()),
427
+ // #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
428
+ // (not z.unknown()) so MCP clients embed it as JSON rather than a string.
429
+ typed_data: v3.z.record(v3.z.string(), v3.z.unknown()).optional(),
430
+ // #1255: see haven_sign.typed_data_b64 — the copy-through-safe form.
431
+ typed_data_b64: v3.z.string().min(1).max(262144).optional()
115
432
  }
116
433
  };
117
434
  var SIGN_DESCRIPTION = [
@@ -119,7 +436,10 @@ var SIGN_DESCRIPTION = [
119
436
  "this process. Pass the payload_hash returned by haven_pay or haven_pay_x402_quote.",
120
437
  "For x402, also pass x402_expected from haven_pay_x402_quote; the signer records it locally",
121
438
  "and returns { signature, x402_binding }. x402_expected includes expires_at; sign before that",
122
- "window closes. Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
439
+ "window closes. DELEGATION-RAIL accounts: when the hosted result carries typed_data_b64, pass that",
440
+ "single string through UNCHANGED (preferred \u2014 never re-type the nested typed_data JSON yourself);",
441
+ "the account validates that EIP-712 payload, not payload_hash, and signing is refused without it.",
442
+ "Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
123
443
  "to mcp__haven-signer__haven_x402_sign_header. For plain SafeTransfer payments, just pass payload_hash and",
124
444
  "relay the returned signature via mcp__haven__haven_submit."
125
445
  ].join(" ");
@@ -139,7 +459,11 @@ var SIGN_X402_DESCRIPTION = [
139
459
  "X-PAYMENT header in a single local call (equivalent to haven_sign followed by",
140
460
  "haven_x402_sign_header). The delegate key never leaves this process. From the haven_pay_mcp_tool",
141
461
  "result pass payload_hash, x402_expected (the nested x402.expected object \u2014 passing the whole x402",
142
- "object is also accepted and unwrapped for you), and payment_required (verbatim). Returns",
462
+ "object is also accepted and unwrapped for you), and payment_required (verbatim). On a",
463
+ "delegation-rail account the result also carries typed_data_b64 \u2014 pass that single string through",
464
+ "UNCHANGED (preferred over re-typing the nested typed_data JSON); the signer",
465
+ "signs it instead of payload_hash and refuses the bare hash when the context commits to typed data.",
466
+ "Returns",
143
467
  "{ signature, x402_binding, payment_header, accepted }; hand signature + payment_header to",
144
468
  "mcp__haven__haven_settle_mcp_tool to fund and settle in one hosted call. The header is built now (before",
145
469
  "funding confirms), so its short validity window starts here \u2014 call mcp__haven__haven_settle_mcp_tool promptly,",
@@ -161,10 +485,22 @@ var toolDescriptions = {
161
485
  haven_sign_x402: SIGN_X402_DESCRIPTION,
162
486
  haven_sign_sweep_delegate: SIGN_SWEEP_DELEGATE_DESCRIPTION
163
487
  };
488
+ async function signFundingLeg(signer, expected, payloadHash, typedData) {
489
+ if (!expected.typedDataHash) {
490
+ return signer.signX402FundingHash(payloadHash, expected);
491
+ }
492
+ if (!typedData) {
493
+ throw new sdk.HavenSigningError(
494
+ "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."
495
+ );
496
+ }
497
+ return signer.signX402FundingTypedData(typedData, expected);
498
+ }
164
499
  function toExpectedX402(raw) {
165
500
  return {
166
501
  paymentId: raw.payment_id,
167
502
  payloadHash: raw.payload_hash,
503
+ typedDataHash: raw.typed_data_hash,
168
504
  resourceUrl: raw.resource_url,
169
505
  merchantTo: raw.merchant_to,
170
506
  amount: raw.amount,
@@ -174,13 +510,35 @@ function toExpectedX402(raw) {
174
510
  auth: raw.auth
175
511
  };
176
512
  }
513
+ function resolveTypedData(args) {
514
+ if (!args.typed_data_b64) return args.typed_data;
515
+ try {
516
+ const decoded = JSON.parse(
517
+ Buffer.from(args.typed_data_b64, "base64").toString("utf8")
518
+ );
519
+ if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
520
+ throw new Error("decoded value is not an object");
521
+ }
522
+ return decoded;
523
+ } catch (err) {
524
+ throw new sdk.HavenSigningError(
525
+ `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)}`
526
+ );
527
+ }
528
+ }
177
529
  function createToolHandlers(signer, options = {}) {
178
530
  return {
179
531
  haven_sign: async (input) => runTool(async () => {
180
532
  const args = parse("haven_sign", coerceX402Expected(input));
533
+ const typedData = resolveTypedData(args);
181
534
  const x402Expected = args.x402_expected ? toExpectedX402(args.x402_expected) : null;
182
- const result = x402Expected ? signer.signX402FundingHash(args.payload_hash, x402Expected) : null;
535
+ const result = x402Expected ? await signFundingLeg(signer, x402Expected, args.payload_hash, typedData) : null;
183
536
  if (!result) {
537
+ if (typedData) {
538
+ const signature2 = await signer.signDelegationTypedData(typedData);
539
+ await auditSigning("haven_sign", args.payload_hash);
540
+ return { signature: signature2 };
541
+ }
184
542
  const signature = signer.signPaymentHash(args.payload_hash);
185
543
  await auditSigning("haven_sign", args.payload_hash);
186
544
  return { signature };
@@ -202,7 +560,12 @@ function createToolHandlers(signer, options = {}) {
202
560
  }),
203
561
  haven_sign_x402: async (input) => runTool(async () => {
204
562
  const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
205
- const funding = signer.signX402FundingHash(args.payload_hash, toExpectedX402(args.x402_expected));
563
+ const funding = await signFundingLeg(
564
+ signer,
565
+ toExpectedX402(args.x402_expected),
566
+ args.payload_hash,
567
+ resolveTypedData(args)
568
+ );
206
569
  const header = await signer.buildX402PaymentHeader(
207
570
  args.payment_required,
208
571
  funding.x402Binding
@@ -431,216 +794,6 @@ async function writeAckFile(path$1, hash) {
431
794
  "utf8"
432
795
  );
433
796
  }
434
- function createEdgeSigner(delegateKey, options = {}) {
435
- let delegateAddress;
436
- try {
437
- delegateAddress = sdk.addressFromKey(delegateKey);
438
- } catch (err) {
439
- throw new sdk.HavenSigningError(
440
- `Invalid delegate key: ${err instanceof Error ? err.message : String(err)}`
441
- );
442
- }
443
- const x402Bindings = /* @__PURE__ */ new Map();
444
- function signAndVerify(hash) {
445
- const signature = sdk.signHash(delegateKey, hash);
446
- if (!sdk.verifySignature(hash, signature, delegateAddress)) {
447
- throw new sdk.HavenSigningError(
448
- "Local signature verification failed \u2014 recovered address does not match the delegate key."
449
- );
450
- }
451
- return signature;
452
- }
453
- return {
454
- delegateAddress,
455
- signPaymentHash(hash) {
456
- return signAndVerify(hash);
457
- },
458
- signX402FundingHash(hash, expected) {
459
- assertExpectedBinding(hash, expected, options.x402BindingSigner);
460
- const signature = signAndVerify(hash);
461
- const x402Binding = crypto.randomUUID();
462
- x402Bindings.set(x402Binding, { ...expected });
463
- return { signature, x402Binding };
464
- },
465
- async buildX402PaymentHeader(paymentRequired, x402Binding) {
466
- const expected = x402Bindings.get(x402Binding);
467
- if (!expected) {
468
- throw new sdk.HavenSigningError(
469
- "x402 funding binding is required before signing a merchant header. Sign the hosted funding hash with x402_expected first."
470
- );
471
- }
472
- try {
473
- assertX402PaymentWindowOpen(expected);
474
- } catch (err) {
475
- x402Bindings.delete(x402Binding);
476
- throw err;
477
- }
478
- const option = sdk.selectStandardPaymentOption(paymentRequired.accepts);
479
- if (!option) {
480
- throw new sdk.HavenApiError(
481
- "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
482
- 400
483
- );
484
- }
485
- assertX402MatchesExpected(paymentRequired, option, expected);
486
- const account = accounts.privateKeyToAccount(delegateKey);
487
- const requirements = sdk.toStandardPaymentRequirements(paymentRequired, option);
488
- const header = await schemes.exact.evm.createPaymentHeader(
489
- account,
490
- paymentRequired.x402Version,
491
- requirements
492
- );
493
- if (paymentRequired.x402Version < 2) {
494
- x402Bindings.delete(x402Binding);
495
- return { paymentHeader: header, accepted: option };
496
- }
497
- try {
498
- const payment = sdk.decodeBase64Json(header);
499
- const wrapped = sdk.encodeBase64Json({
500
- x402Version: paymentRequired.x402Version,
501
- accepted: option,
502
- payload: payment.payload
503
- });
504
- return { paymentHeader: wrapped, accepted: option };
505
- } finally {
506
- x402Bindings.delete(x402Binding);
507
- }
508
- },
509
- async signSweepAuthorization({
510
- authorization,
511
- expectedAuth,
512
- expectedSafe
513
- }) {
514
- assertSweepBinding(authorization, expectedAuth, options.x402BindingSigner);
515
- if (!sameAddress(authorization.from, delegateAddress)) {
516
- throw new sdk.HavenSigningError(
517
- "Sweep authorization `from` does not match this delegate address."
518
- );
519
- }
520
- if (expectedSafe && !sameAddress(authorization.to, expectedSafe)) {
521
- throw new sdk.HavenSigningError(
522
- "Sweep authorization `to` does not match the Safe in the local credential."
523
- );
524
- }
525
- const typedData = sdk.buildSweepTypedData(authorization);
526
- const viemTypedData = {
527
- domain: {
528
- ...typedData.domain,
529
- verifyingContract: typedData.domain.verifyingContract
530
- },
531
- types: typedData.types,
532
- primaryType: typedData.primaryType,
533
- message: {
534
- ...typedData.message,
535
- from: typedData.message.from,
536
- to: typedData.message.to,
537
- nonce: typedData.message.nonce
538
- }
539
- };
540
- const account = accounts.privateKeyToAccount(delegateKey);
541
- const signature = await account.signTypedData(viemTypedData);
542
- const recovered = await viem.recoverTypedDataAddress({ ...viemTypedData, signature });
543
- if (!sameAddress(recovered, delegateAddress)) {
544
- throw new sdk.HavenSigningError(
545
- "Local sweep signature verification failed \u2014 recovered address does not match the delegate key."
546
- );
547
- }
548
- return { signature };
549
- }
550
- };
551
- }
552
- function assertSweepBinding(authorization, expectedAuth, trustedSigner) {
553
- if (!expectedAuth || typeof expectedAuth !== "object") {
554
- throw new sdk.HavenSigningError("Sweep authorization binding is required before signing.");
555
- }
556
- if (!trustedSigner) {
557
- throw new sdk.HavenSigningError(
558
- "Sweep binding verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing sweep authorizations."
559
- );
560
- }
561
- const message = sdk.buildSweepAuthorizationMessage(authorization);
562
- if (expectedAuth.version !== 1 || expectedAuth.message !== message) {
563
- throw new sdk.HavenSigningError("Sweep authorization binding does not match the authorization being signed.");
564
- }
565
- if (!sameAddress(expectedAuth.signer, trustedSigner)) {
566
- throw new sdk.HavenSigningError("Sweep authorization binding was not signed by the configured Haven signer.");
567
- }
568
- if (!sdk.verifySignature(viem.hashMessage(message), expectedAuth.signature, trustedSigner)) {
569
- throw new sdk.HavenSigningError("Sweep authorization binding signature could not be verified.");
570
- }
571
- }
572
- function assertX402MatchesExpected(paymentRequired, option, expected) {
573
- assertExpectedShape(expected);
574
- const headerResource = option.resource ?? paymentRequired.resource.url;
575
- if (headerResource !== expected.resourceUrl) {
576
- throw new sdk.HavenSigningError("x402 payment_required resource does not match the funded intent.");
577
- }
578
- if (!sameAddress(option.payTo, expected.merchantTo)) {
579
- throw new sdk.HavenSigningError("x402 merchant recipient does not match the funded intent.");
580
- }
581
- if (sdk.x402AuthorizationAmount(option) !== expected.amount) {
582
- throw new sdk.HavenSigningError("x402 amount does not match the funded intent.");
583
- }
584
- if (!sameAddress(option.asset, expected.asset)) {
585
- throw new sdk.HavenSigningError("x402 asset does not match the funded intent.");
586
- }
587
- if (option.network !== expected.network) {
588
- throw new sdk.HavenSigningError("x402 network does not match the funded intent.");
589
- }
590
- }
591
- function assertExpectedShape(expected) {
592
- if (!expected || typeof expected !== "object") {
593
- throw new sdk.HavenSigningError("x402 expected funding context is required before signing a merchant header.");
594
- }
595
- }
596
- function assertExpectedBinding(payloadHash, expected, trustedSigner) {
597
- assertExpectedShape(expected);
598
- if (!trustedSigner) {
599
- throw new sdk.HavenSigningError(
600
- "x402 expected-context verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing x402 funding hashes."
601
- );
602
- }
603
- if (expected.payloadHash.toLowerCase() !== payloadHash.toLowerCase()) {
604
- throw new sdk.HavenSigningError("x402 expected context does not match the funding hash being signed.");
605
- }
606
- const message = sdk.buildX402ExpectedMessage({
607
- paymentId: expected.paymentId,
608
- payloadHash: expected.payloadHash,
609
- resourceUrl: expected.resourceUrl,
610
- merchantTo: expected.merchantTo,
611
- amount: expected.amount,
612
- asset: expected.asset,
613
- network: expected.network,
614
- expiresAt: expected.expiresAt
615
- });
616
- if (expected.auth?.version !== 1 || expected.auth.message !== message) {
617
- throw new sdk.HavenSigningError("x402 expected context authentication message is invalid.");
618
- }
619
- if (!sameAddress(expected.auth.signer, trustedSigner)) {
620
- throw new sdk.HavenSigningError("x402 expected context was not signed by the configured Haven signer.");
621
- }
622
- if (!sdk.verifySignature(viem.hashMessage(message), expected.auth.signature, trustedSigner)) {
623
- throw new sdk.HavenSigningError("x402 expected context signature could not be verified.");
624
- }
625
- }
626
- function assertX402PaymentWindowOpen(expected) {
627
- if (!expected.expiresAt) return;
628
- const expiresAtMs = Date.parse(expected.expiresAt);
629
- if (Number.isNaN(expiresAtMs)) {
630
- throw new sdk.HavenSigningError("x402 expected context expiresAt is not a valid ISO timestamp.");
631
- }
632
- if (expiresAtMs <= Date.now()) {
633
- throw new sdk.HavenError(
634
- "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.",
635
- sdk.AgentPaymentFailureCode.PaymentWindowExpired,
636
- 410,
637
- expected.paymentId
638
- );
639
- }
640
- }
641
- function sameAddress(a, b) {
642
- return a.toLowerCase() === b.toLowerCase();
643
- }
644
797
  async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
645
798
  if (path) return loadFromFile(path);
646
799
  const envKey = stringField(process.env.HAVEN_DELEGATE_KEY);
@@ -731,8 +884,9 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
731
884
 
732
885
  // src/server.ts
733
886
  var SIGNER_NAME = "@haven_ai/signer";
734
- var SIGNER_VERSION = "0.1.17-alpha.0";
887
+ var SIGNER_VERSION = "0.1.19-alpha.0";
735
888
  async function resolveSignerRuntime(options = {}) {
889
+ assertSupportedNodeVersion(options.nodeVersion);
736
890
  if (options.delegateKey) {
737
891
  return {
738
892
  signer: createEdgeSigner(options.delegateKey, {
@@ -749,7 +903,13 @@ async function resolveSignerRuntime(options = {}) {
749
903
  };
750
904
  }
751
905
  function buildSignerMcpServer(signer, options = {}) {
752
- const server = new mcp_js.McpServer({ name: SIGNER_NAME, version: SIGNER_VERSION });
906
+ const server = new mcp_js.McpServer(
907
+ { name: SIGNER_NAME, version: SIGNER_VERSION },
908
+ {
909
+ capabilities: signerCapabilityAdvertisement(),
910
+ instructions: signerInstructions()
911
+ }
912
+ );
753
913
  const credentialsPath = options.credentials?.sourcePath;
754
914
  const handlers = createToolHandlers(signer, {
755
915
  audit: {
@@ -770,6 +930,14 @@ function buildSignerMcpServer(signer, options = {}) {
770
930
  }
771
931
  return server;
772
932
  }
933
+ function assertSupportedNodeVersion(nodeVersion = process.versions.node) {
934
+ if (sdk.isSupportedNodeVersion(nodeVersion)) return;
935
+ const err = new Error(
936
+ sdk.unsupportedNodeVersionMessage({ subject: "The Haven signer", nodeVersion })
937
+ );
938
+ err.code = "HAVEN_SIGNER_UNSUPPORTED_NODE";
939
+ throw err;
940
+ }
773
941
  async function runSignerStdioServer(options = {}) {
774
942
  const { signer, credentials } = await resolveSignerRuntime(options);
775
943
  if (!options.skipConsent) {