@haven_ai/signer 0.1.37-alpha.0 → 0.2.1-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/README.md CHANGED
@@ -186,12 +186,48 @@ what a payload means; they re-derive it.
186
186
  `from` check is unconditional. The **destination** check is not, and this is
187
187
  the one asymmetry in this list: the signer compares the sweep's `to` against
188
188
  the account address **only when the local credential records one**
189
- (`safe_address`). Run with `HAVEN_DELEGATE_KEY` alone — or with a credential
190
- whose `safe_address` is absent — and there is no local value to compare
189
+ (`account_address`, or the pre-#2908 `safe_address` / `safeAddress`, which
190
+ are read permanently; from the environment, `HAVEN_ACCOUNT_ADDRESS`, or the
191
+ older `HAVEN_WALLET_ADDRESS` / `HAVEN_SAFE_ADDRESS` until #2914). Run with
192
+ `HAVEN_DELEGATE_KEY` alone — or with a credential
193
+ whose account address is absent — and there is no local value to compare
191
194
  against, so the destination is authenticated by Haven's binding signature and
192
195
  the token/chain canonicality check, but not independently re-derived. Prefer
193
196
  a credential file that carries the account address.
194
197
 
198
+ ## Sign-context refusal codes
199
+
200
+ `{ payment_id }` calls (`haven_sign` / `haven_sign_x402`) fetch the exact
201
+ signing payload from Haven (`GET /x402/:id/sign-context`, see
202
+ [Custody](#custody)). Every refusal on that fetch is a `HavenSignContextError`
203
+ (#3001) — a `HavenSigningError` subclass, so `instanceof HavenSigningError`
204
+ still holds everywhere it did before, but structured like the version-mismatch
205
+ refusal below rather than prose alone: `code`, `next_action`, and — per
206
+ refusal class — `fallback`, `retry_with_new_quote`, `http_status`,
207
+ `backend_error_code`. `message` is unchanged.
208
+
209
+ | `code` | When | `next_action` | `fallback` | extra |
210
+ |---|---|---|---|---|
211
+ | `SIGN_CONTEXT_TIMEOUT` | The fetch (or its body read) did not finish within `SIGN_CONTEXT_TIMEOUT_MS` | `stop_and_tell_user` | `typed_data_b64` | — |
212
+ | `SIGN_CONTEXT_UNREACHABLE` | The fetch failed before any response (DNS, connection refused, TLS, …) | `stop_and_tell_user` | `typed_data_b64` | — |
213
+ | `SIGN_CONTEXT_MALFORMED` | The response body was missing `sign_data.typed_data` or `x402_expected` (a pre-#1263 backend) | `stop_and_tell_user` | `typed_data_b64` | — |
214
+ | `SIGN_CONTEXT_REFUSED` (410 / `expired`) | The quote's window closed | `payment_window_expired` | — | `retry_with_new_quote: true`, `http_status`, `backend_error_code: 'expired'` |
215
+ | `SIGN_CONTEXT_REFUSED` (other) | Unknown `payment_id` (404), `already_executed` / `not_signable` / `sign_context_unavailable` (409) | `stop_and_tell_user` | — | `http_status`, `backend_error_code` |
216
+
217
+ `fallback: 'typed_data_b64'` appears only where signing OTHER bytes is a
218
+ remedy — a transport failure or a body this signer could not read. It is
219
+ **not** in the default quote result since #1272: obtain it by re-running the
220
+ SAME quote tool with the SAME `idempotency_key` plus
221
+ `include_signing_payload: true`, then pass `typed_data_b64` (plus
222
+ `payload_hash` / `x402_expected`) instead of `payment_id`. A backend REFUSAL
223
+ carries no fallback: an expired, executed or unsignable intent cannot be
224
+ rescued by re-signing its bytes — an expired one is re-quoted (the same
225
+ `payment_window_expired` + `retry_with_new_quote` the signer emits for
226
+ `PAYMENT_WINDOW_EXPIRED`), the rest stop. These codes are signer-local,
227
+ not part of `@haven_ai/sdk`'s `AgentPaymentFailureCode` taxonomy, since they
228
+ describe a local fetch failure, not a payment-domain outcome, and never reach
229
+ the backend's REST/OpenAPI surface — only this package's MCP tool responses.
230
+
195
231
  ## Custody
196
232
 
197
233
  The delegate key is read from `HAVEN_DELEGATE_KEY` or a `--credentials` file's
@@ -204,7 +240,14 @@ read.** Since [#1263](https://github.com/d-hinders/Haven-AI/issues/1263) the
204
240
  authenticated, read-only `GET /x402/:payment_id/sign-context` against Haven, so
205
241
  that agents never have to relay multi-KB EIP-712 payloads through a model's
206
242
  context window. **Only the Bearer API key goes out; the delegate key is never
207
- part of that request or its response.** Nothing else in the package reaches the
243
+ part of that request or its response.** Since #2985 that read is bounded:
244
+ it aborts after `SIGN_CONTEXT_TIMEOUT_MS` (15 s) and reports a
245
+ `HavenSignContextError` naming the timeout and the `typed_data_b64` fallback,
246
+ so a hung backend cannot hang the signer — and the agent — past the funding
247
+ window. Every refusal on this fetch (timeout, unreachable host, a non-ok
248
+ backend response, a malformed body) is structured the same way, not just
249
+ prose — see [Sign-context refusal codes](#sign-context-refusal-codes) below.
250
+ Nothing else in the package reaches the
208
251
  network: `haven_x402_sign_header` and `haven_sign_sweep_delegate` never fetch,
209
252
  the library surface above (`createEdgeSigner` and its six signing methods, over
210
253
  the network-free `src/core.ts`) never fetches, and passing the payload as
package/dist/cli.cjs CHANGED
@@ -534,6 +534,49 @@ function signerInstructions() {
534
534
  "fallback fields alongside the message, so you can branch on it directly."
535
535
  ].join("\n");
536
536
  }
537
+ var HavenSignContextError = class extends sdk.HavenSigningError {
538
+ /**
539
+ * #3001 / #3010 review: the recovery path, when signing OTHER bytes is
540
+ * actually a remedy — a transport failure (timeout, unreachable) or a
541
+ * body this signer could not read. `typed_data_b64` is NOT in the default
542
+ * quote result since #1272: obtain it by re-running the SAME quote tool
543
+ * with the SAME idempotency_key plus `include_signing_payload: true`.
544
+ * Absent on a backend REFUSAL: an expired, executed or unsignable intent
545
+ * cannot be rescued by re-signing its bytes.
546
+ */
547
+ fallback;
548
+ /**
549
+ * #3001: `AgentPaymentNextAction` values the signer already emits — the
550
+ * version-mismatch refusal's `stop_and_tell_user` for the classes where
551
+ * retrying the same call cannot help, and `payment_window_expired` (with
552
+ * `retry_with_new_quote`) for the backend's 410 `expired`, exactly as the
553
+ * plain `HavenError` branch already does for that code.
554
+ */
555
+ next_action;
556
+ retry_with_new_quote;
557
+ /** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
558
+ http_status;
559
+ /** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
560
+ backend_error_code;
561
+ constructor(message, code, refusal) {
562
+ super(message);
563
+ this.code = code;
564
+ this.name = "HavenSignContextError";
565
+ if (code === "SIGN_CONTEXT_REFUSED") {
566
+ this.http_status = refusal?.httpStatus;
567
+ this.backend_error_code = refusal?.errorCode;
568
+ if (refusal?.httpStatus === 410 || refusal?.errorCode === "expired") {
569
+ this.next_action = sdk.AgentPaymentNextAction.PaymentWindowExpired;
570
+ this.retry_with_new_quote = true;
571
+ } else {
572
+ this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
573
+ }
574
+ } else {
575
+ this.fallback = "typed_data_b64";
576
+ this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
577
+ }
578
+ }
579
+ };
537
580
  async function loadHavenIdentity(credentialsPath) {
538
581
  if (!credentialsPath) return null;
539
582
  try {
@@ -548,30 +591,50 @@ async function loadHavenIdentity(credentialsPath) {
548
591
  return null;
549
592
  }
550
593
  }
551
- async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch) {
594
+ var SIGN_CONTEXT_TIMEOUT_MS = 15e3;
595
+ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS) {
552
596
  let response;
553
597
  try {
554
598
  response = await fetchImpl(
555
599
  `${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
556
- { headers: { Authorization: `Bearer ${identity.apiKey}` } }
600
+ {
601
+ headers: { Authorization: `Bearer ${identity.apiKey}` },
602
+ signal: AbortSignal.timeout(timeoutMs)
603
+ }
557
604
  );
558
605
  } catch (err) {
559
- throw new sdk.HavenSigningError(
560
- `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.`
606
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
607
+ throw new HavenSignContextError(
608
+ 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.`,
609
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE"
561
610
  );
562
611
  }
563
- const body = await response.json().catch(() => ({}));
612
+ let body;
613
+ try {
614
+ body = await response.json();
615
+ } catch (err) {
616
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
617
+ throw new HavenSignContextError(
618
+ `Haven did not finish sending the signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the quote result instead.`,
619
+ "SIGN_CONTEXT_TIMEOUT"
620
+ );
621
+ }
622
+ body = {};
623
+ }
564
624
  if (!response.ok) {
565
625
  const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
566
- throw new sdk.HavenSigningError(
567
- `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." : "")
626
+ throw new HavenSignContextError(
627
+ `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." : ""),
628
+ "SIGN_CONTEXT_REFUSED",
629
+ { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 }
568
630
  );
569
631
  }
570
632
  const signData = body.sign_data;
571
633
  const x402Expected = body.x402_expected;
572
634
  if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
573
- throw new sdk.HavenSigningError(
574
- "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."
635
+ throw new HavenSignContextError(
636
+ "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.",
637
+ "SIGN_CONTEXT_MALFORMED"
575
638
  );
576
639
  }
577
640
  const paymentRequired = body.payment_required;
@@ -1002,6 +1065,18 @@ function normalizeError(err) {
1002
1065
  next_action: sdk.AgentPaymentNextAction.StopAndTellUser
1003
1066
  };
1004
1067
  }
1068
+ if (err instanceof HavenSignContextError) {
1069
+ return {
1070
+ success: false,
1071
+ code: err.code,
1072
+ message: err.message,
1073
+ next_action: err.next_action,
1074
+ ...err.fallback !== void 0 ? { fallback: err.fallback } : {},
1075
+ ...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
1076
+ ...err.http_status !== void 0 ? { http_status: err.http_status } : {},
1077
+ ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {}
1078
+ };
1079
+ }
1005
1080
  if (err instanceof sdk.HavenSigningError) {
1006
1081
  return { success: false, code: err.code, message: err.message };
1007
1082
  }
@@ -1151,10 +1226,12 @@ async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
1151
1226
  if (path) return loadFromFile(path);
1152
1227
  const envKey = stringField(process.env.HAVEN_DELEGATE_KEY);
1153
1228
  if (envKey) {
1229
+ const accountAddress = readAccountAddressEnv(process.env);
1154
1230
  return {
1155
1231
  delegateKey: envKey,
1156
1232
  agentId: stringField(process.env.HAVEN_AGENT_ID),
1157
- safeAddress: stringField(process.env.HAVEN_SAFE_ADDRESS),
1233
+ accountAddress,
1234
+ safeAddress: accountAddress,
1158
1235
  chainId: chainIdField(process.env.HAVEN_CHAIN_ID, "HAVEN_CHAIN_ID"),
1159
1236
  network: stringField(process.env.HAVEN_NETWORK),
1160
1237
  x402BindingSigner: stringField(process.env.HAVEN_X402_BINDING_SIGNER)
@@ -1184,10 +1261,12 @@ async function loadFromFile(path) {
1184
1261
  if (!delegateKey) {
1185
1262
  throw new Error("Haven credentials are missing delegate_key \u2014 the edge signer needs it to sign.");
1186
1263
  }
1264
+ const accountAddress = readAccountAddressField(raw);
1187
1265
  return {
1188
1266
  delegateKey,
1189
1267
  agentId: stringField(raw.agent_id ?? raw.agentId),
1190
- safeAddress: stringField(raw.safe_address ?? raw.safeAddress),
1268
+ accountAddress,
1269
+ safeAddress: accountAddress,
1191
1270
  chainId: chainIdField(raw.chain_id ?? raw.chainId, "chain_id"),
1192
1271
  network: stringField(raw.network),
1193
1272
  x402BindingSigner: stringField(
@@ -1196,6 +1275,12 @@ async function loadFromFile(path) {
1196
1275
  sourcePath: path
1197
1276
  };
1198
1277
  }
1278
+ function readAccountAddressField(raw) {
1279
+ return stringField(raw.account_address ?? raw.safe_address ?? raw.safeAddress);
1280
+ }
1281
+ function readAccountAddressEnv(env) {
1282
+ return stringField(env.HAVEN_ACCOUNT_ADDRESS ?? env.HAVEN_WALLET_ADDRESS ?? env.HAVEN_SAFE_ADDRESS);
1283
+ }
1199
1284
  function stringField(value) {
1200
1285
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1201
1286
  }
@@ -1237,7 +1322,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
1237
1322
 
1238
1323
  // src/server.ts
1239
1324
  var SIGNER_NAME = "@haven_ai/signer";
1240
- var SIGNER_VERSION = "0.1.37-alpha.0";
1325
+ var SIGNER_VERSION = "0.2.1-alpha.0";
1241
1326
  async function resolveSignerRuntime(options = {}) {
1242
1327
  assertSupportedNodeVersion(options.nodeVersion);
1243
1328
  if (options.delegateKey) {
@@ -1272,7 +1357,7 @@ function buildSignerMcpServer(signer, options = {}) {
1272
1357
  audit: {
1273
1358
  auditPath: options.auditPath ?? defaultSigningAuditPath(credentialsPath),
1274
1359
  delegateAddress: signer.delegateAddress,
1275
- safeAddress: options.credentials?.safeAddress,
1360
+ safeAddress: options.credentials?.accountAddress ?? options.credentials?.safeAddress,
1276
1361
  chainId: options.credentials?.chainId
1277
1362
  },
1278
1363
  // #1263: the payment_id signing path — the ONLY network call this server
@@ -1322,7 +1407,7 @@ async function runSignerConsentGate(signer, credentials, options) {
1322
1407
  return ensureSignerConsent(
1323
1408
  {
1324
1409
  delegateAddress: signer.delegateAddress,
1325
- safeAddress: credentials?.safeAddress,
1410
+ safeAddress: credentials?.accountAddress ?? credentials?.safeAddress,
1326
1411
  agentId: credentials?.agentId,
1327
1412
  chainId: credentials?.chainId,
1328
1413
  network: credentials?.network,