@haven_ai/signer 0.1.17-alpha.0 → 0.1.18-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
@@ -56,8 +56,9 @@ var sweepAuthorizationSchema = v3.z.object({
56
56
  token: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "token must be a 0x address"),
57
57
  chainId: v3.z.number().int().positive()
58
58
  });
59
+ var bindingVersionSchema = v3.z.number().int().positive();
59
60
  var sweepExpectedAuthSchema = v3.z.object({
60
- version: v3.z.literal(1),
61
+ version: bindingVersionSchema,
61
62
  message: v3.z.string().min(1),
62
63
  signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
63
64
  signer: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "signer must be a 0x address")
@@ -75,8 +76,13 @@ var x402ExpectedSchema = v3.z.object({
75
76
  // Omitting it used to fail downstream with a cryptic "authentication message is
76
77
  // invalid" — making it required surfaces a clear INVALID_INPUT at the boundary.
77
78
  expires_at: v3.z.string().min(1),
79
+ // #1138: present on a delegation-rail (v2) context. It commits to the EIP-712
80
+ // typed data the account validates; the signer refuses to raw-sign the bare
81
+ // hash when it is present, and refuses to sign typed data when it is absent.
82
+ typed_data_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "typed_data_hash must be a 0x-prefixed hex string").optional(),
78
83
  auth: v3.z.object({
79
- version: v3.z.literal(1),
84
+ // Open at the boundary, enforced in the signer — see bindingVersionSchema.
85
+ version: bindingVersionSchema,
80
86
  message: v3.z.string().min(1),
81
87
  signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
82
88
  signer: v3.z.string().min(1)
@@ -97,7 +103,10 @@ var toolSchemas = {
97
103
  // Pass x402.expected from hosted haven_pay_x402_quote when this hash funds
98
104
  // a standard x402 merchant retry. The signer records it locally and returns
99
105
  // an opaque x402_binding for the later header-signing step.
100
- x402_expected: x402ExpectedSchema.optional()
106
+ x402_expected: x402ExpectedSchema.optional(),
107
+ // #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
108
+ // (not z.unknown()) so MCP clients embed it as JSON rather than a string.
109
+ typed_data: v3.z.record(v3.z.string(), v3.z.unknown()).optional()
101
110
  },
102
111
  haven_x402_sign_header: {
103
112
  // The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
@@ -111,7 +120,10 @@ var toolSchemas = {
111
120
  // One-shot x402 signing: funding hash + merchant header in one local call.
112
121
  payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
113
122
  x402_expected: x402ExpectedSchema,
114
- payment_required: v3.z.record(v3.z.string(), v3.z.unknown())
123
+ payment_required: v3.z.record(v3.z.string(), v3.z.unknown()),
124
+ // #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
125
+ // (not z.unknown()) so MCP clients embed it as JSON rather than a string.
126
+ typed_data: v3.z.record(v3.z.string(), v3.z.unknown()).optional()
115
127
  }
116
128
  };
117
129
  var SIGN_DESCRIPTION = [
@@ -119,7 +131,9 @@ var SIGN_DESCRIPTION = [
119
131
  "this process. Pass the payload_hash returned by haven_pay or haven_pay_x402_quote.",
120
132
  "For x402, also pass x402_expected from haven_pay_x402_quote; the signer records it locally",
121
133
  "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",
134
+ "window closes. DELEGATION-RAIL accounts: when the hosted result carries typed_data, pass it too \u2014",
135
+ "the account validates that EIP-712 payload, not payload_hash, and signing is refused without it.",
136
+ "Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
123
137
  "to mcp__haven-signer__haven_x402_sign_header. For plain SafeTransfer payments, just pass payload_hash and",
124
138
  "relay the returned signature via mcp__haven__haven_submit."
125
139
  ].join(" ");
@@ -139,7 +153,10 @@ var SIGN_X402_DESCRIPTION = [
139
153
  "X-PAYMENT header in a single local call (equivalent to haven_sign followed by",
140
154
  "haven_x402_sign_header). The delegate key never leaves this process. From the haven_pay_mcp_tool",
141
155
  "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",
156
+ "object is also accepted and unwrapped for you), and payment_required (verbatim). On a",
157
+ "delegation-rail account the result also carries typed_data \u2014 pass that verbatim too; the signer",
158
+ "signs it instead of payload_hash and refuses the bare hash when the context commits to typed data.",
159
+ "Returns",
143
160
  "{ signature, x402_binding, payment_header, accepted }; hand signature + payment_header to",
144
161
  "mcp__haven__haven_settle_mcp_tool to fund and settle in one hosted call. The header is built now (before",
145
162
  "funding confirms), so its short validity window starts here \u2014 call mcp__haven__haven_settle_mcp_tool promptly,",
@@ -161,10 +178,22 @@ var toolDescriptions = {
161
178
  haven_sign_x402: SIGN_X402_DESCRIPTION,
162
179
  haven_sign_sweep_delegate: SIGN_SWEEP_DELEGATE_DESCRIPTION
163
180
  };
181
+ async function signFundingLeg(signer, expected, payloadHash, typedData) {
182
+ if (!expected.typedDataHash) {
183
+ return signer.signX402FundingHash(payloadHash, expected);
184
+ }
185
+ if (!typedData) {
186
+ throw new sdk.HavenSigningError(
187
+ "This x402 funding intent is on the delegation rail: it commits to EIP-712 typed data, which was not supplied. Pass sign_data.typed_data from haven_pay_mcp_tool / haven_pay_x402_quote verbatim \u2014 the bare payload_hash is not what the account validates."
188
+ );
189
+ }
190
+ return signer.signX402FundingTypedData(typedData, expected);
191
+ }
164
192
  function toExpectedX402(raw) {
165
193
  return {
166
194
  paymentId: raw.payment_id,
167
195
  payloadHash: raw.payload_hash,
196
+ typedDataHash: raw.typed_data_hash,
168
197
  resourceUrl: raw.resource_url,
169
198
  merchantTo: raw.merchant_to,
170
199
  amount: raw.amount,
@@ -179,7 +208,7 @@ function createToolHandlers(signer, options = {}) {
179
208
  haven_sign: async (input) => runTool(async () => {
180
209
  const args = parse("haven_sign", coerceX402Expected(input));
181
210
  const x402Expected = args.x402_expected ? toExpectedX402(args.x402_expected) : null;
182
- const result = x402Expected ? signer.signX402FundingHash(args.payload_hash, x402Expected) : null;
211
+ const result = x402Expected ? await signFundingLeg(signer, x402Expected, args.payload_hash, args.typed_data) : null;
183
212
  if (!result) {
184
213
  const signature = signer.signPaymentHash(args.payload_hash);
185
214
  await auditSigning("haven_sign", args.payload_hash);
@@ -202,7 +231,12 @@ function createToolHandlers(signer, options = {}) {
202
231
  }),
203
232
  haven_sign_x402: async (input) => runTool(async () => {
204
233
  const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
205
- const funding = signer.signX402FundingHash(args.payload_hash, toExpectedX402(args.x402_expected));
234
+ const funding = await signFundingLeg(
235
+ signer,
236
+ toExpectedX402(args.x402_expected),
237
+ args.payload_hash,
238
+ args.typed_data
239
+ );
206
240
  const header = await signer.buildX402PaymentHeader(
207
241
  args.payment_required,
208
242
  funding.x402Binding
@@ -456,12 +490,28 @@ function createEdgeSigner(delegateKey, options = {}) {
456
490
  return signAndVerify(hash);
457
491
  },
458
492
  signX402FundingHash(hash, expected) {
459
- assertExpectedBinding(hash, expected, options.x402BindingSigner);
493
+ assertExpectedBinding(hash, expected, options.x402BindingSigner, "hash");
460
494
  const signature = signAndVerify(hash);
461
495
  const x402Binding = crypto.randomUUID();
462
496
  x402Bindings.set(x402Binding, { ...expected });
463
497
  return { signature, x402Binding };
464
498
  },
499
+ async signX402FundingTypedData(typedData, expected) {
500
+ assertExpectedBinding(expected.payloadHash, expected, options.x402BindingSigner, "typed-data");
501
+ const digest = viem.hashTypedData(typedData);
502
+ if (digest.toLowerCase() !== expected.typedDataHash?.toLowerCase()) {
503
+ throw new sdk.HavenSigningError(
504
+ "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."
505
+ );
506
+ }
507
+ const account = accounts.privateKeyToAccount(delegateKey);
508
+ const signature = await account.signTypedData(
509
+ typedData
510
+ );
511
+ const x402Binding = crypto.randomUUID();
512
+ x402Bindings.set(x402Binding, { ...expected });
513
+ return { signature, x402Binding };
514
+ },
465
515
  async buildX402PaymentHeader(paymentRequired, x402Binding) {
466
516
  const expected = x402Bindings.get(x402Binding);
467
517
  if (!expected) {
@@ -558,8 +608,13 @@ function assertSweepBinding(authorization, expectedAuth, trustedSigner) {
558
608
  "Sweep binding verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing sweep authorizations."
559
609
  );
560
610
  }
611
+ assertSupportedBindingVersion(
612
+ expectedAuth.version,
613
+ SUPPORTED_SWEEP_BINDING_VERSIONS,
614
+ "sweep authorization binding"
615
+ );
561
616
  const message = sdk.buildSweepAuthorizationMessage(authorization);
562
- if (expectedAuth.version !== 1 || expectedAuth.message !== message) {
617
+ if (expectedAuth.message !== message) {
563
618
  throw new sdk.HavenSigningError("Sweep authorization binding does not match the authorization being signed.");
564
619
  }
565
620
  if (!sameAddress(expectedAuth.signer, trustedSigner)) {
@@ -593,16 +648,43 @@ function assertExpectedShape(expected) {
593
648
  throw new sdk.HavenSigningError("x402 expected funding context is required before signing a merchant header.");
594
649
  }
595
650
  }
596
- function assertExpectedBinding(payloadHash, expected, trustedSigner) {
651
+ var SUPPORTED_X402_EXPECTED_VERSIONS = [1, 2];
652
+ var SUPPORTED_SWEEP_BINDING_VERSIONS = [1];
653
+ function assertSupportedBindingVersion(received, supported, context) {
654
+ if (supported.includes(received)) return;
655
+ const highest = Math.max(...supported);
656
+ 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(", ")}.`;
657
+ throw new sdk.HavenSigningError(
658
+ `${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.`
659
+ );
660
+ }
661
+ function assertExpectedBinding(payloadHash, expected, trustedSigner, mode = "hash") {
597
662
  assertExpectedShape(expected);
598
663
  if (!trustedSigner) {
599
664
  throw new sdk.HavenSigningError(
600
665
  "x402 expected-context verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing x402 funding hashes."
601
666
  );
602
667
  }
668
+ if (expected.auth) {
669
+ assertSupportedBindingVersion(
670
+ expected.auth.version,
671
+ SUPPORTED_X402_EXPECTED_VERSIONS,
672
+ "x402 expected context"
673
+ );
674
+ }
603
675
  if (expected.payloadHash.toLowerCase() !== payloadHash.toLowerCase()) {
604
676
  throw new sdk.HavenSigningError("x402 expected context does not match the funding hash being signed.");
605
677
  }
678
+ if (mode === "hash" && expected.typedDataHash) {
679
+ throw new sdk.HavenSigningError(
680
+ "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."
681
+ );
682
+ }
683
+ if (mode === "typed-data" && !expected.typedDataHash) {
684
+ throw new sdk.HavenSigningError(
685
+ "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."
686
+ );
687
+ }
606
688
  const message = sdk.buildX402ExpectedMessage({
607
689
  paymentId: expected.paymentId,
608
690
  payloadHash: expected.payloadHash,
@@ -611,9 +693,11 @@ function assertExpectedBinding(payloadHash, expected, trustedSigner) {
611
693
  amount: expected.amount,
612
694
  asset: expected.asset,
613
695
  network: expected.network,
614
- expiresAt: expected.expiresAt
696
+ expiresAt: expected.expiresAt,
697
+ typedDataHash: expected.typedDataHash
615
698
  });
616
- if (expected.auth?.version !== 1 || expected.auth.message !== message) {
699
+ const expectedVersion = expected.typedDataHash ? 2 : 1;
700
+ if (expected.auth?.version !== expectedVersion || expected.auth.message !== message) {
617
701
  throw new sdk.HavenSigningError("x402 expected context authentication message is invalid.");
618
702
  }
619
703
  if (!sameAddress(expected.auth.signer, trustedSigner)) {
@@ -731,7 +815,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
731
815
 
732
816
  // src/server.ts
733
817
  var SIGNER_NAME = "@haven_ai/signer";
734
- var SIGNER_VERSION = "0.1.17-alpha.0";
818
+ var SIGNER_VERSION = "0.1.18-alpha.0";
735
819
  async function resolveSignerRuntime(options = {}) {
736
820
  if (options.delegateKey) {
737
821
  return {