@haven_ai/signer 0.6.0-alpha.0 → 0.7.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 +7 -3
- package/dist/cli.cjs +267 -20
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +268 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +268 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -4
- package/dist/index.d.ts +31 -4
- package/dist/index.js +269 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,8 +43,12 @@ every "rerun the connector" hint it prints names the channel THIS build was
|
|
|
43
43
|
published under, so a build installed from a non-production channel tells you
|
|
44
44
|
to reinstall from that same channel rather than sending you to production.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
backend's expected-context version
|
|
46
|
+
That command SETS UP an agent. To update a signer that has fallen behind the
|
|
47
|
+
backend's expected-context version on a machine that is already set up, run
|
|
48
|
+
the connector doctor instead — `npx -y @haven_ai/connect@<channel> --doctor` —
|
|
49
|
+
and then the `--repair` line it prints; a bare re-run stops at "Missing
|
|
50
|
+
--setup" (#3412). Every update hint this signer prints names that form. To run
|
|
51
|
+
the signer directly:
|
|
48
52
|
|
|
49
53
|
```sh
|
|
50
54
|
HAVEN_DELEGATE_KEY=0x... npx @haven_ai/signer@alpha
|
|
@@ -337,7 +341,7 @@ carries no x402 context to fund a merchant retry with, so it surfaces the
|
|
|
337
341
|
| `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` | — |
|
|
338
342
|
| `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 |
|
|
339
343
|
| `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` |
|
|
340
|
-
| `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.
|
|
344
|
+
| `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. Run `client_update.upgrade_command` and the repair line it prints, 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 |
|
|
341
345
|
| `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` |
|
|
342
346
|
| `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 |
|
|
343
347
|
| `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
|
@@ -214,7 +214,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
|
|
|
214
214
|
/**
|
|
215
215
|
* #3303: on a 426 `client_outdated` refusal, the backend's update hint —
|
|
216
216
|
* this signer is below the minimum the deployment set, and
|
|
217
|
-
* `client_update.upgrade_command`
|
|
217
|
+
* `client_update.upgrade_command` starts the update (#3412: for the signer, the doctor, then its repair line).
|
|
218
218
|
*/
|
|
219
219
|
client_update;
|
|
220
220
|
constructor(message, code, refusal, paymentId, flow = "x402") {
|
|
@@ -231,7 +231,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
|
|
|
231
231
|
step = signerRefusalStep({
|
|
232
232
|
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
233
233
|
nextTool: null,
|
|
234
|
-
nextToolOmittedReason: `this signer is below the minimum version Haven accepts \u2014 tell the user to run ${refusal.clientUpdate?.upgrade_command ??
|
|
234
|
+
nextToolOmittedReason: `this signer is below the minimum version Haven accepts \u2014 tell the user to run ${refusal.clientUpdate?.upgrade_command ?? edge.connectorUpgradeCommand()} and the repair line it prints, restart the agent runtime, then retry the same haven_sign / haven_sign_x402 call`
|
|
235
235
|
});
|
|
236
236
|
} else if (flow === "direct" && refusal?.httpStatus === 404) {
|
|
237
237
|
this.fallback = "typed_data_b64";
|
|
@@ -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 = [
|
|
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.7.0-alpha.0";
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1108
|
-
|
|
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,
|
|
@@ -1165,7 +1404,9 @@ function normalizeError(err) {
|
|
|
1165
1404
|
...nextStepWireFields(signerRefusalStep({
|
|
1166
1405
|
nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
|
|
1167
1406
|
nextTool: null,
|
|
1168
|
-
|
|
1407
|
+
// #3412: the upgrade command, not "re-run the connector" — a bare re-run
|
|
1408
|
+
// stops at "Missing --setup" on an existing install.
|
|
1409
|
+
nextToolOmittedReason: `update @haven_ai/signer: run ${edge.connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
|
|
1169
1410
|
}))
|
|
1170
1411
|
};
|
|
1171
1412
|
}
|
|
@@ -1682,7 +1923,7 @@ function assertSupportedBindingVersion(received, supported, context) {
|
|
|
1682
1923
|
const highest = Math.max(...supported);
|
|
1683
1924
|
const outOfDate = received > highest;
|
|
1684
1925
|
const code = context === "x402 expected context" ? edge.SignerRefusalCode.UnsupportedExpectedContextVersion : edge.SignerRefusalCode.UnsupportedSweepBindingVersion;
|
|
1685
|
-
const ceiling = outOfDate ? `This signer is out of date: it supports ${context} versions up to ${highest}, and Haven sent version ${received}. Update @haven_ai/signer \u2014
|
|
1926
|
+
const ceiling = outOfDate ? `This signer is out of date: it supports ${context} versions up to ${highest}, and Haven sent version ${received}. Update @haven_ai/signer \u2014 run \`${edge.connectorUpgradeCommand()}\`, then the repair line it prints, which reinstalls the pinned MCP runtime.` : `Unsupported ${context} version ${received}: this signer supports ${supported.join(", ")}.`;
|
|
1686
1927
|
const fallback = outOfDate ? edge.SIGNER_UPDATE_FALLBACK : `This ${context} version (${received}) is older than what this signer enforces (${supported.join(", ")}) \u2014 updating @haven_ai/signer will not restore it. Nothing was signed or spent; stop and tell the user rather than retrying.`;
|
|
1687
1928
|
throw new edge.HavenUnsupportedSignerVersionError(
|
|
1688
1929
|
`${ceiling} Nothing was signed. Do not rewrite the version field to a supported value: it is part of the Haven-signed binding message, so changing it invalidates the signature and would misrepresent what Haven declared.`,
|
|
@@ -1808,8 +2049,8 @@ function signerInstructions() {
|
|
|
1808
2049
|
"Haven quote and prepare results report the expected-context version they will emit",
|
|
1809
2050
|
"(signer_compatibility.x402_expected_context_version). If that version is not in the list",
|
|
1810
2051
|
"above, this signer is out of date: STOP before signing, and tell the user to update",
|
|
1811
|
-
`@haven_ai/signer
|
|
1812
|
-
"MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
|
|
2052
|
+
`@haven_ai/signer: run \`${edge.connectorUpgradeCommand()}\`, then the repair line it prints,`,
|
|
2053
|
+
"which reinstalls the pinned MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
|
|
1813
2054
|
"Haven-signed binding message, so changing it invalidates the signature.",
|
|
1814
2055
|
"",
|
|
1815
2056
|
"A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
|
|
@@ -1817,7 +2058,13 @@ function signerInstructions() {
|
|
|
1817
2058
|
"fallback fields alongside the message, so you can branch on it directly. Every signer",
|
|
1818
2059
|
"refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
|
|
1819
2060
|
"hosted tool follows (resolve the role against your own server names), else",
|
|
1820
|
-
"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."
|
|
1821
2068
|
].join("\n");
|
|
1822
2069
|
}
|
|
1823
2070
|
async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
|
|
@@ -1920,8 +2167,6 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
|
|
|
1920
2167
|
}
|
|
1921
2168
|
|
|
1922
2169
|
// src/server.ts
|
|
1923
|
-
var SIGNER_NAME = "@haven_ai/signer";
|
|
1924
|
-
var SIGNER_VERSION = "0.6.0-alpha.0";
|
|
1925
2170
|
async function resolveSignerRuntime(options = {}) {
|
|
1926
2171
|
assertSupportedNodeVersion(options.nodeVersion);
|
|
1927
2172
|
if (options.delegateKey) {
|
|
@@ -1972,12 +2217,14 @@ function buildSignerMcpServer(signer, options = {}) {
|
|
|
1972
2217
|
clientIdentity: edge.havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
|
|
1973
2218
|
}
|
|
1974
2219
|
});
|
|
1975
|
-
const registerTool = server.
|
|
2220
|
+
const registerTool = server.registerTool.bind(server);
|
|
1976
2221
|
for (const name of Object.keys(toolSchemas)) {
|
|
1977
2222
|
registerTool(
|
|
1978
2223
|
name,
|
|
1979
|
-
|
|
1980
|
-
|
|
2224
|
+
{
|
|
2225
|
+
description: toolDescriptions[name],
|
|
2226
|
+
inputSchema: zod.z.object(toolSchemas[name]).passthrough()
|
|
2227
|
+
},
|
|
1981
2228
|
async (args) => toMcpResult(await handlers[name](args))
|
|
1982
2229
|
);
|
|
1983
2230
|
}
|