@haven_ai/signer 0.3.0-alpha.0 → 0.4.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/README.md CHANGED
@@ -180,8 +180,8 @@ what a payload means; they re-derive it.
180
180
  allowed — top-level caveats are AND-ed during redemption, so an unrecognised
181
181
  one can only add a constraint.
182
182
  - **A binding version it does not understand.** The refusal is machine-readable
183
- — `code`, `supported_versions`, `received_version`, `fallback` — and names
184
- updating the signer as the fix.
183
+ — `code`, `supported_versions`, `received_version`, `fallback`, and (#3103)
184
+ `next_tool_omitted_reason` — and names updating the signer as the fix.
185
185
  - **A sweep that does not move funds out of this delegate's own key** — the
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
@@ -206,7 +206,12 @@ signing payload from Haven (`GET /x402/:id/sign-context`, see
206
206
  still holds everywhere it did before, but structured like the version-mismatch
207
207
  refusal below rather than prose alone: `code`, `next_action`, and — per
208
208
  refusal class — `fallback`, `retry_with_new_quote`, `http_status`,
209
- `backend_error_code`. `message` is unchanged.
209
+ `backend_error_code`. Since #3103 each also carries a typed next step: the
210
+ `SIGN_CONTEXT_REFUSED` (other) row names the hosted status read
211
+ (`next_tool_server_role: hosted`, `next_tool_name: haven_get_payment_status`,
212
+ `next_arguments: { payment_id }` — resolve the role against your own server
213
+ names); every other row carries `next_tool_omitted_reason` with the exact
214
+ remedy. `message` is unchanged.
210
215
 
211
216
  | `code` | When | `next_action` | `fallback` | extra |
212
217
  |---|---|---|---|---|
package/dist/cli.cjs CHANGED
@@ -12,6 +12,7 @@ var viem = require('viem');
12
12
  var accounts = require('viem/accounts');
13
13
  var schemes = require('x402/schemes');
14
14
  var v3 = require('zod/v3');
15
+ var zod = require('zod');
15
16
 
16
17
  function defaultSigningAuditPath(credentialsPath) {
17
18
  if (credentialsPath) return path.resolve(`${credentialsPath}.signer-audit.jsonl`);
@@ -531,9 +532,44 @@ function signerInstructions() {
531
532
  "",
532
533
  "A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
533
534
  "machine-readable, not just prose: it carries code, supported_versions, received_version, and",
534
- "fallback fields alongside the message, so you can branch on it directly."
535
+ "fallback fields alongside the message, so you can branch on it directly. Every signer",
536
+ "refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
537
+ "hosted tool follows (resolve the role against your own server names), else",
538
+ "next_tool_omitted_reason saying why not."
535
539
  ].join("\n");
536
540
  }
541
+ var SIGNER_HOSTED_HANDOFF_SHAPES = {
542
+ haven_get_payment_status: { payment_id: zod.z.string().min(1) }
543
+ };
544
+ function target(shape) {
545
+ const schema = zod.z.object(shape).strict();
546
+ return {
547
+ role: "hosted",
548
+ validate: (input) => {
549
+ const r = schema.safeParse(input ?? {});
550
+ return r.success ? null : r.error.errors.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join("; ");
551
+ }
552
+ };
553
+ }
554
+ var SIGNER_NEXT_STEP_TARGETS = {
555
+ haven_get_payment_status: target(SIGNER_HOSTED_HANDOFF_SHAPES.haven_get_payment_status)
556
+ };
557
+ var nextStep = sdk.createNextStepBuilder(SIGNER_NEXT_STEP_TARGETS);
558
+ function signerRefusalStep(input) {
559
+ return nextStep({ ...input, safeToContinue: false, reason: "" });
560
+ }
561
+ function nextStepWireFields(step) {
562
+ return {
563
+ ...step.next_tool ? { next_tool: step.next_tool } : {},
564
+ ...step.next_tool_server ? { next_tool_server: step.next_tool_server } : {},
565
+ ...step.next_tool_name ? { next_tool_name: step.next_tool_name } : {},
566
+ ...step.next_tool_server_role ? { next_tool_server_role: step.next_tool_server_role } : {},
567
+ ...step.next_arguments ? { next_arguments: step.next_arguments } : {},
568
+ ...step.next_tool_omitted_reason ? { next_tool_omitted_reason: step.next_tool_omitted_reason } : {}
569
+ };
570
+ }
571
+
572
+ // src/sign-context.ts
537
573
  var HavenSignContextError = class extends sdk.HavenSigningError {
538
574
  /**
539
575
  * #3001 / #3010 review: the recovery path, when signing OTHER bytes is
@@ -553,28 +589,63 @@ var HavenSignContextError = class extends sdk.HavenSigningError {
553
589
  * plain `HavenError` branch already does for that code.
554
590
  */
555
591
  next_action;
592
+ /**
593
+ * #3103 (epic #3105, decisions 1 and 3): the typed next step beside the
594
+ * action. A refusal Haven made (`SIGN_CONTEXT_REFUSED`, not expired) names
595
+ * the hosted status read with the payment id; a transport failure or a
596
+ * malformed body names no tool — the remedy is re-running the SAME quote
597
+ * tool with `include_signing_payload: true` — and an expired window names
598
+ * none either (which quote tool depends on the flow). Never null: a step
599
+ * with no tool carries `next_tool_omitted_reason`. Additive: the class is
600
+ * not exported from the package index — the published surface is the
601
+ * `ToolFailure` envelope `tools.ts` builds from these fields — and every
602
+ * constructor call site is in this file.
603
+ */
604
+ next_tool;
605
+ next_tool_server;
606
+ next_tool_name;
607
+ next_tool_server_role;
608
+ next_arguments;
609
+ next_tool_omitted_reason;
556
610
  retry_with_new_quote;
557
611
  /** Present only for `SIGN_CONTEXT_REFUSED` (the backend's HTTP status). */
558
612
  http_status;
559
613
  /** Present only for `SIGN_CONTEXT_REFUSED`: the backend's own `error_code`. */
560
614
  backend_error_code;
561
- constructor(message, code, refusal) {
615
+ constructor(message, code, refusal, paymentId) {
562
616
  super(message);
563
617
  this.code = code;
564
618
  this.name = "HavenSignContextError";
619
+ let step;
565
620
  if (code === "SIGN_CONTEXT_REFUSED") {
566
621
  this.http_status = refusal?.httpStatus;
567
622
  this.backend_error_code = refusal?.errorCode;
568
623
  if (refusal?.httpStatus === 410 || refusal?.errorCode === "expired") {
569
624
  this.next_action = sdk.AgentPaymentNextAction.PaymentWindowExpired;
570
625
  this.retry_with_new_quote = true;
626
+ step = signerRefusalStep({
627
+ nextAction: sdk.AgentPaymentNextAction.PaymentWindowExpired,
628
+ nextTool: null,
629
+ nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow"
630
+ });
571
631
  } else {
572
632
  this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
633
+ step = signerRefusalStep({
634
+ nextAction: sdk.AgentPaymentNextAction.StopAndTellUser,
635
+ nextTool: "haven_get_payment_status",
636
+ nextArguments: { payment_id: paymentId }
637
+ });
573
638
  }
574
639
  } else {
575
640
  this.fallback = "typed_data_b64";
576
641
  this.next_action = sdk.AgentPaymentNextAction.StopAndTellUser;
642
+ step = signerRefusalStep({
643
+ nextAction: sdk.AgentPaymentNextAction.StopAndTellUser,
644
+ nextTool: null,
645
+ nextToolOmittedReason: "re-run the SAME hosted quote tool with the same idempotency_key and include_signing_payload: true, then pass its typed_data_b64 to this signer"
646
+ });
577
647
  }
648
+ Object.assign(this, nextStepWireFields(step));
578
649
  }
579
650
  };
580
651
  async function loadHavenIdentity(credentialsPath) {
@@ -606,7 +677,9 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
606
677
  const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
607
678
  throw new HavenSignContextError(
608
679
  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"
680
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
681
+ void 0,
682
+ paymentId
610
683
  );
611
684
  }
612
685
  let body;
@@ -616,7 +689,9 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
616
689
  if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
617
690
  throw new HavenSignContextError(
618
691
  `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"
692
+ "SIGN_CONTEXT_TIMEOUT",
693
+ void 0,
694
+ paymentId
620
695
  );
621
696
  }
622
697
  body = {};
@@ -626,7 +701,8 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
626
701
  throw new HavenSignContextError(
627
702
  `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
703
  "SIGN_CONTEXT_REFUSED",
629
- { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 }
704
+ { httpStatus: response.status, errorCode: typeof body.error_code === "string" ? body.error_code : void 0 },
705
+ paymentId
630
706
  );
631
707
  }
632
708
  const signData = body.sign_data;
@@ -634,7 +710,9 @@ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch, time
634
710
  if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
635
711
  throw new HavenSignContextError(
636
712
  "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"
713
+ "SIGN_CONTEXT_MALFORMED",
714
+ void 0,
715
+ paymentId
638
716
  );
639
717
  }
640
718
  const paymentRequired = body.payment_required;
@@ -758,6 +836,7 @@ var toolSchemas = {
758
836
  // #1255: see haven_sign.typed_data_b64 — the copy-through-safe form.
759
837
  typed_data_b64: v3.z.string().min(1).max(262144).optional()
760
838
  }
839
+ // #3101: keys survive on the type (see the hosted server's contracts.ts).
761
840
  };
762
841
  var SIGN_DESCRIPTION = [
763
842
  "Sign an unsigned Haven payment hash with the local delegate key. The delegate key never leaves",
@@ -1062,7 +1141,13 @@ function normalizeError(err) {
1062
1141
  supported_versions: [...err.supportedVersions],
1063
1142
  received_version: err.receivedVersion,
1064
1143
  fallback: err.fallback,
1065
- next_action: sdk.AgentPaymentNextAction.StopAndTellUser
1144
+ next_action: sdk.AgentPaymentNextAction.StopAndTellUser,
1145
+ // #3103: no tool can fix a version skew from inside the call.
1146
+ ...nextStepWireFields(signerRefusalStep({
1147
+ nextAction: sdk.AgentPaymentNextAction.StopAndTellUser,
1148
+ nextTool: null,
1149
+ nextToolOmittedReason: "update @haven_ai/signer by re-running the connector, then repeat the same call"
1150
+ }))
1066
1151
  };
1067
1152
  }
1068
1153
  if (err instanceof HavenSignContextError) {
@@ -1074,7 +1159,14 @@ function normalizeError(err) {
1074
1159
  ...err.fallback !== void 0 ? { fallback: err.fallback } : {},
1075
1160
  ...err.retry_with_new_quote ? { retry_with_new_quote: true } : {},
1076
1161
  ...err.http_status !== void 0 ? { http_status: err.http_status } : {},
1077
- ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {}
1162
+ ...err.backend_error_code !== void 0 ? { backend_error_code: err.backend_error_code } : {},
1163
+ // #3103: the typed step the error decided beside its action.
1164
+ ...err.next_tool ? { next_tool: err.next_tool } : {},
1165
+ ...err.next_tool_server ? { next_tool_server: err.next_tool_server } : {},
1166
+ ...err.next_tool_name ? { next_tool_name: err.next_tool_name } : {},
1167
+ ...err.next_tool_server_role ? { next_tool_server_role: err.next_tool_server_role } : {},
1168
+ ...err.next_arguments ? { next_arguments: err.next_arguments } : {},
1169
+ ...err.next_tool_omitted_reason ? { next_tool_omitted_reason: err.next_tool_omitted_reason } : {}
1078
1170
  };
1079
1171
  }
1080
1172
  if (err instanceof sdk.HavenSigningError) {
@@ -1093,7 +1185,13 @@ function normalizeError(err) {
1093
1185
  ...err.code === sdk.AgentPaymentFailureCode.PaymentWindowExpired ? {
1094
1186
  next_action: sdk.AgentPaymentNextAction.PaymentWindowExpired,
1095
1187
  retry_with_new_quote: true,
1096
- suggested_tool: "haven_pay_mcp_tool"
1188
+ suggested_tool: "haven_pay_mcp_tool",
1189
+ // #3103: same omission as the hosted window-expired helper.
1190
+ ...nextStepWireFields(signerRefusalStep({
1191
+ nextAction: sdk.AgentPaymentNextAction.PaymentWindowExpired,
1192
+ nextTool: null,
1193
+ nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow (suggested_tool names the MCP one)"
1194
+ }))
1097
1195
  } : {}
1098
1196
  };
1099
1197
  }
@@ -1335,7 +1433,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
1335
1433
 
1336
1434
  // src/server.ts
1337
1435
  var SIGNER_NAME = "@haven_ai/signer";
1338
- var SIGNER_VERSION = "0.3.0-alpha.0";
1436
+ var SIGNER_VERSION = "0.4.0-alpha.0";
1339
1437
  async function resolveSignerRuntime(options = {}) {
1340
1438
  assertSupportedNodeVersion(options.nodeVersion);
1341
1439
  if (options.delegateKey) {