@haven_ai/signer 0.2.1-alpha.0 → 0.4.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.js CHANGED
@@ -2,13 +2,14 @@ import { randomUUID, createHash } from 'crypto';
2
2
  import { recoverTypedDataAddress, hashTypedData, hashMessage } from 'viem';
3
3
  import { privateKeyToAccount } from 'viem/accounts';
4
4
  import { exact } from 'x402/schemes';
5
- import { addressFromKey, HavenSigningError, buildSweepTypedData, selectStandardPaymentOption, HavenApiError, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, buildSweepAuthorizationMessage, verifySignature, x402AuthorizationAmount, SignerRefusalCode, connectorRerunCommand, SIGNER_UPDATE_FALLBACK, HavenUnsupportedSignerVersionError, buildX402ExpectedMessage, HavenError, AgentPaymentFailureCode, AgentPaymentNextAction, isSupportedNodeVersion, unsupportedNodeVersionMessage, signHash } from '@haven_ai/sdk';
5
+ import { createNextStepBuilder, addressFromKey, HavenSigningError, buildSweepTypedData, selectStandardPaymentOption, HavenApiError, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, buildSweepAuthorizationMessage, verifySignature, x402AuthorizationAmount, SignerRefusalCode, connectorRerunCommand, SIGNER_UPDATE_FALLBACK, HavenUnsupportedSignerVersionError, buildX402ExpectedMessage, HavenError, AgentPaymentFailureCode, AgentPaymentNextAction, isSupportedNodeVersion, unsupportedNodeVersionMessage, signHash } from '@haven_ai/sdk';
6
6
  import { readFile, stat, mkdir, appendFile, writeFile } from 'fs/promises';
7
7
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
9
  import { homedir } from 'os';
10
10
  import { resolve, dirname, join } from 'path';
11
- import { z } from 'zod/v3';
11
+ import { z as z$1 } from 'zod/v3';
12
+ import { z } from 'zod';
12
13
 
13
14
  // src/core.ts
14
15
  var DELEGATION_MANAGER = "0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3";
@@ -494,7 +495,10 @@ function signerInstructions() {
494
495
  "",
495
496
  "A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
496
497
  "machine-readable, not just prose: it carries code, supported_versions, received_version, and",
497
- "fallback fields alongside the message, so you can branch on it directly."
498
+ "fallback fields alongside the message, so you can branch on it directly. Every signer",
499
+ "refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
500
+ "hosted tool follows (resolve the role against your own server names), else",
501
+ "next_tool_omitted_reason saying why not."
498
502
  ].join("\n");
499
503
  }
500
504
  async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
@@ -506,7 +510,6 @@ async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
506
510
  delegateKey: envKey,
507
511
  agentId: stringField(process.env.HAVEN_AGENT_ID),
508
512
  accountAddress,
509
- safeAddress: accountAddress,
510
513
  chainId: chainIdField(process.env.HAVEN_CHAIN_ID, "HAVEN_CHAIN_ID"),
511
514
  network: stringField(process.env.HAVEN_NETWORK),
512
515
  x402BindingSigner: stringField(process.env.HAVEN_X402_BINDING_SIGNER)
@@ -541,7 +544,6 @@ async function loadFromFile(path) {
541
544
  delegateKey,
542
545
  agentId: stringField(raw.agent_id ?? raw.agentId),
543
546
  accountAddress,
544
- safeAddress: accountAddress,
545
547
  chainId: chainIdField(raw.chain_id ?? raw.chainId, "chain_id"),
546
548
  network: stringField(raw.network),
547
549
  x402BindingSigner: stringField(
@@ -554,7 +556,22 @@ function readAccountAddressField(raw) {
554
556
  return stringField(raw.account_address ?? raw.safe_address ?? raw.safeAddress);
555
557
  }
556
558
  function readAccountAddressEnv(env) {
557
- return stringField(env.HAVEN_ACCOUNT_ADDRESS ?? env.HAVEN_WALLET_ADDRESS ?? env.HAVEN_SAFE_ADDRESS);
559
+ const current = stringField(env.HAVEN_ACCOUNT_ADDRESS);
560
+ for (const name of ["HAVEN_WALLET_ADDRESS", "HAVEN_SAFE_ADDRESS"]) {
561
+ const retired = stringField(env[name]);
562
+ if (!retired) continue;
563
+ if (!current) {
564
+ throw new Error(
565
+ `${name} is retired (#2906) \u2014 Haven accounts are addressed as accounts, not Safes. Set HAVEN_ACCOUNT_ADDRESS to the same value. Refusing rather than ignoring it, because an unset account address silently skips the sweep-destination check.`
566
+ );
567
+ }
568
+ if (retired.toLowerCase() !== current.toLowerCase()) {
569
+ throw new Error(
570
+ `${name} and HAVEN_ACCOUNT_ADDRESS were both set to different addresses. Remove the retired name, or make them match \u2014 picking one silently would hide the mismatch.`
571
+ );
572
+ }
573
+ }
574
+ return current;
558
575
  }
559
576
  function stringField(value) {
560
577
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -611,7 +628,7 @@ function createSigningAuditEntry(tool, payloadHash, context, now = /* @__PURE__
611
628
  payload_hash: payloadHash,
612
629
  delegate_address: context.delegateAddress
613
630
  };
614
- if (context.safeAddress) entry.safe_address = context.safeAddress;
631
+ if (context.accountAddress) entry.safe_address = context.accountAddress;
615
632
  if (typeof context.chainId === "number") entry.chain_id = context.chainId;
616
633
  return entry;
617
634
  }
@@ -627,6 +644,38 @@ function stableStringify(value) {
627
644
  const object = value;
628
645
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
629
646
  }
647
+ var SIGNER_HOSTED_HANDOFF_SHAPES = {
648
+ haven_get_payment_status: { payment_id: z.string().min(1) }
649
+ };
650
+ function target(shape) {
651
+ const schema = z.object(shape).strict();
652
+ return {
653
+ role: "hosted",
654
+ validate: (input) => {
655
+ const r = schema.safeParse(input ?? {});
656
+ return r.success ? null : r.error.errors.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join("; ");
657
+ }
658
+ };
659
+ }
660
+ var SIGNER_NEXT_STEP_TARGETS = {
661
+ haven_get_payment_status: target(SIGNER_HOSTED_HANDOFF_SHAPES.haven_get_payment_status)
662
+ };
663
+ var nextStep = createNextStepBuilder(SIGNER_NEXT_STEP_TARGETS);
664
+ function signerRefusalStep(input) {
665
+ return nextStep({ ...input, safeToContinue: false, reason: "" });
666
+ }
667
+ function nextStepWireFields(step) {
668
+ return {
669
+ ...step.next_tool ? { next_tool: step.next_tool } : {},
670
+ ...step.next_tool_server ? { next_tool_server: step.next_tool_server } : {},
671
+ ...step.next_tool_name ? { next_tool_name: step.next_tool_name } : {},
672
+ ...step.next_tool_server_role ? { next_tool_server_role: step.next_tool_server_role } : {},
673
+ ...step.next_arguments ? { next_arguments: step.next_arguments } : {},
674
+ ...step.next_tool_omitted_reason ? { next_tool_omitted_reason: step.next_tool_omitted_reason } : {}
675
+ };
676
+ }
677
+
678
+ // src/sign-context.ts
630
679
  var HavenSignContextError = class extends HavenSigningError {
631
680
  /**
632
681
  * #3001 / #3010 review: the recovery path, when signing OTHER bytes is
@@ -646,28 +695,63 @@ var HavenSignContextError = class extends HavenSigningError {
646
695
  * plain `HavenError` branch already does for that code.
647
696
  */
648
697
  next_action;
698
+ /**
699
+ * #3103 (epic #3105, decisions 1 and 3): the typed next step beside the
700
+ * action. A refusal Haven made (`SIGN_CONTEXT_REFUSED`, not expired) names
701
+ * the hosted status read with the payment id; a transport failure or a
702
+ * malformed body names no tool — the remedy is re-running the SAME quote
703
+ * tool with `include_signing_payload: true` — and an expired window names
704
+ * none either (which quote tool depends on the flow). Never null: a step
705
+ * with no tool carries `next_tool_omitted_reason`. Additive: the class is
706
+ * not exported from the package index — the published surface is the
707
+ * `ToolFailure` envelope `tools.ts` builds from these fields — and every
708
+ * constructor call site is in this file.
709
+ */
710
+ next_tool;
711
+ next_tool_server;
712
+ next_tool_name;
713
+ next_tool_server_role;
714
+ next_arguments;
715
+ next_tool_omitted_reason;
649
716
  retry_with_new_quote;
650
717
  /** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
651
718
  http_status;
652
719
  /** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
653
720
  backend_error_code;
654
- constructor(message, code, refusal) {
721
+ constructor(message, code, refusal, paymentId) {
655
722
  super(message);
656
723
  this.code = code;
657
724
  this.name = "HavenSignContextError";
725
+ let step;
658
726
  if (code === "SIGN_CONTEXT_REFUSED") {
659
727
  this.http_status = refusal?.httpStatus;
660
728
  this.backend_error_code = refusal?.errorCode;
661
729
  if (refusal?.httpStatus === 410 || refusal?.errorCode === "expired") {
662
730
  this.next_action = AgentPaymentNextAction.PaymentWindowExpired;
663
731
  this.retry_with_new_quote = true;
732
+ step = signerRefusalStep({
733
+ nextAction: AgentPaymentNextAction.PaymentWindowExpired,
734
+ nextTool: null,
735
+ nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow"
736
+ });
664
737
  } else {
665
738
  this.next_action = AgentPaymentNextAction.StopAndTellUser;
739
+ step = signerRefusalStep({
740
+ nextAction: AgentPaymentNextAction.StopAndTellUser,
741
+ nextTool: "haven_get_payment_status",
742
+ nextArguments: { payment_id: paymentId }
743
+ });
666
744
  }
667
745
  } else {
668
746
  this.fallback = "typed_data_b64";
669
747
  this.next_action = AgentPaymentNextAction.StopAndTellUser;
748
+ step = signerRefusalStep({
749
+ nextAction: AgentPaymentNextAction.StopAndTellUser,
750
+ nextTool: null,
751
+ nextToolOmittedReason: "re-run the SAME hosted quote tool with the same idempotency_key and include_signing_payload: true, then pass its typed_data_b64 to this signer"
752
+ });
670
753
  }
754
+ Object.assign(this, nextStepWireFields(step));
671
755
  }
672
756
  };
673
757
  async function loadHavenIdentity(credentialsPath) {
@@ -699,7 +783,9 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
699
783
  const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
700
784
  throw new HavenSignContextError(
701
785
  timedOut ? `Haven did not answer the signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.` : `Could not reach Haven to fetch the signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or pass typed_data_b64 from the quote result instead.`,
702
- timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE"
786
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
787
+ void 0,
788
+ paymentId
703
789
  );
704
790
  }
705
791
  let body;
@@ -709,7 +795,9 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
709
795
  if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
710
796
  throw new HavenSignContextError(
711
797
  `Haven did not finish sending the signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.`,
712
- "SIGN_CONTEXT_TIMEOUT"
798
+ "SIGN_CONTEXT_TIMEOUT",
799
+ void 0,
800
+ paymentId
713
801
  );
714
802
  }
715
803
  body = {};
@@ -719,7 +807,8 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
719
807
  throw new HavenSignContextError(
720
808
  `Haven refused the signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 check the payment_id came from this agent\u2019s own quote." : response.status === 410 ? " Re-run the quote with the same idempotency key, then sign the fresh payment_id." : ""),
721
809
  "SIGN_CONTEXT_REFUSED",
722
- { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 }
810
+ { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 },
811
+ paymentId
723
812
  );
724
813
  }
725
814
  const signData = body.sign_data;
@@ -727,7 +816,9 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
727
816
  if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
728
817
  throw new HavenSignContextError(
729
818
  "The Haven sign-context response is missing sign_data.typed_data or x402_expected \u2014 the backend may predate #1263. Pass typed_data_b64 from the quote result instead.",
730
- "SIGN_CONTEXT_MALFORMED"
819
+ "SIGN_CONTEXT_MALFORMED",
820
+ void 0,
821
+ paymentId
731
822
  );
732
823
  }
733
824
  const paymentRequired = body.payment_required;
@@ -741,54 +832,54 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
741
832
  }
742
833
 
743
834
  // src/tools.ts
744
- var sweepAuthorizationSchema = z.object({
745
- from: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "from must be a 0x address"),
746
- to: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "to must be a 0x address"),
747
- value: z.string().regex(/^[0-9]+$/, "value must be a decimal atomic amount"),
748
- validAfter: z.string().regex(/^[0-9]+$/, "validAfter must be a decimal unix time"),
749
- validBefore: z.string().regex(/^[0-9]+$/, "validBefore must be a decimal unix time"),
750
- nonce: z.string().regex(/^0x[0-9a-fA-F]{64}$/, "nonce must be a 0x-prefixed 32-byte hex string"),
751
- token: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "token must be a 0x address"),
752
- chainId: z.number().int().positive()
835
+ var sweepAuthorizationSchema = z$1.object({
836
+ from: z$1.string().regex(/^0x[0-9a-fA-F]{40}$/, "from must be a 0x address"),
837
+ to: z$1.string().regex(/^0x[0-9a-fA-F]{40}$/, "to must be a 0x address"),
838
+ value: z$1.string().regex(/^[0-9]+$/, "value must be a decimal atomic amount"),
839
+ validAfter: z$1.string().regex(/^[0-9]+$/, "validAfter must be a decimal unix time"),
840
+ validBefore: z$1.string().regex(/^[0-9]+$/, "validBefore must be a decimal unix time"),
841
+ nonce: z$1.string().regex(/^0x[0-9a-fA-F]{64}$/, "nonce must be a 0x-prefixed 32-byte hex string"),
842
+ token: z$1.string().regex(/^0x[0-9a-fA-F]{40}$/, "token must be a 0x address"),
843
+ chainId: z$1.number().int().positive()
753
844
  });
754
- var bindingVersionSchema = z.number().int().positive();
755
- var sweepExpectedAuthSchema = z.object({
845
+ var bindingVersionSchema = z$1.number().int().positive();
846
+ var sweepExpectedAuthSchema = z$1.object({
756
847
  version: bindingVersionSchema,
757
- message: z.string().min(1),
758
- signature: z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
759
- signer: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "signer must be a 0x address")
848
+ message: z$1.string().min(1),
849
+ signature: z$1.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
850
+ signer: z$1.string().regex(/^0x[0-9a-fA-F]{40}$/, "signer must be a 0x address")
760
851
  });
761
852
  var x402ExpectedShape = {
762
- payment_id: z.string().min(1),
763
- payload_hash: z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
764
- resource_url: z.string().url(),
765
- merchant_to: z.string().min(1),
766
- amount: z.string().min(1),
767
- asset: z.string().min(1),
768
- network: z.string().min(1),
853
+ payment_id: z$1.string().min(1),
854
+ payload_hash: z$1.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
855
+ resource_url: z$1.string().url(),
856
+ merchant_to: z$1.string().min(1),
857
+ amount: z$1.string().min(1),
858
+ asset: z$1.string().min(1),
859
+ network: z$1.string().min(1),
769
860
  // Required: expires_at is folded into the Haven-signed binding message, so the
770
861
  // signer must receive the exact same value to reconstruct a matching message.
771
862
  // Omitting it used to fail downstream with a cryptic "authentication message is
772
863
  // invalid" — making it required surfaces a clear INVALID_INPUT at the boundary.
773
- expires_at: z.string().min(1),
864
+ expires_at: z$1.string().min(1),
774
865
  // #1138: present on a delegation-rail (v2) context. It commits to the EIP-712
775
866
  // typed data the account validates; the signer refuses to raw-sign the bare
776
867
  // hash when it is present, and refuses to sign typed data when it is absent.
777
- typed_data_hash: z.string().regex(/^0x[0-9a-fA-F]+$/, "typed_data_hash must be a 0x-prefixed hex string").optional(),
868
+ typed_data_hash: z$1.string().regex(/^0x[0-9a-fA-F]+$/, "typed_data_hash must be a 0x-prefixed hex string").optional(),
778
869
  // #1690: present on a v3 context. The delegate this quote was created FOR —
779
870
  // the signer refuses to sign when it is not its own. Inside the Haven-signed
780
871
  // message, so stripping it here would only break the binding signature.
781
- payer_delegate: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "payer_delegate must be a 0x-prefixed address").optional(),
782
- payer_agent_id: z.string().min(1).optional(),
783
- auth: z.object({
872
+ payer_delegate: z$1.string().regex(/^0x[0-9a-fA-F]{40}$/, "payer_delegate must be a 0x-prefixed address").optional(),
873
+ payer_agent_id: z$1.string().min(1).optional(),
874
+ auth: z$1.object({
784
875
  // Open at the boundary, enforced in the signer — see bindingVersionSchema.
785
876
  version: bindingVersionSchema,
786
- message: z.string().min(1),
787
- signature: z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
788
- signer: z.string().min(1)
877
+ message: z$1.string().min(1),
878
+ signature: z$1.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
879
+ signer: z$1.string().min(1)
789
880
  })
790
881
  };
791
- var x402ExpectedSchema = z.object(x402ExpectedShape);
882
+ var x402ExpectedSchema = z$1.object(x402ExpectedShape);
792
883
  var toolSchemas = {
793
884
  haven_sign_sweep_delegate: {
794
885
  // The authorization fields prepared by Haven's POST /sweep/prepare. Passed
@@ -803,17 +894,17 @@ var toolSchemas = {
803
894
  // payload + expected context from Haven itself (authenticated with the
804
895
  // locally-stored agent credential), so no bulky bytes ever cross the
805
896
  // model. Pass payment_id ALONE for a delegation-rail x402 funding intent.
806
- payment_id: z.string().min(1).optional(),
897
+ payment_id: z$1.string().min(1).optional(),
807
898
  // The unsigned hash from haven_pay / haven_pay_x402_quote (payload_hash).
808
899
  // Optional when payment_id is supplied (the fetch carries it).
809
- payload_hash: z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
900
+ payload_hash: z$1.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
810
901
  // Pass x402.expected from hosted haven_pay_x402_quote when this hash funds
811
902
  // a standard x402 merchant retry. The signer records it locally and returns
812
903
  // an opaque x402_binding for the later header-signing step.
813
904
  x402_expected: x402ExpectedSchema.optional(),
814
905
  // #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
815
906
  // (not z.unknown()) so MCP clients embed it as JSON rather than a string.
816
- typed_data: z.record(z.string(), z.unknown()).optional(),
907
+ typed_data: z$1.record(z$1.string(), z$1.unknown()).optional(),
817
908
  // #1255: the same payload as ONE opaque base64 string, exactly as returned
818
909
  // by the hosted tools. Preferred over typed_data when both are present —
819
910
  // an agent re-emitting multi-KB nested JSON between tool calls is the
@@ -822,35 +913,36 @@ var toolSchemas = {
822
913
  // Bounded: a realistic redemption payload is ~10KB encoded; 256KB is
823
914
  // generous headroom while keeping the offline signer from materializing
824
915
  // arbitrarily large caller input.
825
- typed_data_b64: z.string().min(1).max(262144).optional()
916
+ typed_data_b64: z$1.string().min(1).max(262144).optional()
826
917
  },
827
918
  haven_x402_sign_header: {
828
919
  // The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
829
920
  // (not z.unknown(), which becomes empty JSON Schema `{}`) so MCP clients
830
921
  // embed it as JSON rather than serialising the object to a string.
831
- payment_required: z.record(z.string(), z.unknown()),
922
+ payment_required: z$1.record(z$1.string(), z$1.unknown()),
832
923
  // Opaque binding returned by haven_sign when x402_expected was supplied.
833
- x402_binding: z.string().min(1)
924
+ x402_binding: z$1.string().min(1)
834
925
  },
835
926
  haven_sign_x402: {
836
927
  // #1263: preferred — pass payment_id (plus payment_required) and the
837
928
  // signer fetches the exact signing payload + expected context itself.
838
- payment_id: z.string().min(1).optional(),
929
+ payment_id: z$1.string().min(1).optional(),
839
930
  // One-shot x402 signing: funding hash + merchant header in one local call.
840
931
  // Both optional when payment_id is supplied (the fetch carries them).
841
- payload_hash: z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
932
+ payload_hash: z$1.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
842
933
  x402_expected: x402ExpectedSchema.optional(),
843
934
  // #1355: optional when payment_id is supplied — the signer's context fetch
844
935
  // carries the stored 402 PaymentRequired, so `{ payment_id }` alone is the
845
936
  // preferred call. Still required (via the runtime check in the handler)
846
937
  // for the quote-based fallback path without a payment_id.
847
- payment_required: z.record(z.string(), z.unknown()).optional(),
938
+ payment_required: z$1.record(z$1.string(), z$1.unknown()).optional(),
848
939
  // #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
849
940
  // (not z.unknown()) so MCP clients embed it as JSON rather than a string.
850
- typed_data: z.record(z.string(), z.unknown()).optional(),
941
+ typed_data: z$1.record(z$1.string(), z$1.unknown()).optional(),
851
942
  // #1255: see haven_sign.typed_data_b64 — the copy-through-safe form.
852
- typed_data_b64: z.string().min(1).max(262144).optional()
943
+ typed_data_b64: z$1.string().min(1).max(262144).optional()
853
944
  }
945
+ // #3101: keys survive on the type (see the hosted server's contracts.ts).
854
946
  };
855
947
  var SIGN_DESCRIPTION = [
856
948
  "Sign an unsigned Haven payment hash with the local delegate key. The delegate key never leaves",
@@ -948,7 +1040,7 @@ function toExpectedX402(raw) {
948
1040
  };
949
1041
  }
950
1042
  function parseFetchedExpected(fetched) {
951
- const parsed = z.object(x402ExpectedShape).safeParse(fetched.x402Expected);
1043
+ const parsed = z$1.object(x402ExpectedShape).safeParse(fetched.x402Expected);
952
1044
  if (!parsed.success) {
953
1045
  throw new HavenSigningError(
954
1046
  `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"}`
@@ -1078,7 +1170,7 @@ function createToolHandlers(signer, options = {}) {
1078
1170
  authorization: args.authorization,
1079
1171
  expectedAuth: args.expected_auth,
1080
1172
  // Cross-check `to` against the Safe in the local credential when present.
1081
- expectedSafe: options.audit?.safeAddress
1173
+ expectedSafe: options.audit?.accountAddress
1082
1174
  });
1083
1175
  await auditSigning(
1084
1176
  "haven_sign_sweep_delegate",
@@ -1100,7 +1192,7 @@ function createToolHandlers(signer, options = {}) {
1100
1192
  }
1101
1193
  }
1102
1194
  function parse(name, input) {
1103
- return z.object(toolSchemas[name]).parse(input ?? {});
1195
+ return z$1.object(toolSchemas[name]).parse(input ?? {});
1104
1196
  }
1105
1197
  function coerceX402Expected(input) {
1106
1198
  if (!input || typeof input !== "object") return input;
@@ -1139,7 +1231,7 @@ async function runTool(fn) {
1139
1231
  }
1140
1232
  }
1141
1233
  function normalizeError(err) {
1142
- if (err instanceof z.ZodError) {
1234
+ if (err instanceof z$1.ZodError) {
1143
1235
  return {
1144
1236
  success: false,
1145
1237
  code: "INVALID_INPUT",
@@ -1155,7 +1247,13 @@ function normalizeError(err) {
1155
1247
  supported_versions: [...err.supportedVersions],
1156
1248
  received_version: err.receivedVersion,
1157
1249
  fallback: err.fallback,
1158
- next_action: AgentPaymentNextAction.StopAndTellUser
1250
+ next_action: AgentPaymentNextAction.StopAndTellUser,
1251
+ // #3103: no tool can fix a version skew from inside the call.
1252
+ ...nextStepWireFields(signerRefusalStep({
1253
+ nextAction: AgentPaymentNextAction.StopAndTellUser,
1254
+ nextTool: null,
1255
+ nextToolOmittedReason: "update @haven_ai/signer by re-running the connector, then repeat the same call"
1256
+ }))
1159
1257
  };
1160
1258
  }
1161
1259
  if (err instanceof HavenSignContextError) {
@@ -1167,7 +1265,14 @@ function normalizeError(err) {
1167
1265
  ...err.fallback !== void 0 ? { fallback: err.fallback } : {},
1168
1266
  ...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
1169
1267
  ...err.http_status !== void 0 ? { http_status: err.http_status } : {},
1170
- ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {}
1268
+ ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {},
1269
+ // #3103: the typed step the error decided beside its action.
1270
+ ...err.next_tool ? { next_tool: err.next_tool } : {},
1271
+ ...err.next_tool_server ? { next_tool_server: err.next_tool_server } : {},
1272
+ ...err.next_tool_name ? { next_tool_name: err.next_tool_name } : {},
1273
+ ...err.next_tool_server_role ? { next_tool_server_role: err.next_tool_server_role } : {},
1274
+ ...err.next_arguments ? { next_arguments: err.next_arguments } : {},
1275
+ ...err.next_tool_omitted_reason ? { next_tool_omitted_reason: err.next_tool_omitted_reason } : {}
1171
1276
  };
1172
1277
  }
1173
1278
  if (err instanceof HavenSigningError) {
@@ -1186,7 +1291,13 @@ function normalizeError(err) {
1186
1291
  ...err.code === AgentPaymentFailureCode.PaymentWindowExpired ? {
1187
1292
  next_action: AgentPaymentNextAction.PaymentWindowExpired,
1188
1293
  retry_with_new_quote: true,
1189
- suggested_tool: "haven_pay_mcp_tool"
1294
+ suggested_tool: "haven_pay_mcp_tool",
1295
+ // #3103: same omission as the hosted window-expired helper.
1296
+ ...nextStepWireFields(signerRefusalStep({
1297
+ nextAction: AgentPaymentNextAction.PaymentWindowExpired,
1298
+ nextTool: null,
1299
+ 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)"
1300
+ }))
1190
1301
  } : {}
1191
1302
  };
1192
1303
  }
@@ -1203,7 +1314,7 @@ var SIGNER_CONSENT_SURFACE_VERSION = 2;
1203
1314
  function computeSignerConsentHash(input) {
1204
1315
  const identity = [
1205
1316
  input.delegateAddress.toLowerCase(),
1206
- (input.safeAddress ?? "").toLowerCase(),
1317
+ (input.accountAddress ?? "").toLowerCase(),
1207
1318
  input.agentId ?? "",
1208
1319
  input.chainId ?? "",
1209
1320
  input.network ?? ""
@@ -1222,7 +1333,7 @@ function renderSignerConsentBlock(input, hash) {
1222
1333
  "",
1223
1334
  `Delegate address: ${input.delegateAddress}`
1224
1335
  ];
1225
- lines.push(`Haven wallet: ${input.safeAddress ?? "not provided to this signer"}`);
1336
+ lines.push(`Haven wallet: ${input.accountAddress ?? "not provided to this signer"}`);
1226
1337
  if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
1227
1338
  if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
1228
1339
  if (input.network) lines.push(`Network: ${input.network}`);
@@ -1316,7 +1427,7 @@ async function writeAckFile(path, hash) {
1316
1427
  );
1317
1428
  }
1318
1429
  var SIGNER_NAME = "@haven_ai/signer";
1319
- var SIGNER_VERSION = "0.2.1-alpha.0";
1430
+ var SIGNER_VERSION = "0.4.0-alpha.0";
1320
1431
  async function resolveEdgeSigner(options = {}) {
1321
1432
  const { signer } = await resolveSignerRuntime(options);
1322
1433
  return signer;
@@ -1355,7 +1466,7 @@ function buildSignerMcpServer(signer, options = {}) {
1355
1466
  audit: {
1356
1467
  auditPath: options.auditPath ?? defaultSigningAuditPath(credentialsPath),
1357
1468
  delegateAddress: signer.delegateAddress,
1358
- safeAddress: options.credentials?.accountAddress ?? options.credentials?.safeAddress,
1469
+ accountAddress: options.credentials?.accountAddress,
1359
1470
  chainId: options.credentials?.chainId
1360
1471
  },
1361
1472
  // #1263: the payment_id signing path — the ONLY network call this server
@@ -1405,7 +1516,7 @@ async function runSignerConsentGate(signer, credentials, options) {
1405
1516
  return ensureSignerConsent(
1406
1517
  {
1407
1518
  delegateAddress: signer.delegateAddress,
1408
- safeAddress: credentials?.accountAddress ?? credentials?.safeAddress,
1519
+ accountAddress: credentials?.accountAddress,
1409
1520
  agentId: credentials?.agentId,
1410
1521
  chainId: credentials?.chainId,
1411
1522
  network: credentials?.network,
@@ -1426,6 +1537,6 @@ function toMcpResult(payload) {
1426
1537
  };
1427
1538
  }
1428
1539
 
1429
- export { SIGNER_ACK_ENV, SIGNER_CAPABILITY_KEY, SIGNER_NAME, SIGNER_VERSION, SUPPORTED_SWEEP_BINDING_VERSIONS, SUPPORTED_X402_EXPECTED_VERSIONS, appendSigningAuditEntry, assertSupportedBindingVersion, assertSupportedNodeVersion, assertX402MatchesExpected, buildSignerMcpServer, computeSignerConsentHash, createEdgeSigner, createSigningAuditEntry, createToolHandlers, defaultSigningAuditPath, ensureSignerConsent, hashPayloadForAudit, loadSignerCredentials, readAccountAddressEnv, readAccountAddressField, renderSignerConsentBlock, resolveEdgeSigner, resolveSignerRuntime, runSignerConsentGate, runSignerStdioServer, signerCapabilityAdvertisement, signerCompatibility, signerInstructions, toolDescriptions, toolSchemas, warnIfCredentialFilePermissive };
1540
+ export { SIGNER_ACK_ENV, SIGNER_CAPABILITY_KEY, SIGNER_HOSTED_HANDOFF_SHAPES, SIGNER_NAME, SIGNER_VERSION, SUPPORTED_SWEEP_BINDING_VERSIONS, SUPPORTED_X402_EXPECTED_VERSIONS, appendSigningAuditEntry, assertSupportedBindingVersion, assertSupportedNodeVersion, assertX402MatchesExpected, buildSignerMcpServer, computeSignerConsentHash, createEdgeSigner, createSigningAuditEntry, createToolHandlers, defaultSigningAuditPath, ensureSignerConsent, hashPayloadForAudit, loadSignerCredentials, readAccountAddressEnv, readAccountAddressField, renderSignerConsentBlock, resolveEdgeSigner, resolveSignerRuntime, runSignerConsentGate, runSignerStdioServer, signerCapabilityAdvertisement, signerCompatibility, signerInstructions, toolDescriptions, toolSchemas, warnIfCredentialFilePermissive };
1430
1541
  //# sourceMappingURL=index.js.map
1431
1542
  //# sourceMappingURL=index.js.map