@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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { randomUUID, createHash } from 'crypto';
3
3
  import { readFile, mkdir, writeFile, stat, lstat, rename, appendFile, chmod } from 'fs/promises';
4
4
  import { resolve, dirname, join } from 'path';
5
- import { createNextStepBuilder, isSupportedNodeVersion, unsupportedNodeVersionMessage, addressFromKey, HavenSigningError, buildSweepTypedData, selectStandardPaymentOption, HavenApiError, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, isSettlementChildTypedData, chainIdForNetwork, assertOwnSettlementChild, isPackedUserOperationTypedData, assertUserOpTypedDataBinding, assertBoundDirectPaymentUserOp as assertBoundDirectPaymentUserOp$1, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, connectorRerunCommand, buildSweepAuthorizationMessage, verifySignature, x402AuthorizationAmount, SignerRefusalCode, SIGNER_UPDATE_FALLBACK, HavenUnsupportedSignerVersionError, buildX402ExpectedMessage, DIRECT_SIGN_CONTEXT_VERSION, HavenError, AgentPaymentFailureCode, AgentPaymentNextAction, HavenUserOpBindingError } from '@haven_ai/sdk/edge';
5
+ import { createNextStepBuilder, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, addressFromKey, HavenSigningError, buildSweepTypedData, selectStandardPaymentOption, HavenApiError, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, isSettlementChildTypedData, chainIdForNetwork, assertOwnSettlementChild, isPackedUserOperationTypedData, assertUserOpTypedDataBinding, assertBoundDirectPaymentUserOp as assertBoundDirectPaymentUserOp$1, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, connectorRerunCommand, buildSweepAuthorizationMessage, verifySignature, x402AuthorizationAmount, SignerRefusalCode, SIGNER_UPDATE_FALLBACK, HavenUnsupportedSignerVersionError, buildX402ExpectedMessage, DIRECT_SIGN_CONTEXT_VERSION, HavenError, AgentPaymentFailureCode, AgentPaymentNextAction, HavenUserOpBindingError, readClientUpdate, HAVEN_CLIENT_HEADER } from '@haven_ai/sdk/edge';
6
6
  import { recoverTypedDataAddress, hashTypedData, hashMessage } from 'viem';
7
7
  import { z as z$1 } from 'zod/v3';
8
8
  import { z } from 'zod';
@@ -209,6 +209,12 @@ var HavenSignContextError = class extends HavenSigningError {
209
209
  http_status;
210
210
  /** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
211
211
  backend_error_code;
212
+ /**
213
+ * #3303: on a 426 `client_outdated` refusal, the backend's update hint —
214
+ * this signer is below the minimum the deployment set, and
215
+ * `client_update.upgrade_command` is what updates it.
216
+ */
217
+ client_update;
212
218
  constructor(message, code, refusal, paymentId, flow = "x402") {
213
219
  super(message);
214
220
  this.code = code;
@@ -217,7 +223,15 @@ var HavenSignContextError = class extends HavenSigningError {
217
223
  if (code === "SIGN_CONTEXT_REFUSED") {
218
224
  this.http_status = refusal?.httpStatus;
219
225
  this.backend_error_code = refusal?.errorCode;
220
- if (flow === "direct" && refusal?.httpStatus === 404) {
226
+ if (refusal?.errorCode === "client_outdated") {
227
+ this.client_update = refusal.clientUpdate;
228
+ this.next_action = AgentPaymentNextAction.StopAndTellUser;
229
+ step = signerRefusalStep({
230
+ nextAction: AgentPaymentNextAction.StopAndTellUser,
231
+ nextTool: null,
232
+ 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`
233
+ });
234
+ } else if (flow === "direct" && refusal?.httpStatus === 404) {
221
235
  this.fallback = "typed_data_b64";
222
236
  this.next_action = AgentPaymentNextAction.StopAndTellUser;
223
237
  step = signerRefusalStep({
@@ -275,13 +289,26 @@ async function loadHavenIdentity(credentialsPath) {
275
289
  }
276
290
  }
277
291
  var SIGN_CONTEXT_TIMEOUT_MS = 15e3;
278
- async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS) {
292
+ function signContextHeaders(identity, clientIdentity) {
293
+ return {
294
+ Authorization: `Bearer ${identity.apiKey}`,
295
+ ...clientIdentity ? { [HAVEN_CLIENT_HEADER]: clientIdentity } : {}
296
+ };
297
+ }
298
+ function refusalOf(status, body) {
299
+ return {
300
+ httpStatus: status,
301
+ errorCode: typeof body.error_code === "string" ? body.error_code : void 0,
302
+ clientUpdate: readClientUpdate(body.client_update)
303
+ };
304
+ }
305
+ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
279
306
  let response;
280
307
  try {
281
308
  response = await fetchImpl(
282
309
  `${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
283
310
  {
284
- headers: { Authorization: `Bearer ${identity.apiKey}` },
311
+ headers: signContextHeaders(identity, clientIdentity),
285
312
  signal: AbortSignal.timeout(timeoutMs)
286
313
  }
287
314
  );
@@ -313,7 +340,7 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
313
340
  throw new HavenSignContextError(
314
341
  `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." : ""),
315
342
  "SIGN_CONTEXT_REFUSED",
316
- { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 },
343
+ refusalOf(response.status, body),
317
344
  paymentId
318
345
  );
319
346
  }
@@ -333,17 +360,18 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
333
360
  payloadHash: signData.hash,
334
361
  typedData: signData.typed_data,
335
362
  x402Expected,
336
- paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null
363
+ paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null,
364
+ clientUpdate: readClientUpdate(body.client_update)
337
365
  };
338
366
  }
339
367
  var SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS = [DIRECT_SIGN_CONTEXT_VERSION];
340
- async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS) {
368
+ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
341
369
  let response;
342
370
  try {
343
371
  response = await fetchImpl(
344
372
  `${identity.apiUrl}/payments/${encodeURIComponent(paymentId)}/sign-context`,
345
373
  {
346
- headers: { Authorization: `Bearer ${identity.apiKey}` },
374
+ headers: signContextHeaders(identity, clientIdentity),
347
375
  signal: AbortSignal.timeout(timeoutMs)
348
376
  }
349
377
  );
@@ -377,7 +405,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
377
405
  throw new HavenSignContextError(
378
406
  `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." : ""),
379
407
  "SIGN_CONTEXT_REFUSED",
380
- { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 },
408
+ refusalOf(response.status, body),
381
409
  paymentId,
382
410
  "direct"
383
411
  );
@@ -398,7 +426,8 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
398
426
  payloadHash: signData.hash,
399
427
  typedData: signData.typed_data,
400
428
  status: typeof body.status === "string" ? body.status : "pending_signature",
401
- expiresAt: typeof body.expires_at === "string" ? body.expires_at : void 0
429
+ expiresAt: typeof body.expires_at === "string" ? body.expires_at : void 0,
430
+ clientUpdate: readClientUpdate(body.client_update)
402
431
  };
403
432
  }
404
433
 
@@ -669,6 +698,9 @@ function parseFetchedExpected(fetched) {
669
698
  }
670
699
  return parsed.data;
671
700
  }
701
+ function clientUpdateField(resolved) {
702
+ return resolved?.ctx.clientUpdate ? { client_update: resolved.ctx.clientUpdate } : {};
703
+ }
672
704
  function resolveTypedData(args) {
673
705
  if (!args.typed_data_b64) return args.typed_data;
674
706
  try {
@@ -702,14 +734,26 @@ function createToolHandlers(signer, options = {}) {
702
734
  );
703
735
  }
704
736
  try {
705
- const ctx = await fetchX402SignContext(identity, args.payment_id, options.signContext?.fetchImpl);
737
+ const ctx = await fetchX402SignContext(
738
+ identity,
739
+ args.payment_id,
740
+ options.signContext?.fetchImpl,
741
+ void 0,
742
+ options.signContext?.clientIdentity
743
+ );
706
744
  checkPayloadHashMatch(args.payload_hash, ctx.payloadHash, args.payment_id);
707
745
  return { kind: "x402", ctx };
708
746
  } catch (err) {
709
747
  if (opts.allowDirectFallback && err instanceof HavenSignContextError && err.http_status === 409 && err.backend_error_code === "sign_context_unavailable") {
710
748
  let ctx;
711
749
  try {
712
- ctx = await fetchDirectSignContext(identity, args.payment_id, options.signContext?.fetchImpl);
750
+ ctx = await fetchDirectSignContext(
751
+ identity,
752
+ args.payment_id,
753
+ options.signContext?.fetchImpl,
754
+ void 0,
755
+ options.signContext?.clientIdentity
756
+ );
713
757
  } catch (directErr) {
714
758
  if (directErr instanceof HavenSignContextError && directErr.http_status === 409 && directErr.backend_error_code === "sign_context_unavailable") {
715
759
  throw err;
@@ -759,12 +803,12 @@ function createToolHandlers(signer, options = {}) {
759
803
  assertBoundDirectPaymentUserOp(typedData, signer.delegateAddress);
760
804
  const signature = await signer.signDelegationTypedData(typedData);
761
805
  await auditSigning("haven_sign", hashTypedData(typedData));
762
- return { signature };
806
+ return { signature, ...clientUpdateField(resolved) };
763
807
  }
764
808
  throw new HavenBareHashRefusedError();
765
809
  }
766
810
  await auditSigning("haven_sign", hashTypedData(typedData));
767
- return { signature: result.signature, x402_binding: result.x402Binding };
811
+ return { signature: result.signature, x402_binding: result.x402Binding, ...clientUpdateField(resolved) };
768
812
  }),
769
813
  haven_x402_sign_header: async (input) => runTool(async () => {
770
814
  const args = parse("haven_x402_sign_header", coercePaymentRequired(input));
@@ -810,7 +854,8 @@ function createToolHandlers(signer, options = {}) {
810
854
  signature: funding.signature,
811
855
  x402_binding: funding.x402Binding,
812
856
  payment_header: header.paymentHeader,
813
- accepted: header.accepted
857
+ accepted: header.accepted,
858
+ ...clientUpdateField(resolved)
814
859
  };
815
860
  }),
816
861
  haven_sign_sweep_delegate: async (input) => runTool(async () => {
@@ -949,6 +994,7 @@ function normalizeError(err) {
949
994
  ...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
950
995
  ...err.http_status !== void 0 ? { http_status: err.http_status } : {},
951
996
  ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {},
997
+ ...err.client_update ? { client_update: err.client_update } : {},
952
998
  // #3103: the typed step the error decided beside its action.
953
999
  ...err.next_tool ? { next_tool: err.next_tool } : {},
954
1000
  ...err.next_tool_server ? { next_tool_server: err.next_tool_server } : {},
@@ -1661,7 +1707,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
1661
1707
 
1662
1708
  // src/server.ts
1663
1709
  var SIGNER_NAME = "@haven_ai/signer";
1664
- var SIGNER_VERSION = "0.0.0-dev.202609250737.3bd5a51";
1710
+ var SIGNER_VERSION = "0.0.0-dev.202609251409.98ed67a";
1665
1711
  async function resolveSignerRuntime(options = {}) {
1666
1712
  assertSupportedNodeVersion(options.nodeVersion);
1667
1713
  if (options.delegateKey) {
@@ -1706,7 +1752,10 @@ function buildSignerMcpServer(signer, options = {}) {
1706
1752
  // The signer CORE stays network-free; fetched bytes still pass the same
1707
1753
  // binding verification + digest re-derivation as tool-argument bytes.
1708
1754
  signContext: {
1709
- loadIdentity: () => loadHavenIdentity(credentialsPath)
1755
+ loadIdentity: () => loadHavenIdentity(credentialsPath),
1756
+ // #3303: the backend refuses a signer below a minimum it has set, at
1757
+ // sign-context — the one place this signer meets it.
1758
+ clientIdentity: havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
1710
1759
  }
1711
1760
  });
1712
1761
  const registerTool = server.tool.bind(server);