@haven_ai/signer 0.0.0-dev.202609282018.d7956ad → 0.0.0-dev.202609291042.e2cc5e2

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.cjs CHANGED
@@ -446,7 +446,120 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
446
446
  clientUpdate: edge.readClientUpdate(body.client_update)
447
447
  };
448
448
  }
449
- var SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS = [1];
449
+ var SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS = [edge.TASK_SIGN_CONTEXT_VERSION];
450
+ var SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS = [1];
451
+ async function fetchSubBudgetSignContext(identity, subBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
452
+ let response;
453
+ try {
454
+ response = await fetchImpl(
455
+ `${identity.apiUrl}/sub-budgets/${encodeURIComponent(subBudgetId)}/sign-context`,
456
+ {
457
+ headers: signContextHeaders(identity, clientIdentity),
458
+ signal: AbortSignal.timeout(timeoutMs)
459
+ }
460
+ );
461
+ } catch (err) {
462
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
463
+ throw new HavenSignContextError(
464
+ timedOut ? `Haven did not answer the sub-budget signing-context fetch for ${subBudgetId} within ${timeoutMs} ms. Retry.` : `Could not reach Haven to fetch the sub-budget signing context for ${subBudgetId}: ${err instanceof Error ? err.message : String(err)}.`,
465
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
466
+ void 0,
467
+ subBudgetId,
468
+ "task"
469
+ );
470
+ }
471
+ let body;
472
+ try {
473
+ body = await response.json();
474
+ } catch (err) {
475
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
476
+ throw new HavenSignContextError(
477
+ `Haven did not finish sending the sub-budget signing context for ${subBudgetId} within ${timeoutMs} ms. Retry.`,
478
+ "SIGN_CONTEXT_TIMEOUT",
479
+ void 0,
480
+ subBudgetId,
481
+ "task"
482
+ );
483
+ }
484
+ body = {};
485
+ }
486
+ if (!response.ok) {
487
+ const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
488
+ throw new HavenSignContextError(
489
+ `Haven refused the sub-budget signing-context fetch for ${subBudgetId}: ${detail}`,
490
+ "SIGN_CONTEXT_REFUSED",
491
+ refusalOf(response.status, body),
492
+ subBudgetId,
493
+ "task"
494
+ );
495
+ }
496
+ const purpose = body.purpose;
497
+ const typedData = body.typed_data;
498
+ const version = body.sub_budget_sign_context_version;
499
+ const expected = body.expected;
500
+ const malformed = (detail) => {
501
+ throw new HavenSignContextError(
502
+ `The Haven sub-budget sign-context response for ${subBudgetId} is malformed: ${detail}.`,
503
+ "SIGN_CONTEXT_MALFORMED",
504
+ void 0,
505
+ subBudgetId,
506
+ "task"
507
+ );
508
+ };
509
+ if (typeof version !== "number" || !SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS.includes(version)) {
510
+ throw new HavenSignContextError(
511
+ `The Haven sub-budget sign-context response is version ${String(version)}, which this signer does not support (supported: ${SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.`,
512
+ "SIGN_CONTEXT_MALFORMED",
513
+ void 0,
514
+ subBudgetId,
515
+ "task"
516
+ );
517
+ }
518
+ if (!typedData || typeof typedData !== "object") return malformed("missing typed_data");
519
+ if (!expected || typeof expected !== "object") return malformed("missing expected");
520
+ const e = expected;
521
+ if (purpose === "open") {
522
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.token_address !== "string" || typeof e.period_amount_atomic !== "string" || typeof e.period_duration_seconds !== "number" || typeof e.start_date !== "number" || e.recipient_address !== null && typeof e.recipient_address !== "string" || typeof e.expires_at !== "number" || typeof e.parent_delegation_hash !== "string" || typeof e.child_delegate_account !== "string") {
523
+ return malformed("expected is missing a required field for purpose open");
524
+ }
525
+ return {
526
+ purpose: "open",
527
+ subBudgetId: String(body.sub_budget_id ?? subBudgetId),
528
+ typedData,
529
+ expected: {
530
+ delegateAccount: e.delegate_account,
531
+ chainId: e.chain_id,
532
+ tokenAddress: e.token_address,
533
+ periodAmountAtomic: e.period_amount_atomic,
534
+ periodDurationSeconds: e.period_duration_seconds,
535
+ startDate: e.start_date,
536
+ recipientAddress: e.recipient_address ?? null,
537
+ expiresAt: e.expires_at,
538
+ parentDelegationHash: e.parent_delegation_hash,
539
+ childDelegateAccount: e.child_delegate_account
540
+ }
541
+ };
542
+ }
543
+ if (purpose === "close") {
544
+ const userOpHash = body.user_op_hash;
545
+ if (typeof userOpHash !== "string") return malformed("missing user_op_hash for purpose close");
546
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.delegation_hash !== "string") {
547
+ return malformed("expected is missing a required field for purpose close");
548
+ }
549
+ return {
550
+ purpose: "close",
551
+ subBudgetId: String(body.sub_budget_id ?? subBudgetId),
552
+ typedData,
553
+ userOpHash,
554
+ expected: {
555
+ delegateAccount: e.delegate_account,
556
+ chainId: e.chain_id,
557
+ delegationHash: e.delegation_hash
558
+ }
559
+ };
560
+ }
561
+ return malformed(`unknown purpose '${String(purpose)}'`);
562
+ }
450
563
  async function fetchTaskBudgetSignContext(identity, taskBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
451
564
  let response;
452
565
  try {
@@ -558,6 +671,9 @@ async function fetchTaskBudgetSignContext(identity, taskBudgetId, fetchImpl = fe
558
671
  }
559
672
 
560
673
  // src/tools.ts
674
+ var SIGNER_NAME = "@haven_ai/signer";
675
+ var SIGNER_VERSION = "0.0.0-dev.202609291042.e2cc5e2";
676
+ var UNSUPPORTED_ARGUMENT_MARKER = "HavenSignerUnsupportedArgument:";
561
677
  var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
562
678
  var HavenUserOpBindingRefusedError = class extends edge.HavenSigningError {
563
679
  next_action = edge.AgentPaymentNextAction.StopAndTellUser;
@@ -685,7 +801,13 @@ var toolSchemas = {
685
801
  // Mutually exclusive with payment_id / payload_hash (checked in the
686
802
  // handler, since Zod's object-level refine on a raw shape used by
687
803
  // several call sites is not the shape this schema is composed as).
688
- task_budget_id: v3.z.string().min(1).optional()
804
+ task_budget_id: v3.z.string().min(1).optional(),
805
+ // #3330: sign a sub-budget's own child delegation or its early-close
806
+ // UserOp — same fetch-verify-sign channel as task_budget_id, against
807
+ // `GET /sub-budgets/:id/sign-context`. Mutually exclusive with
808
+ // payment_id / payload_hash AND with task_budget_id (checked in the
809
+ // handler).
810
+ sub_budget_id: v3.z.string().min(1).optional()
689
811
  },
690
812
  haven_x402_sign_header: {
691
813
  // The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
@@ -966,9 +1088,76 @@ function createToolHandlers(signer, options = {}) {
966
1088
  next_arguments: { task_budget_id: ctx.taskBudgetId, signature }
967
1089
  };
968
1090
  }
1091
+ async function signSubBudget(subBudgetId) {
1092
+ const identity = await options.signContext?.loadIdentity() ?? null;
1093
+ if (!identity) {
1094
+ throw new edge.HavenSigningError(
1095
+ `sub_budget_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${edge.connectorRerunCommand()}\` to restore it.`
1096
+ );
1097
+ }
1098
+ const ctx = await fetchSubBudgetSignContext(
1099
+ identity,
1100
+ subBudgetId,
1101
+ options.signContext?.fetchImpl,
1102
+ void 0,
1103
+ options.signContext?.clientIdentity
1104
+ );
1105
+ let signature;
1106
+ if (ctx.purpose === "open") {
1107
+ try {
1108
+ edge.assertOwnSubBudgetChild(
1109
+ ctx.typedData,
1110
+ {
1111
+ chainId: ctx.expected.chainId,
1112
+ tokenAddress: ctx.expected.tokenAddress,
1113
+ periodAmountAtomic: ctx.expected.periodAmountAtomic,
1114
+ periodDurationSeconds: ctx.expected.periodDurationSeconds,
1115
+ startDate: ctx.expected.startDate,
1116
+ recipientAddress: ctx.expected.recipientAddress,
1117
+ expiresAt: ctx.expected.expiresAt,
1118
+ parentDelegationHash: ctx.expected.parentDelegationHash,
1119
+ delegateAccount: ctx.expected.delegateAccount,
1120
+ childDelegateAccount: ctx.expected.childDelegateAccount
1121
+ },
1122
+ signer.delegateAddress
1123
+ );
1124
+ } catch (err) {
1125
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1126
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1127
+ }
1128
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1129
+ } else {
1130
+ try {
1131
+ edge.assertUserOpTypedDataBinding(ctx.typedData, ctx.userOpHash);
1132
+ } catch (err) {
1133
+ if (err instanceof edge.HavenUserOpBindingError) throw new HavenUserOpBindingRefusedError(err.message);
1134
+ throw err;
1135
+ }
1136
+ try {
1137
+ edge.assertOwnSubBudgetCloseUserOp(
1138
+ ctx.typedData,
1139
+ { delegationHash: ctx.expected.delegationHash },
1140
+ signer.delegateAddress
1141
+ );
1142
+ } catch (err) {
1143
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1144
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1145
+ }
1146
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1147
+ }
1148
+ await auditSigning("haven_sign", viem.hashTypedData(ctx.typedData));
1149
+ return {
1150
+ signature,
1151
+ sub_budget_id: ctx.subBudgetId,
1152
+ purpose: ctx.purpose,
1153
+ next_tool_name: "haven_submit",
1154
+ next_tool_server_role: "hosted",
1155
+ next_arguments: { sub_budget_id: ctx.subBudgetId, signature }
1156
+ };
1157
+ }
969
1158
  return {
970
1159
  haven_sign: async (input) => runTool(async () => {
971
- const args = parse("haven_sign", coerceX402Expected(input));
1160
+ const args = parseStrictFor("haven_sign", coerceX402Expected(input));
972
1161
  if (args.task_budget_id) {
973
1162
  if (args.payment_id || args.payload_hash) {
974
1163
  throw new v3.z.ZodError([
@@ -979,8 +1168,29 @@ function createToolHandlers(signer, options = {}) {
979
1168
  }
980
1169
  ]);
981
1170
  }
1171
+ if (args.sub_budget_id) {
1172
+ throw new v3.z.ZodError([
1173
+ {
1174
+ code: v3.z.ZodIssueCode.custom,
1175
+ path: ["sub_budget_id"],
1176
+ message: "task_budget_id is mutually exclusive with sub_budget_id \u2014 pass exactly one."
1177
+ }
1178
+ ]);
1179
+ }
982
1180
  return signTaskBudget(args.task_budget_id);
983
1181
  }
1182
+ if (args.sub_budget_id) {
1183
+ if (args.payment_id || args.payload_hash) {
1184
+ throw new v3.z.ZodError([
1185
+ {
1186
+ code: v3.z.ZodIssueCode.custom,
1187
+ path: ["sub_budget_id"],
1188
+ message: "sub_budget_id is mutually exclusive with payment_id / payload_hash \u2014 pass exactly one."
1189
+ }
1190
+ ]);
1191
+ }
1192
+ return signSubBudget(args.sub_budget_id);
1193
+ }
984
1194
  const resolved = await resolveSignContext(args, { allowDirectFallback: true });
985
1195
  const typedData = resolved?.ctx.typedData ?? resolveTypedData(args);
986
1196
  const payloadHash = resolved?.ctx.payloadHash ?? args.payload_hash;
@@ -1023,7 +1233,7 @@ function createToolHandlers(signer, options = {}) {
1023
1233
  return { signature: result.signature, x402_binding: result.x402Binding, ...clientUpdateField(resolved) };
1024
1234
  }),
1025
1235
  haven_x402_sign_header: async (input) => runTool(async () => {
1026
- const args = parse("haven_x402_sign_header", coercePaymentRequired(input));
1236
+ const args = parseStrictFor("haven_x402_sign_header", coercePaymentRequired(input));
1027
1237
  const result = await signer.buildX402PaymentHeader(
1028
1238
  args.payment_required,
1029
1239
  args.x402_binding
@@ -1035,7 +1245,7 @@ function createToolHandlers(signer, options = {}) {
1035
1245
  return { payment_header: result.paymentHeader, accepted: result.accepted };
1036
1246
  }),
1037
1247
  haven_sign_x402: async (input) => runTool(async () => {
1038
- const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
1248
+ const args = parseStrictFor("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
1039
1249
  const resolved = await resolveSignContext(args, { allowDirectFallback: false });
1040
1250
  const fetched = resolved?.kind === "x402" ? resolved.ctx : void 0;
1041
1251
  const payloadHash = fetched?.payloadHash ?? args.payload_hash;
@@ -1071,7 +1281,7 @@ function createToolHandlers(signer, options = {}) {
1071
1281
  };
1072
1282
  }),
1073
1283
  haven_sign_sweep_delegate: async (input) => runTool(async () => {
1074
- const args = parse("haven_sign_sweep_delegate", input);
1284
+ const args = parseStrictFor("haven_sign_sweep_delegate", input);
1075
1285
  const result = await signer.signSweepAuthorization({
1076
1286
  authorization: args.authorization,
1077
1287
  expectedAuth: args.expected_auth,
@@ -1104,8 +1314,18 @@ function createToolHandlers(signer, options = {}) {
1104
1314
  }
1105
1315
  }
1106
1316
  }
1107
- function parse(name, input) {
1108
- return v3.z.object(toolSchemas[name]).parse(input ?? {});
1317
+ function parseStrictFor(name, input) {
1318
+ const result = v3.z.object(toolSchemas[name]).strict().safeParse(input ?? {});
1319
+ if (result.success) return result.data;
1320
+ const topLevelUnknown = result.error.issues.flatMap(
1321
+ (issue) => issue.code === "unrecognized_keys" && issue.path.length === 0 ? issue.keys : []
1322
+ );
1323
+ if (topLevelUnknown.length === 0) throw result.error;
1324
+ const unknown = [...new Set(topLevelUnknown)];
1325
+ const err = new Error(
1326
+ `${UNSUPPORTED_ARGUMENT_MARKER} ${name} does not accept ${unknown.map((k) => `"${k}"`).join(", ")} \u2014 this signer predates that argument form. Nothing was signed or fetched.`
1327
+ );
1328
+ throw err;
1109
1329
  }
1110
1330
  function coerceX402Expected(input) {
1111
1331
  if (!input || typeof input !== "object") return input;
@@ -1144,6 +1364,25 @@ async function runTool(fn) {
1144
1364
  }
1145
1365
  }
1146
1366
  function normalizeError(err) {
1367
+ if (err instanceof Error && err.message.startsWith(UNSUPPORTED_ARGUMENT_MARKER)) {
1368
+ const unknown = [...err.message.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
1369
+ return {
1370
+ success: false,
1371
+ code: "UNSUPPORTED_ARGUMENT",
1372
+ message: err.message.replace(`${UNSUPPORTED_ARGUMENT_MARKER} `, ""),
1373
+ unknown_arguments: unknown,
1374
+ signer_version: SIGNER_VERSION,
1375
+ fallback: edge.connectorUpgradeCommand(),
1376
+ next_action: edge.AgentPaymentNextAction.StopAndTellUser,
1377
+ ...nextStepWireFields(
1378
+ signerRefusalStep({
1379
+ nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
1380
+ nextTool: null,
1381
+ nextToolOmittedReason: `update @haven_ai/signer: run ${edge.connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
1382
+ })
1383
+ )
1384
+ };
1385
+ }
1147
1386
  if (err instanceof v3.z.ZodError) {
1148
1387
  return {
1149
1388
  success: false,
@@ -1819,7 +2058,13 @@ function signerInstructions() {
1819
2058
  "fallback fields alongside the message, so you can branch on it directly. Every signer",
1820
2059
  "refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
1821
2060
  "hosted tool follows (resolve the role against your own server names), else",
1822
- "next_tool_omitted_reason saying why not."
2061
+ "next_tool_omitted_reason saying why not.",
2062
+ "",
2063
+ "An undeclared top-level argument is refused, not stripped: haven_sign answers",
2064
+ "UNSUPPORTED_ARGUMENT with unknown_arguments, signer_version and fallback (the update",
2065
+ "command) instead of dropping the key and answering the generic signing error \u2014 if you see",
2066
+ "that refusal, this signer predates the argument form you sent. Update the signer; nothing",
2067
+ "was signed, fetched or audited."
1823
2068
  ].join("\n");
1824
2069
  }
1825
2070
  async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
@@ -1922,8 +2167,6 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
1922
2167
  }
1923
2168
 
1924
2169
  // src/server.ts
1925
- var SIGNER_NAME = "@haven_ai/signer";
1926
- var SIGNER_VERSION = "0.0.0-dev.202609282018.d7956ad";
1927
2170
  async function resolveSignerRuntime(options = {}) {
1928
2171
  assertSupportedNodeVersion(options.nodeVersion);
1929
2172
  if (options.delegateKey) {
@@ -1974,12 +2217,14 @@ function buildSignerMcpServer(signer, options = {}) {
1974
2217
  clientIdentity: edge.havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
1975
2218
  }
1976
2219
  });
1977
- const registerTool = server.tool.bind(server);
2220
+ const registerTool = server.registerTool.bind(server);
1978
2221
  for (const name of Object.keys(toolSchemas)) {
1979
2222
  registerTool(
1980
2223
  name,
1981
- toolDescriptions[name],
1982
- toolSchemas[name],
2224
+ {
2225
+ description: toolDescriptions[name],
2226
+ inputSchema: zod.z.object(toolSchemas[name]).passthrough()
2227
+ },
1983
2228
  async (args) => toMcpResult(await handlers[name](args))
1984
2229
  );
1985
2230
  }