@haven_ai/signer 0.1.19-alpha.0 → 0.1.21-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
@@ -325,8 +325,10 @@ function signerCapabilityAdvertisement() {
325
325
  function signerInstructions() {
326
326
  const compatibility = signerCompatibility();
327
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.",
328
+ "Haven edge signer: sign-only tools bound to the local delegate key. It never emits the",
329
+ "key. Its one network capability is an authenticated READ of a signing context from",
330
+ "Haven by payment_id \u2014 pass payment_id to haven_sign / haven_sign_x402 (preferred for",
331
+ "delegation-rail x402) instead of relaying bulky typed-data payloads yourself.",
330
332
  "",
331
333
  "Version compatibility (check this BEFORE signing, not after):",
332
334
  `- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
@@ -340,6 +342,55 @@ function signerInstructions() {
340
342
  "Haven-signed binding message, so changing it invalidates the signature."
341
343
  ].join("\n");
342
344
  }
345
+ async function loadHavenIdentity(credentialsPath) {
346
+ if (!credentialsPath) return null;
347
+ try {
348
+ const raw = JSON.parse(
349
+ await promises.readFile(path.join(path.dirname(credentialsPath), "identity.json"), "utf8")
350
+ );
351
+ const apiKey = typeof raw.api_key === "string" ? raw.api_key : void 0;
352
+ const apiUrl = typeof raw.api_url === "string" ? raw.api_url : void 0;
353
+ if (!apiKey || !apiUrl) return null;
354
+ return { apiKey, apiUrl: apiUrl.replace(/\/+$/, "") };
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch) {
360
+ let response;
361
+ try {
362
+ response = await fetchImpl(
363
+ `${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
364
+ { headers: { Authorization: `Bearer ${identity.apiKey}` } }
365
+ );
366
+ } catch (err) {
367
+ throw new sdk.HavenSigningError(
368
+ `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.`
369
+ );
370
+ }
371
+ const body = await response.json().catch(() => ({}));
372
+ if (!response.ok) {
373
+ const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
374
+ throw new sdk.HavenSigningError(
375
+ `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." : "")
376
+ );
377
+ }
378
+ const signData = body.sign_data;
379
+ const x402Expected = body.x402_expected;
380
+ if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
381
+ throw new sdk.HavenSigningError(
382
+ "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."
383
+ );
384
+ }
385
+ return {
386
+ paymentId: String(body.payment_id ?? paymentId),
387
+ payloadHash: signData.hash,
388
+ typedData: signData.typed_data,
389
+ x402Expected
390
+ };
391
+ }
392
+
393
+ // src/tools.ts
343
394
  var sweepAuthorizationSchema = v3.z.object({
344
395
  from: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "from must be a 0x address"),
345
396
  to: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "to must be a 0x address"),
@@ -357,7 +408,7 @@ var sweepExpectedAuthSchema = v3.z.object({
357
408
  signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
358
409
  signer: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "signer must be a 0x address")
359
410
  });
360
- var x402ExpectedSchema = v3.z.object({
411
+ var x402ExpectedShape = {
361
412
  payment_id: v3.z.string().min(1),
362
413
  payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
363
414
  resource_url: v3.z.string().url(),
@@ -381,7 +432,8 @@ var x402ExpectedSchema = v3.z.object({
381
432
  signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
382
433
  signer: v3.z.string().min(1)
383
434
  })
384
- });
435
+ };
436
+ var x402ExpectedSchema = v3.z.object(x402ExpectedShape);
385
437
  var toolSchemas = {
386
438
  haven_sign_sweep_delegate: {
387
439
  // The authorization fields prepared by Haven's POST /sweep/prepare. Passed
@@ -392,8 +444,14 @@ var toolSchemas = {
392
444
  expected_auth: sweepExpectedAuthSchema
393
445
  },
394
446
  haven_sign: {
447
+ // #1263: THE preferred x402 input — the signer fetches the exact signing
448
+ // payload + expected context from Haven itself (authenticated with the
449
+ // locally-stored agent credential), so no bulky bytes ever cross the
450
+ // model. Pass payment_id ALONE for a delegation-rail x402 funding intent.
451
+ payment_id: v3.z.string().min(1).optional(),
395
452
  // The unsigned hash from haven_pay / haven_pay_x402_quote (payload_hash).
396
- payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
453
+ // Optional when payment_id is supplied (the fetch carries it).
454
+ payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
397
455
  // Pass x402.expected from hosted haven_pay_x402_quote when this hash funds
398
456
  // a standard x402 merchant retry. The signer records it locally and returns
399
457
  // an opaque x402_binding for the later header-signing step.
@@ -420,9 +478,13 @@ var toolSchemas = {
420
478
  x402_binding: v3.z.string().min(1)
421
479
  },
422
480
  haven_sign_x402: {
481
+ // #1263: preferred — pass payment_id (plus payment_required) and the
482
+ // signer fetches the exact signing payload + expected context itself.
483
+ payment_id: v3.z.string().min(1).optional(),
423
484
  // One-shot x402 signing: funding hash + merchant header in one local call.
424
- payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
425
- x402_expected: x402ExpectedSchema,
485
+ // Both optional when payment_id is supplied (the fetch carries them).
486
+ payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
487
+ x402_expected: x402ExpectedSchema.optional(),
426
488
  payment_required: v3.z.record(v3.z.string(), v3.z.unknown()),
427
489
  // #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
428
490
  // (not z.unknown()) so MCP clients embed it as JSON rather than a string.
@@ -436,9 +498,10 @@ var SIGN_DESCRIPTION = [
436
498
  "this process. Pass the payload_hash returned by haven_pay or haven_pay_x402_quote.",
437
499
  "For x402, also pass x402_expected from haven_pay_x402_quote; the signer records it locally",
438
500
  "and returns { signature, x402_binding }. x402_expected includes expires_at; sign before that",
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.",
501
+ "window closes. DELEGATION-RAIL x402 accounts (#1263): pass payment_id ALONE (preferred) \u2014 this",
502
+ "signer fetches the exact signing payload and expected context from Haven itself, so nothing",
503
+ "bulky ever crosses your context. Fallback: pass typed_data_b64 through UNCHANGED (never re-type",
504
+ "the nested typed_data JSON); the account validates that EIP-712 payload, not payload_hash.",
442
505
  "Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
443
506
  "to mcp__haven-signer__haven_x402_sign_header. For plain SafeTransfer payments, just pass payload_hash and",
444
507
  "relay the returned signature via mcp__haven__haven_submit."
@@ -458,11 +521,12 @@ var SIGN_X402_DESCRIPTION = [
458
521
  "One-shot x402 signing for the fast 3-call flow: sign the funding hash AND build the EIP-3009",
459
522
  "X-PAYMENT header in a single local call (equivalent to haven_sign followed by",
460
523
  "haven_x402_sign_header). The delegate key never leaves this process. From the haven_pay_mcp_tool",
461
- "result pass payload_hash, x402_expected (the nested x402.expected object \u2014 passing the whole x402",
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.",
524
+ "result pass payment_id and payment_required \u2014 PREFERRED (#1263): this signer fetches the exact",
525
+ "signing payload and expected context from Haven itself, so nothing bulky crosses your context.",
526
+ "Fallback for older backends: pass payload_hash, x402_expected (the nested x402.expected object \u2014",
527
+ "passing the whole x402 object is also accepted and unwrapped for you), and typed_data_b64",
528
+ "through UNCHANGED; the signer",
529
+ "signs the typed data instead of payload_hash and refuses the bare hash when the context commits to typed data.",
466
530
  "Returns",
467
531
  "{ signature, x402_binding, payment_header, accepted }; hand signature + payment_header to",
468
532
  "mcp__haven__haven_settle_mcp_tool to fund and settle in one hosted call. The header is built now (before",
@@ -510,6 +574,15 @@ function toExpectedX402(raw) {
510
574
  auth: raw.auth
511
575
  };
512
576
  }
577
+ function parseFetchedExpected(fetched) {
578
+ const parsed = v3.z.object(x402ExpectedShape).safeParse(fetched.x402Expected);
579
+ if (!parsed.success) {
580
+ throw new sdk.HavenSigningError(
581
+ `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"}`
582
+ );
583
+ }
584
+ return parsed.data;
585
+ }
513
586
  function resolveTypedData(args) {
514
587
  if (!args.typed_data_b64) return args.typed_data;
515
588
  try {
@@ -527,23 +600,51 @@ function resolveTypedData(args) {
527
600
  }
528
601
  }
529
602
  function createToolHandlers(signer, options = {}) {
603
+ async function resolveSignContext(args) {
604
+ if (!args.payment_id) return null;
605
+ const identity = await options.signContext?.loadIdentity() ?? null;
606
+ if (!identity) {
607
+ throw new sdk.HavenSigningError(
608
+ "payment_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run `npx @haven_ai/connect@alpha` to restore it, or pass typed_data_b64 from the quote result instead."
609
+ );
610
+ }
611
+ const ctx = await fetchX402SignContext(
612
+ identity,
613
+ args.payment_id,
614
+ options.signContext?.fetchImpl
615
+ );
616
+ if (args.payload_hash && args.payload_hash.toLowerCase() !== ctx.payloadHash.toLowerCase()) {
617
+ throw new sdk.HavenSigningError(
618
+ `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.`
619
+ );
620
+ }
621
+ return ctx;
622
+ }
530
623
  return {
531
624
  haven_sign: async (input) => runTool(async () => {
532
625
  const args = parse("haven_sign", coerceX402Expected(input));
533
- const typedData = resolveTypedData(args);
534
- const x402Expected = args.x402_expected ? toExpectedX402(args.x402_expected) : null;
535
- const result = x402Expected ? await signFundingLeg(signer, x402Expected, args.payload_hash, typedData) : null;
626
+ const fetched = await resolveSignContext(args);
627
+ const typedData = fetched?.typedData ?? resolveTypedData(args);
628
+ const payloadHash = fetched?.payloadHash ?? args.payload_hash;
629
+ if (!payloadHash) {
630
+ throw new sdk.HavenSigningError(
631
+ "Pass payment_id (preferred for delegation-rail x402) or payload_hash."
632
+ );
633
+ }
634
+ const expectedRaw = fetched ? parseFetchedExpected(fetched) : args.x402_expected;
635
+ const x402Expected = expectedRaw ? toExpectedX402(expectedRaw) : null;
636
+ const result = x402Expected ? await signFundingLeg(signer, x402Expected, payloadHash, typedData) : null;
536
637
  if (!result) {
537
638
  if (typedData) {
538
639
  const signature2 = await signer.signDelegationTypedData(typedData);
539
- await auditSigning("haven_sign", args.payload_hash);
640
+ await auditSigning("haven_sign", payloadHash);
540
641
  return { signature: signature2 };
541
642
  }
542
- const signature = signer.signPaymentHash(args.payload_hash);
543
- await auditSigning("haven_sign", args.payload_hash);
643
+ const signature = signer.signPaymentHash(payloadHash);
644
+ await auditSigning("haven_sign", payloadHash);
544
645
  return { signature };
545
646
  }
546
- await auditSigning("haven_sign", args.payload_hash);
647
+ await auditSigning("haven_sign", payloadHash);
547
648
  return { signature: result.signature, x402_binding: result.x402Binding };
548
649
  }),
549
650
  haven_x402_sign_header: async (input) => runTool(async () => {
@@ -560,17 +661,25 @@ function createToolHandlers(signer, options = {}) {
560
661
  }),
561
662
  haven_sign_x402: async (input) => runTool(async () => {
562
663
  const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
664
+ const fetched = await resolveSignContext(args);
665
+ const payloadHash = fetched?.payloadHash ?? args.payload_hash;
666
+ const expectedRaw = fetched ? parseFetchedExpected(fetched) : args.x402_expected;
667
+ if (!payloadHash || !expectedRaw) {
668
+ throw new sdk.HavenSigningError(
669
+ "Pass payment_id (preferred \u2014 the signer fetches the signing context itself) or payload_hash + x402_expected from the quote result."
670
+ );
671
+ }
563
672
  const funding = await signFundingLeg(
564
673
  signer,
565
- toExpectedX402(args.x402_expected),
566
- args.payload_hash,
567
- resolveTypedData(args)
674
+ toExpectedX402(expectedRaw),
675
+ payloadHash,
676
+ fetched?.typedData ?? resolveTypedData(args)
568
677
  );
569
678
  const header = await signer.buildX402PaymentHeader(
570
679
  args.payment_required,
571
680
  funding.x402Binding
572
681
  );
573
- await auditSigning("haven_sign_x402", args.payload_hash);
682
+ await auditSigning("haven_sign_x402", payloadHash);
574
683
  await auditSigning("haven_sign_x402", hashPayloadForAudit(args.payment_required));
575
684
  return {
576
685
  signature: funding.signature,
@@ -683,6 +792,7 @@ function normalizeError(err) {
683
792
 
684
793
  // src/consent.ts
685
794
  var SIGNER_ACK_ENV = "HAVEN_SIGNER_ACK";
795
+ var SIGNER_CONSENT_SURFACE_VERSION = 2;
686
796
  function computeSignerConsentHash(input) {
687
797
  const identity = [
688
798
  input.delegateAddress.toLowerCase(),
@@ -693,7 +803,8 @@ function computeSignerConsentHash(input) {
693
803
  ].join("|");
694
804
  const toolCanonical = [...input.toolNames].sort().join(",");
695
805
  return crypto.createHash("sha256").update(`${identity}
696
- ${toolCanonical}`).digest("hex").slice(0, 16);
806
+ ${toolCanonical}
807
+ surface:v${SIGNER_CONSENT_SURFACE_VERSION}`).digest("hex").slice(0, 16);
697
808
  }
698
809
  function renderSignerConsentBlock(input, hash) {
699
810
  const lines = [
@@ -711,7 +822,10 @@ function renderSignerConsentBlock(input, hash) {
711
822
  lines.push("");
712
823
  lines.push("This local signer holds the delegate key on this machine and signs");
713
824
  lines.push("payment payloads or x402 merchant headers for the delegate address above.");
714
- lines.push("It does not call the Haven API, so it cannot show a live allowance summary.");
825
+ lines.push("Its one network use is a read-only fetch of a pending payment's signing");
826
+ lines.push("payload from Haven, authenticated with the agent credential stored next to");
827
+ lines.push("this signer's key file - it never sends the key, a signature, or anything");
828
+ lines.push("else outbound, and it cannot show a live allowance summary.");
715
829
  lines.push("On-chain Safe rules remain the real spend gate, and the wallet owner can");
716
830
  lines.push("pause or revoke agent authority outside this signer.");
717
831
  lines.push("");
@@ -884,7 +998,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
884
998
 
885
999
  // src/server.ts
886
1000
  var SIGNER_NAME = "@haven_ai/signer";
887
- var SIGNER_VERSION = "0.1.19-alpha.0";
1001
+ var SIGNER_VERSION = "0.1.21-alpha.0";
888
1002
  async function resolveSignerRuntime(options = {}) {
889
1003
  assertSupportedNodeVersion(options.nodeVersion);
890
1004
  if (options.delegateKey) {
@@ -917,6 +1031,14 @@ function buildSignerMcpServer(signer, options = {}) {
917
1031
  delegateAddress: signer.delegateAddress,
918
1032
  safeAddress: options.credentials?.safeAddress,
919
1033
  chainId: options.credentials?.chainId
1034
+ },
1035
+ // #1263: the payment_id signing path — the ONLY network call this server
1036
+ // can make, an authenticated read of a signing context from Haven, using
1037
+ // the agent identity the connector stores next to the signer credential.
1038
+ // The signer CORE stays network-free; fetched bytes still pass the same
1039
+ // binding verification + digest re-derivation as tool-argument bytes.
1040
+ signContext: {
1041
+ loadIdentity: () => loadHavenIdentity(credentialsPath)
920
1042
  }
921
1043
  });
922
1044
  const registerTool = server.tool.bind(server);