@haven_ai/signer 0.2.0-alpha.0 → 0.3.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
@@ -508,7 +508,6 @@ async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
508
508
  delegateKey: envKey,
509
509
  agentId: stringField(process.env.HAVEN_AGENT_ID),
510
510
  accountAddress,
511
- safeAddress: accountAddress,
512
511
  chainId: chainIdField(process.env.HAVEN_CHAIN_ID, "HAVEN_CHAIN_ID"),
513
512
  network: stringField(process.env.HAVEN_NETWORK),
514
513
  x402BindingSigner: stringField(process.env.HAVEN_X402_BINDING_SIGNER)
@@ -543,7 +542,6 @@ async function loadFromFile(path) {
543
542
  delegateKey,
544
543
  agentId: stringField(raw.agent_id ?? raw.agentId),
545
544
  accountAddress,
546
- safeAddress: accountAddress,
547
545
  chainId: chainIdField(raw.chain_id ?? raw.chainId, "chain_id"),
548
546
  network: stringField(raw.network),
549
547
  x402BindingSigner: stringField(
@@ -556,7 +554,22 @@ function readAccountAddressField(raw) {
556
554
  return stringField(raw.account_address ?? raw.safe_address ?? raw.safeAddress);
557
555
  }
558
556
  function readAccountAddressEnv(env) {
559
- return stringField(env.HAVEN_ACCOUNT_ADDRESS ?? env.HAVEN_WALLET_ADDRESS ?? env.HAVEN_SAFE_ADDRESS);
557
+ const current = stringField(env.HAVEN_ACCOUNT_ADDRESS);
558
+ for (const name of ["HAVEN_WALLET_ADDRESS", "HAVEN_SAFE_ADDRESS"]) {
559
+ const retired = stringField(env[name]);
560
+ if (!retired) continue;
561
+ if (!current) {
562
+ throw new Error(
563
+ `${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.`
564
+ );
565
+ }
566
+ if (retired.toLowerCase() !== current.toLowerCase()) {
567
+ throw new Error(
568
+ `${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.`
569
+ );
570
+ }
571
+ }
572
+ return current;
560
573
  }
561
574
  function stringField(value) {
562
575
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -613,7 +626,7 @@ function createSigningAuditEntry(tool, payloadHash, context, now = /* @__PURE__
613
626
  payload_hash: payloadHash,
614
627
  delegate_address: context.delegateAddress
615
628
  };
616
- if (context.safeAddress) entry.safe_address = context.safeAddress;
629
+ if (context.accountAddress) entry.safe_address = context.accountAddress;
617
630
  if (typeof context.chainId === "number") entry.chain_id = context.chainId;
618
631
  return entry;
619
632
  }
@@ -629,6 +642,49 @@ function stableStringify(value) {
629
642
  const object = value;
630
643
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
631
644
  }
645
+ var HavenSignContextError = class extends sdk.HavenSigningError {
646
+ /**
647
+ * #3001 / #3010 review: the recovery path, when signing OTHER bytes is
648
+ * actually a remedy — a transport failure (timeout, unreachable) or a
649
+ * body this signer could not read. `typed_data_b64` is NOT in the default
650
+ * quote result since #1272: obtain it by re-running the SAME quote tool
651
+ * with the SAME idempotency_key plus `include_signing_payload: true`.
652
+ * Absent on a backend REFUSAL: an expired, executed or unsignable intent
653
+ * cannot be rescued by re-signing its bytes.
654
+ */
655
+ fallback;
656
+ /**
657
+ * #3001: `AgentPaymentNextAction` values the signer already emits — the
658
+ * version-mismatch refusal's `stop_and_tell_user` for the classes where
659
+ * retrying the same call cannot help, and `payment_window_expired` (with
660
+ * `retry_with_new_quote`) for the backend's 410 `expired`, exactly as the
661
+ * plain `HavenError` branch already does for that code.
662
+ */
663
+ next_action;
664
+ retry_with_new_quote;
665
+ /** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
666
+ http_status;
667
+ /** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
668
+ backend_error_code;
669
+ constructor(message, code, refusal) {
670
+ super(message);
671
+ this.code = code;
672
+ this.name = "HavenSignContextError";
673
+ if (code === "SIGN_CONTEXT_REFUSED") {
674
+ this.http_status = refusal?.httpStatus;
675
+ this.backend_error_code = refusal?.errorCode;
676
+ if (refusal?.httpStatus === 410 || refusal?.errorCode === "expired") {
677
+ this.next_action = sdk.AgentPaymentNextAction.PaymentWindowExpired;
678
+ this.retry_with_new_quote = true;
679
+ } else {
680
+ this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
681
+ }
682
+ } else {
683
+ this.fallback = "typed_data_b64";
684
+ this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
685
+ }
686
+ }
687
+ };
632
688
  async function loadHavenIdentity(credentialsPath) {
633
689
  if (!credentialsPath) return null;
634
690
  try {
@@ -643,30 +699,50 @@ async function loadHavenIdentity(credentialsPath) {
643
699
  return null;
644
700
  }
645
701
  }
646
- async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch) {
702
+ var SIGN_CONTEXT_TIMEOUT_MS = 15e3;
703
+ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS) {
647
704
  let response;
648
705
  try {
649
706
  response = await fetchImpl(
650
707
  `${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
651
- { headers: { Authorization: `Bearer ${identity.apiKey}` } }
708
+ {
709
+ headers: { Authorization: `Bearer ${identity.apiKey}` },
710
+ signal: AbortSignal.timeout(timeoutMs)
711
+ }
652
712
  );
653
713
  } catch (err) {
654
- throw new sdk.HavenSigningError(
655
- `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.`
714
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
715
+ throw new HavenSignContextError(
716
+ 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.`,
717
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE"
656
718
  );
657
719
  }
658
- const body = await response.json().catch(() => ({}));
720
+ let body;
721
+ try {
722
+ body = await response.json();
723
+ } catch (err) {
724
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
725
+ throw new HavenSignContextError(
726
+ `Haven did not finish sending the signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.`,
727
+ "SIGN_CONTEXT_TIMEOUT"
728
+ );
729
+ }
730
+ body = {};
731
+ }
659
732
  if (!response.ok) {
660
733
  const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
661
- throw new sdk.HavenSigningError(
662
- `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." : "")
734
+ throw new HavenSignContextError(
735
+ `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." : ""),
736
+ "SIGN_CONTEXT_REFUSED",
737
+ { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 }
663
738
  );
664
739
  }
665
740
  const signData = body.sign_data;
666
741
  const x402Expected = body.x402_expected;
667
742
  if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
668
- throw new sdk.HavenSigningError(
669
- "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."
743
+ throw new HavenSignContextError(
744
+ "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.",
745
+ "SIGN_CONTEXT_MALFORMED"
670
746
  );
671
747
  }
672
748
  const paymentRequired = body.payment_required;
@@ -1017,7 +1093,7 @@ function createToolHandlers(signer, options = {}) {
1017
1093
  authorization: args.authorization,
1018
1094
  expectedAuth: args.expected_auth,
1019
1095
  // Cross-check `to` against the Safe in the local credential when present.
1020
- expectedSafe: options.audit?.safeAddress
1096
+ expectedSafe: options.audit?.accountAddress
1021
1097
  });
1022
1098
  await auditSigning(
1023
1099
  "haven_sign_sweep_delegate",
@@ -1097,6 +1173,18 @@ function normalizeError(err) {
1097
1173
  next_action: sdk.AgentPaymentNextAction.StopAndTellUser
1098
1174
  };
1099
1175
  }
1176
+ if (err instanceof HavenSignContextError) {
1177
+ return {
1178
+ success: false,
1179
+ code: err.code,
1180
+ message: err.message,
1181
+ next_action: err.next_action,
1182
+ ...err.fallback !== void 0 ? { fallback: err.fallback } : {},
1183
+ ...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
1184
+ ...err.http_status !== void 0 ? { http_status: err.http_status } : {},
1185
+ ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {}
1186
+ };
1187
+ }
1100
1188
  if (err instanceof sdk.HavenSigningError) {
1101
1189
  return { success: false, code: err.code, message: err.message };
1102
1190
  }
@@ -1130,7 +1218,7 @@ var SIGNER_CONSENT_SURFACE_VERSION = 2;
1130
1218
  function computeSignerConsentHash(input) {
1131
1219
  const identity = [
1132
1220
  input.delegateAddress.toLowerCase(),
1133
- (input.safeAddress ?? "").toLowerCase(),
1221
+ (input.accountAddress ?? "").toLowerCase(),
1134
1222
  input.agentId ?? "",
1135
1223
  input.chainId ?? "",
1136
1224
  input.network ?? ""
@@ -1149,7 +1237,7 @@ function renderSignerConsentBlock(input, hash) {
1149
1237
  "",
1150
1238
  `Delegate address: ${input.delegateAddress}`
1151
1239
  ];
1152
- lines.push(`Haven wallet: ${input.safeAddress ?? "not provided to this signer"}`);
1240
+ lines.push(`Haven wallet: ${input.accountAddress ?? "not provided to this signer"}`);
1153
1241
  if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
1154
1242
  if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
1155
1243
  if (input.network) lines.push(`Network: ${input.network}`);
@@ -1243,7 +1331,7 @@ async function writeAckFile(path$1, hash) {
1243
1331
  );
1244
1332
  }
1245
1333
  var SIGNER_NAME = "@haven_ai/signer";
1246
- var SIGNER_VERSION = "0.2.0-alpha.0";
1334
+ var SIGNER_VERSION = "0.3.0-alpha.0";
1247
1335
  async function resolveEdgeSigner(options = {}) {
1248
1336
  const { signer } = await resolveSignerRuntime(options);
1249
1337
  return signer;
@@ -1282,7 +1370,7 @@ function buildSignerMcpServer(signer, options = {}) {
1282
1370
  audit: {
1283
1371
  auditPath: options.auditPath ?? defaultSigningAuditPath(credentialsPath),
1284
1372
  delegateAddress: signer.delegateAddress,
1285
- safeAddress: options.credentials?.accountAddress ?? options.credentials?.safeAddress,
1373
+ accountAddress: options.credentials?.accountAddress,
1286
1374
  chainId: options.credentials?.chainId
1287
1375
  },
1288
1376
  // #1263: the payment_id signing path — the ONLY network call this server
@@ -1332,7 +1420,7 @@ async function runSignerConsentGate(signer, credentials, options) {
1332
1420
  return ensureSignerConsent(
1333
1421
  {
1334
1422
  delegateAddress: signer.delegateAddress,
1335
- safeAddress: credentials?.accountAddress ?? credentials?.safeAddress,
1423
+ accountAddress: credentials?.accountAddress,
1336
1424
  agentId: credentials?.agentId,
1337
1425
  chainId: credentials?.chainId,
1338
1426
  network: credentials?.network,