@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.cjs CHANGED
@@ -11,6 +11,7 @@ var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
11
11
  var os = require('os');
12
12
  var path = require('path');
13
13
  var v3 = require('zod/v3');
14
+ var zod = require('zod');
14
15
 
15
16
  // src/core.ts
16
17
  var DELEGATION_MANAGER = "0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3";
@@ -496,7 +497,10 @@ function signerInstructions() {
496
497
  "",
497
498
  "A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
498
499
  "machine-readable, not just prose: it carries code, supported_versions, received_version, and",
499
- "fallback fields alongside the message, so you can branch on it directly."
500
+ "fallback fields alongside the message, so you can branch on it directly. Every signer",
501
+ "refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
502
+ "hosted tool follows (resolve the role against your own server names), else",
503
+ "next_tool_omitted_reason saying why not."
500
504
  ].join("\n");
501
505
  }
502
506
  async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
@@ -508,7 +512,6 @@ async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
508
512
  delegateKey: envKey,
509
513
  agentId: stringField(process.env.HAVEN_AGENT_ID),
510
514
  accountAddress,
511
- safeAddress: accountAddress,
512
515
  chainId: chainIdField(process.env.HAVEN_CHAIN_ID, "HAVEN_CHAIN_ID"),
513
516
  network: stringField(process.env.HAVEN_NETWORK),
514
517
  x402BindingSigner: stringField(process.env.HAVEN_X402_BINDING_SIGNER)
@@ -543,7 +546,6 @@ async function loadFromFile(path) {
543
546
  delegateKey,
544
547
  agentId: stringField(raw.agent_id ?? raw.agentId),
545
548
  accountAddress,
546
- safeAddress: accountAddress,
547
549
  chainId: chainIdField(raw.chain_id ?? raw.chainId, "chain_id"),
548
550
  network: stringField(raw.network),
549
551
  x402BindingSigner: stringField(
@@ -556,7 +558,22 @@ function readAccountAddressField(raw) {
556
558
  return stringField(raw.account_address ?? raw.safe_address ?? raw.safeAddress);
557
559
  }
558
560
  function readAccountAddressEnv(env) {
559
- return stringField(env.HAVEN_ACCOUNT_ADDRESS ?? env.HAVEN_WALLET_ADDRESS ?? env.HAVEN_SAFE_ADDRESS);
561
+ const current = stringField(env.HAVEN_ACCOUNT_ADDRESS);
562
+ for (const name of ["HAVEN_WALLET_ADDRESS", "HAVEN_SAFE_ADDRESS"]) {
563
+ const retired = stringField(env[name]);
564
+ if (!retired) continue;
565
+ if (!current) {
566
+ throw new Error(
567
+ `${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.`
568
+ );
569
+ }
570
+ if (retired.toLowerCase() !== current.toLowerCase()) {
571
+ throw new Error(
572
+ `${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.`
573
+ );
574
+ }
575
+ }
576
+ return current;
560
577
  }
561
578
  function stringField(value) {
562
579
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -613,7 +630,7 @@ function createSigningAuditEntry(tool, payloadHash, context, now = /* @__PURE__
613
630
  payload_hash: payloadHash,
614
631
  delegate_address: context.delegateAddress
615
632
  };
616
- if (context.safeAddress) entry.safe_address = context.safeAddress;
633
+ if (context.accountAddress) entry.safe_address = context.accountAddress;
617
634
  if (typeof context.chainId === "number") entry.chain_id = context.chainId;
618
635
  return entry;
619
636
  }
@@ -629,6 +646,38 @@ function stableStringify(value) {
629
646
  const object = value;
630
647
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
631
648
  }
649
+ var SIGNER_HOSTED_HANDOFF_SHAPES = {
650
+ haven_get_payment_status: { payment_id: zod.z.string().min(1) }
651
+ };
652
+ function target(shape) {
653
+ const schema = zod.z.object(shape).strict();
654
+ return {
655
+ role: "hosted",
656
+ validate: (input) => {
657
+ const r = schema.safeParse(input ?? {});
658
+ return r.success ? null : r.error.errors.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join("; ");
659
+ }
660
+ };
661
+ }
662
+ var SIGNER_NEXT_STEP_TARGETS = {
663
+ haven_get_payment_status: target(SIGNER_HOSTED_HANDOFF_SHAPES.haven_get_payment_status)
664
+ };
665
+ var nextStep = sdk.createNextStepBuilder(SIGNER_NEXT_STEP_TARGETS);
666
+ function signerRefusalStep(input) {
667
+ return nextStep({ ...input, safeToContinue: false, reason: "" });
668
+ }
669
+ function nextStepWireFields(step) {
670
+ return {
671
+ ...step.next_tool ? { next_tool: step.next_tool } : {},
672
+ ...step.next_tool_server ? { next_tool_server: step.next_tool_server } : {},
673
+ ...step.next_tool_name ? { next_tool_name: step.next_tool_name } : {},
674
+ ...step.next_tool_server_role ? { next_tool_server_role: step.next_tool_server_role } : {},
675
+ ...step.next_arguments ? { next_arguments: step.next_arguments } : {},
676
+ ...step.next_tool_omitted_reason ? { next_tool_omitted_reason: step.next_tool_omitted_reason } : {}
677
+ };
678
+ }
679
+
680
+ // src/sign-context.ts
632
681
  var HavenSignContextError = class extends sdk.HavenSigningError {
633
682
  /**
634
683
  * #3001 / #3010 review: the recovery path, when signing OTHER bytes is
@@ -648,28 +697,63 @@ var HavenSignContextError = class extends sdk.HavenSigningError {
648
697
  * plain `HavenError` branch already does for that code.
649
698
  */
650
699
  next_action;
700
+ /**
701
+ * #3103 (epic #3105, decisions 1 and 3): the typed next step beside the
702
+ * action. A refusal Haven made (`SIGN_CONTEXT_REFUSED`, not expired) names
703
+ * the hosted status read with the payment id; a transport failure or a
704
+ * malformed body names no tool — the remedy is re-running the SAME quote
705
+ * tool with `include_signing_payload: true` — and an expired window names
706
+ * none either (which quote tool depends on the flow). Never null: a step
707
+ * with no tool carries `next_tool_omitted_reason`. Additive: the class is
708
+ * not exported from the package index — the published surface is the
709
+ * `ToolFailure` envelope `tools.ts` builds from these fields — and every
710
+ * constructor call site is in this file.
711
+ */
712
+ next_tool;
713
+ next_tool_server;
714
+ next_tool_name;
715
+ next_tool_server_role;
716
+ next_arguments;
717
+ next_tool_omitted_reason;
651
718
  retry_with_new_quote;
652
719
  /** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
653
720
  http_status;
654
721
  /** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
655
722
  backend_error_code;
656
- constructor(message, code, refusal) {
723
+ constructor(message, code, refusal, paymentId) {
657
724
  super(message);
658
725
  this.code = code;
659
726
  this.name = "HavenSignContextError";
727
+ let step;
660
728
  if (code === "SIGN_CONTEXT_REFUSED") {
661
729
  this.http_status = refusal?.httpStatus;
662
730
  this.backend_error_code = refusal?.errorCode;
663
731
  if (refusal?.httpStatus === 410 || refusal?.errorCode === "expired") {
664
732
  this.next_action = sdk.AgentPaymentNextAction.PaymentWindowExpired;
665
733
  this.retry_with_new_quote = true;
734
+ step = signerRefusalStep({
735
+ nextAction: sdk.AgentPaymentNextAction.PaymentWindowExpired,
736
+ nextTool: null,
737
+ nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow"
738
+ });
666
739
  } else {
667
740
  this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
741
+ step = signerRefusalStep({
742
+ nextAction: sdk.AgentPaymentNextAction.StopAndTellUser,
743
+ nextTool: "haven_get_payment_status",
744
+ nextArguments: { payment_id: paymentId }
745
+ });
668
746
  }
669
747
  } else {
670
748
  this.fallback = "typed_data_b64";
671
749
  this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
750
+ step = signerRefusalStep({
751
+ nextAction: sdk.AgentPaymentNextAction.StopAndTellUser,
752
+ nextTool: null,
753
+ nextToolOmittedReason: "re-run the SAME hosted quote tool with the same idempotency_key and include_signing_payload: true, then pass its typed_data_b64 to this signer"
754
+ });
672
755
  }
756
+ Object.assign(this, nextStepWireFields(step));
673
757
  }
674
758
  };
675
759
  async function loadHavenIdentity(credentialsPath) {
@@ -701,7 +785,9 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
701
785
  const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
702
786
  throw new HavenSignContextError(
703
787
  timedOut ? `Haven did not answer the signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.` : `Could not reach Haven to fetch the signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or pass typed_data_b64 from the quote result instead.`,
704
- timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE"
788
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
789
+ void 0,
790
+ paymentId
705
791
  );
706
792
  }
707
793
  let body;
@@ -711,7 +797,9 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
711
797
  if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
712
798
  throw new HavenSignContextError(
713
799
  `Haven did not finish sending the signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.`,
714
- "SIGN_CONTEXT_TIMEOUT"
800
+ "SIGN_CONTEXT_TIMEOUT",
801
+ void 0,
802
+ paymentId
715
803
  );
716
804
  }
717
805
  body = {};
@@ -721,7 +809,8 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
721
809
  throw new HavenSignContextError(
722
810
  `Haven refused the signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 check the payment_id came from this agent\u2019s own quote." : response.status === 410 ? " Re-run the quote with the same idempotency key, then sign the fresh payment_id." : ""),
723
811
  "SIGN_CONTEXT_REFUSED",
724
- { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 }
812
+ { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 },
813
+ paymentId
725
814
  );
726
815
  }
727
816
  const signData = body.sign_data;
@@ -729,7 +818,9 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
729
818
  if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
730
819
  throw new HavenSignContextError(
731
820
  "The Haven sign-context response is missing sign_data.typed_data or x402_expected \u2014 the backend may predate #1263. Pass typed_data_b64 from the quote result instead.",
732
- "SIGN_CONTEXT_MALFORMED"
821
+ "SIGN_CONTEXT_MALFORMED",
822
+ void 0,
823
+ paymentId
733
824
  );
734
825
  }
735
826
  const paymentRequired = body.payment_required;
@@ -853,6 +944,7 @@ var toolSchemas = {
853
944
  // #1255: see haven_sign.typed_data_b64 — the copy-through-safe form.
854
945
  typed_data_b64: v3.z.string().min(1).max(262144).optional()
855
946
  }
947
+ // #3101: keys survive on the type (see the hosted server's contracts.ts).
856
948
  };
857
949
  var SIGN_DESCRIPTION = [
858
950
  "Sign an unsigned Haven payment hash with the local delegate key. The delegate key never leaves",
@@ -1080,7 +1172,7 @@ function createToolHandlers(signer, options = {}) {
1080
1172
  authorization: args.authorization,
1081
1173
  expectedAuth: args.expected_auth,
1082
1174
  // Cross-check `to` against the Safe in the local credential when present.
1083
- expectedSafe: options.audit?.safeAddress
1175
+ expectedSafe: options.audit?.accountAddress
1084
1176
  });
1085
1177
  await auditSigning(
1086
1178
  "haven_sign_sweep_delegate",
@@ -1157,7 +1249,13 @@ function normalizeError(err) {
1157
1249
  supported_versions: [...err.supportedVersions],
1158
1250
  received_version: err.receivedVersion,
1159
1251
  fallback: err.fallback,
1160
- next_action: sdk.AgentPaymentNextAction.StopAndTellUser
1252
+ next_action: sdk.AgentPaymentNextAction.StopAndTellUser,
1253
+ // #3103: no tool can fix a version skew from inside the call.
1254
+ ...nextStepWireFields(signerRefusalStep({
1255
+ nextAction: sdk.AgentPaymentNextAction.StopAndTellUser,
1256
+ nextTool: null,
1257
+ nextToolOmittedReason: "update @haven_ai/signer by re-running the connector, then repeat the same call"
1258
+ }))
1161
1259
  };
1162
1260
  }
1163
1261
  if (err instanceof HavenSignContextError) {
@@ -1169,7 +1267,14 @@ function normalizeError(err) {
1169
1267
  ...err.fallback !== void 0 ? { fallback: err.fallback } : {},
1170
1268
  ...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
1171
1269
  ...err.http_status !== void 0 ? { http_status: err.http_status } : {},
1172
- ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {}
1270
+ ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {},
1271
+ // #3103: the typed step the error decided beside its action.
1272
+ ...err.next_tool ? { next_tool: err.next_tool } : {},
1273
+ ...err.next_tool_server ? { next_tool_server: err.next_tool_server } : {},
1274
+ ...err.next_tool_name ? { next_tool_name: err.next_tool_name } : {},
1275
+ ...err.next_tool_server_role ? { next_tool_server_role: err.next_tool_server_role } : {},
1276
+ ...err.next_arguments ? { next_arguments: err.next_arguments } : {},
1277
+ ...err.next_tool_omitted_reason ? { next_tool_omitted_reason: err.next_tool_omitted_reason } : {}
1173
1278
  };
1174
1279
  }
1175
1280
  if (err instanceof sdk.HavenSigningError) {
@@ -1188,7 +1293,13 @@ function normalizeError(err) {
1188
1293
  ...err.code === sdk.AgentPaymentFailureCode.PaymentWindowExpired ? {
1189
1294
  next_action: sdk.AgentPaymentNextAction.PaymentWindowExpired,
1190
1295
  retry_with_new_quote: true,
1191
- suggested_tool: "haven_pay_mcp_tool"
1296
+ suggested_tool: "haven_pay_mcp_tool",
1297
+ // #3103: same omission as the hosted window-expired helper.
1298
+ ...nextStepWireFields(signerRefusalStep({
1299
+ nextAction: sdk.AgentPaymentNextAction.PaymentWindowExpired,
1300
+ nextTool: null,
1301
+ nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow (suggested_tool names the MCP one)"
1302
+ }))
1192
1303
  } : {}
1193
1304
  };
1194
1305
  }
@@ -1205,7 +1316,7 @@ var SIGNER_CONSENT_SURFACE_VERSION = 2;
1205
1316
  function computeSignerConsentHash(input) {
1206
1317
  const identity = [
1207
1318
  input.delegateAddress.toLowerCase(),
1208
- (input.safeAddress ?? "").toLowerCase(),
1319
+ (input.accountAddress ?? "").toLowerCase(),
1209
1320
  input.agentId ?? "",
1210
1321
  input.chainId ?? "",
1211
1322
  input.network ?? ""
@@ -1224,7 +1335,7 @@ function renderSignerConsentBlock(input, hash) {
1224
1335
  "",
1225
1336
  `Delegate address: ${input.delegateAddress}`
1226
1337
  ];
1227
- lines.push(`Haven wallet: ${input.safeAddress ?? "not provided to this signer"}`);
1338
+ lines.push(`Haven wallet: ${input.accountAddress ?? "not provided to this signer"}`);
1228
1339
  if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
1229
1340
  if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
1230
1341
  if (input.network) lines.push(`Network: ${input.network}`);
@@ -1318,7 +1429,7 @@ async function writeAckFile(path$1, hash) {
1318
1429
  );
1319
1430
  }
1320
1431
  var SIGNER_NAME = "@haven_ai/signer";
1321
- var SIGNER_VERSION = "0.2.1-alpha.0";
1432
+ var SIGNER_VERSION = "0.4.0-alpha.0";
1322
1433
  async function resolveEdgeSigner(options = {}) {
1323
1434
  const { signer } = await resolveSignerRuntime(options);
1324
1435
  return signer;
@@ -1357,7 +1468,7 @@ function buildSignerMcpServer(signer, options = {}) {
1357
1468
  audit: {
1358
1469
  auditPath: options.auditPath ?? defaultSigningAuditPath(credentialsPath),
1359
1470
  delegateAddress: signer.delegateAddress,
1360
- safeAddress: options.credentials?.accountAddress ?? options.credentials?.safeAddress,
1471
+ accountAddress: options.credentials?.accountAddress,
1361
1472
  chainId: options.credentials?.chainId
1362
1473
  },
1363
1474
  // #1263: the payment_id signing path — the ONLY network call this server
@@ -1407,7 +1518,7 @@ async function runSignerConsentGate(signer, credentials, options) {
1407
1518
  return ensureSignerConsent(
1408
1519
  {
1409
1520
  delegateAddress: signer.delegateAddress,
1410
- safeAddress: credentials?.accountAddress ?? credentials?.safeAddress,
1521
+ accountAddress: credentials?.accountAddress,
1411
1522
  agentId: credentials?.agentId,
1412
1523
  chainId: credentials?.chainId,
1413
1524
  network: credentials?.network,
@@ -1430,6 +1541,7 @@ function toMcpResult(payload) {
1430
1541
 
1431
1542
  exports.SIGNER_ACK_ENV = SIGNER_ACK_ENV;
1432
1543
  exports.SIGNER_CAPABILITY_KEY = SIGNER_CAPABILITY_KEY;
1544
+ exports.SIGNER_HOSTED_HANDOFF_SHAPES = SIGNER_HOSTED_HANDOFF_SHAPES;
1433
1545
  exports.SIGNER_NAME = SIGNER_NAME;
1434
1546
  exports.SIGNER_VERSION = SIGNER_VERSION;
1435
1547
  exports.SUPPORTED_SWEEP_BINDING_VERSIONS = SUPPORTED_SWEEP_BINDING_VERSIONS;