@haven_ai/signer 0.0.0-dev.202609250737.3bd5a51 → 0.0.0-dev.202609251409.98ed67a

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
@@ -335,6 +335,7 @@ carries no x402 context to fund a merchant retry with, so it surfaces the
335
335
  | `SIGN_CONTEXT_MALFORMED` | The response body was missing `sign_data.typed_data` / `x402_expected` (a pre-#1263 backend), or — on the direct-payment fetch — an unsupported `direct_sign_context_version` or a `signature_scheme` other than `eip712_userop` | `stop_and_tell_user` | `typed_data_b64` | — |
336
336
  | `SIGN_CONTEXT_REFUSED` (x402: 410 or `expired`; direct: `expired` only) | x402: the quote's window closed. Direct: the payment's window closed — call `haven_send` / `haven_pay` again with the same `idempotency_key` | `payment_window_expired` | — | x402 only: `retry_with_new_quote: true`; both: `http_status`, and `backend_error_code: 'expired'` when the backend sent one |
337
337
  | `SIGN_CONTEXT_REFUSED` (404, direct fetch) | The `payment_id` is not this agent's, or the backend predates #3271 and has no direct route | `stop_and_tell_user` | `typed_data_b64` | `http_status`, `backend_error_code` |
338
+ | `SIGN_CONTEXT_REFUSED` (426 `client_outdated`, #3303) | This signer is below the minimum version the Haven deployment has set (only when one is set). Nothing is signed; the prepared payment is left unsigned. Update with `client_update.upgrade_command`, restart the agent runtime, then retry the same call | `stop_and_tell_user` | — | `http_status: 426`, `backend_error_code: 'client_outdated'`, `client_update` (the update command as data); carries `next_tool_omitted_reason`, not the status read |
338
339
  | `SIGN_CONTEXT_REFUSED` (other) | Unknown `payment_id` (404, x402 fetch), `already_executed` / `not_signable` (409), a bare 410 retired-rail tombstone (direct fetch) — or `sign_context_unavailable` (409): always on `haven_sign_x402`; on `haven_sign` only when neither route can serve the row | `stop_and_tell_user` | — | `http_status`, `backend_error_code` |
339
340
  | `USEROP_BINDING_MISMATCH` | A direct payment's `PackedUserOperation` typed data (from the direct fetch, or a tool argument) does not recompute to its own `payload_hash`, or a fetched direct context is not a `PackedUserOperation` — see [Two ways to use it](#two-ways-to-use-it) above | `stop_and_tell_user` | — | no `http_status` — this is a local recomputation, not a backend refusal |
340
341
  | `TYPED_DATA_NOT_ALLOWED` | Typed data without an x402 context that is not a bound direct-payment `PackedUserOperation` (#3272): wrong chain, not this signer's own account, a call other than `execute` → DelegationManager → `redeemDelegations`, or a redemption whose delegations are empty, not made to this account, or in a non-default mode — see the allowlist paragraph above. Reachable by `payment_id` when Haven serves such a payload | `stop_and_tell_user` | — | no `http_status` — a local shape check, not a backend refusal |
package/dist/cli.cjs CHANGED
@@ -211,6 +211,12 @@ var HavenSignContextError = class extends edge.HavenSigningError {
211
211
  http_status;
212
212
  /** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
213
213
  backend_error_code;
214
+ /**
215
+ * #3303: on a 426 `client_outdated` refusal, the backend's update hint —
216
+ * this signer is below the minimum the deployment set, and
217
+ * `client_update.upgrade_command` is what updates it.
218
+ */
219
+ client_update;
214
220
  constructor(message, code, refusal, paymentId, flow = "x402") {
215
221
  super(message);
216
222
  this.code = code;
@@ -219,7 +225,15 @@ var HavenSignContextError = class extends edge.HavenSigningError {
219
225
  if (code === "SIGN_CONTEXT_REFUSED") {
220
226
  this.http_status = refusal?.httpStatus;
221
227
  this.backend_error_code = refusal?.errorCode;
222
- if (flow === "direct" && refusal?.httpStatus === 404) {
228
+ if (refusal?.errorCode === "client_outdated") {
229
+ this.client_update = refusal.clientUpdate;
230
+ this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
231
+ step = signerRefusalStep({
232
+ nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
233
+ nextTool: null,
234
+ nextToolOmittedReason: `this signer is below the minimum version Haven accepts \u2014 tell the user to run ${refusal.clientUpdate?.upgrade_command ?? "the connector again"}, restart the agent runtime, then retry the same haven_sign / haven_sign_x402 call`
235
+ });
236
+ } else if (flow === "direct" && refusal?.httpStatus === 404) {
223
237
  this.fallback = "typed_data_b64";
224
238
  this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
225
239
  step = signerRefusalStep({
@@ -277,13 +291,26 @@ async function loadHavenIdentity(credentialsPath) {
277
291
  }
278
292
  }
279
293
  var SIGN_CONTEXT_TIMEOUT_MS = 15e3;
280
- async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS) {
294
+ function signContextHeaders(identity, clientIdentity) {
295
+ return {
296
+ Authorization: `Bearer ${identity.apiKey}`,
297
+ ...clientIdentity ? { [edge.HAVEN_CLIENT_HEADER]: clientIdentity } : {}
298
+ };
299
+ }
300
+ function refusalOf(status, body) {
301
+ return {
302
+ httpStatus: status,
303
+ errorCode: typeof body.error_code === "string" ? body.error_code : void 0,
304
+ clientUpdate: edge.readClientUpdate(body.client_update)
305
+ };
306
+ }
307
+ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
281
308
  let response;
282
309
  try {
283
310
  response = await fetchImpl(
284
311
  `${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
285
312
  {
286
- headers: { Authorization: `Bearer ${identity.apiKey}` },
313
+ headers: signContextHeaders(identity, clientIdentity),
287
314
  signal: AbortSignal.timeout(timeoutMs)
288
315
  }
289
316
  );
@@ -315,7 +342,7 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
315
342
  throw new HavenSignContextError(
316
343
  `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." : ""),
317
344
  "SIGN_CONTEXT_REFUSED",
318
- { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 },
345
+ refusalOf(response.status, body),
319
346
  paymentId
320
347
  );
321
348
  }
@@ -335,17 +362,18 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
335
362
  payloadHash: signData.hash,
336
363
  typedData: signData.typed_data,
337
364
  x402Expected,
338
- paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null
365
+ paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null,
366
+ clientUpdate: edge.readClientUpdate(body.client_update)
339
367
  };
340
368
  }
341
369
  var SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS = [edge.DIRECT_SIGN_CONTEXT_VERSION];
342
- async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS) {
370
+ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
343
371
  let response;
344
372
  try {
345
373
  response = await fetchImpl(
346
374
  `${identity.apiUrl}/payments/${encodeURIComponent(paymentId)}/sign-context`,
347
375
  {
348
- headers: { Authorization: `Bearer ${identity.apiKey}` },
376
+ headers: signContextHeaders(identity, clientIdentity),
349
377
  signal: AbortSignal.timeout(timeoutMs)
350
378
  }
351
379
  );
@@ -379,7 +407,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
379
407
  throw new HavenSignContextError(
380
408
  `Haven refused the direct-payment signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 either the payment_id is not from this agent\u2019s own POST /payments call, or this Haven backend predates #3271. Pass payload_hash and typed_data_b64 from the haven_send / haven_pay result, unchanged, instead." : body.error_code === "expired" ? " The payment window has expired; call haven_send / haven_pay again with the same idempotency_key." : ""),
381
409
  "SIGN_CONTEXT_REFUSED",
382
- { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 },
410
+ refusalOf(response.status, body),
383
411
  paymentId,
384
412
  "direct"
385
413
  );
@@ -400,7 +428,8 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
400
428
  payloadHash: signData.hash,
401
429
  typedData: signData.typed_data,
402
430
  status: typeof body.status === "string" ? body.status : "pending_signature",
403
- expiresAt: typeof body.expires_at === "string" ? body.expires_at : void 0
431
+ expiresAt: typeof body.expires_at === "string" ? body.expires_at : void 0,
432
+ clientUpdate: edge.readClientUpdate(body.client_update)
404
433
  };
405
434
  }
406
435
 
@@ -671,6 +700,9 @@ function parseFetchedExpected(fetched) {
671
700
  }
672
701
  return parsed.data;
673
702
  }
703
+ function clientUpdateField(resolved) {
704
+ return resolved?.ctx.clientUpdate ? { client_update: resolved.ctx.clientUpdate } : {};
705
+ }
674
706
  function resolveTypedData(args) {
675
707
  if (!args.typed_data_b64) return args.typed_data;
676
708
  try {
@@ -704,14 +736,26 @@ function createToolHandlers(signer, options = {}) {
704
736
  );
705
737
  }
706
738
  try {
707
- const ctx = await fetchX402SignContext(identity, args.payment_id, options.signContext?.fetchImpl);
739
+ const ctx = await fetchX402SignContext(
740
+ identity,
741
+ args.payment_id,
742
+ options.signContext?.fetchImpl,
743
+ void 0,
744
+ options.signContext?.clientIdentity
745
+ );
708
746
  checkPayloadHashMatch(args.payload_hash, ctx.payloadHash, args.payment_id);
709
747
  return { kind: "x402", ctx };
710
748
  } catch (err) {
711
749
  if (opts.allowDirectFallback && err instanceof HavenSignContextError && err.http_status === 409 && err.backend_error_code === "sign_context_unavailable") {
712
750
  let ctx;
713
751
  try {
714
- ctx = await fetchDirectSignContext(identity, args.payment_id, options.signContext?.fetchImpl);
752
+ ctx = await fetchDirectSignContext(
753
+ identity,
754
+ args.payment_id,
755
+ options.signContext?.fetchImpl,
756
+ void 0,
757
+ options.signContext?.clientIdentity
758
+ );
715
759
  } catch (directErr) {
716
760
  if (directErr instanceof HavenSignContextError && directErr.http_status === 409 && directErr.backend_error_code === "sign_context_unavailable") {
717
761
  throw err;
@@ -761,12 +805,12 @@ function createToolHandlers(signer, options = {}) {
761
805
  assertBoundDirectPaymentUserOp(typedData, signer.delegateAddress);
762
806
  const signature = await signer.signDelegationTypedData(typedData);
763
807
  await auditSigning("haven_sign", viem.hashTypedData(typedData));
764
- return { signature };
808
+ return { signature, ...clientUpdateField(resolved) };
765
809
  }
766
810
  throw new HavenBareHashRefusedError();
767
811
  }
768
812
  await auditSigning("haven_sign", viem.hashTypedData(typedData));
769
- return { signature: result.signature, x402_binding: result.x402Binding };
813
+ return { signature: result.signature, x402_binding: result.x402Binding, ...clientUpdateField(resolved) };
770
814
  }),
771
815
  haven_x402_sign_header: async (input) => runTool(async () => {
772
816
  const args = parse("haven_x402_sign_header", coercePaymentRequired(input));
@@ -812,7 +856,8 @@ function createToolHandlers(signer, options = {}) {
812
856
  signature: funding.signature,
813
857
  x402_binding: funding.x402Binding,
814
858
  payment_header: header.paymentHeader,
815
- accepted: header.accepted
859
+ accepted: header.accepted,
860
+ ...clientUpdateField(resolved)
816
861
  };
817
862
  }),
818
863
  haven_sign_sweep_delegate: async (input) => runTool(async () => {
@@ -951,6 +996,7 @@ function normalizeError(err) {
951
996
  ...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
952
997
  ...err.http_status !== void 0 ? { http_status: err.http_status } : {},
953
998
  ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {},
999
+ ...err.client_update ? { client_update: err.client_update } : {},
954
1000
  // #3103: the typed step the error decided beside its action.
955
1001
  ...err.next_tool ? { next_tool: err.next_tool } : {},
956
1002
  ...err.next_tool_server ? { next_tool_server: err.next_tool_server } : {},
@@ -1663,7 +1709,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
1663
1709
 
1664
1710
  // src/server.ts
1665
1711
  var SIGNER_NAME = "@haven_ai/signer";
1666
- var SIGNER_VERSION = "0.0.0-dev.202609250737.3bd5a51";
1712
+ var SIGNER_VERSION = "0.0.0-dev.202609251409.98ed67a";
1667
1713
  async function resolveSignerRuntime(options = {}) {
1668
1714
  assertSupportedNodeVersion(options.nodeVersion);
1669
1715
  if (options.delegateKey) {
@@ -1708,7 +1754,10 @@ function buildSignerMcpServer(signer, options = {}) {
1708
1754
  // The signer CORE stays network-free; fetched bytes still pass the same
1709
1755
  // binding verification + digest re-derivation as tool-argument bytes.
1710
1756
  signContext: {
1711
- loadIdentity: () => loadHavenIdentity(credentialsPath)
1757
+ loadIdentity: () => loadHavenIdentity(credentialsPath),
1758
+ // #3303: the backend refuses a signer below a minimum it has set, at
1759
+ // sign-context — the one place this signer meets it.
1760
+ clientIdentity: edge.havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
1712
1761
  }
1713
1762
  });
1714
1763
  const registerTool = server.tool.bind(server);