@haven_ai/signer 0.6.0-alpha.0 → 0.8.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 +25 -10
- package/dist/cli.cjs +287 -30
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +288 -31
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +288 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -4
- package/dist/index.d.ts +46 -4
- package/dist/index.js +289 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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, havenClientIdentity, isSupportedNodeVersion, unsupportedNodeVersionMessage, addressFromKey, HavenSigningError, buildSweepTypedData, selectStandardPaymentOption, HavenApiError, toStandardPaymentRequirements, decodeBase64Json, encodeBase64Json, x402V2PaymentEnvelope, isSettlementChildTypedData, chainIdForNetwork, assertOwnSettlementChild, isPackedUserOperationTypedData, assertUserOpTypedDataBinding, assertBoundDirectPaymentUserOp as assertBoundDirectPaymentUserOp$1, assertFundingLegPaysDelegate, HavenTypedDataRefusedError,
|
|
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, connectorUpgradeCommand, buildSweepAuthorizationMessage, verifySignature, x402AuthorizationAmount, SignerRefusalCode, SIGNER_UPDATE_FALLBACK, HavenUnsupportedSignerVersionError, buildX402ExpectedMessage, TASK_SIGN_CONTEXT_VERSION, DIRECT_SIGN_CONTEXT_VERSION, HavenError, AgentPaymentFailureCode, AgentPaymentNextAction, HavenUserOpBindingError, connectorRerunCommand, assertOwnTaskChild, assertOwnTaskBudgetCloseUserOp, assertOwnSubBudgetChild, assertOwnSubBudgetCloseUserOp, 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';
|
|
@@ -12,7 +12,14 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
12
12
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
13
13
|
|
|
14
14
|
var SIGNER_HOSTED_HANDOFF_SHAPES = {
|
|
15
|
-
haven_get_payment_status: { payment_id: z.string().min(1) }
|
|
15
|
+
haven_get_payment_status: { payment_id: z.string().min(1) },
|
|
16
|
+
// #3506: `haven_sign` hands a signed budget row to the hosted `haven_submit`
|
|
17
|
+
// (`signTaskBudget` / `signSubBudget` in tools.ts emit exactly these
|
|
18
|
+
// `next_arguments`). One hosted tool, two argument shapes, so each shape is
|
|
19
|
+
// its own entry keyed `<hosted tool>#<variant>`; the parity test reads the
|
|
20
|
+
// part before `#` as the hosted tool it checks the entry against.
|
|
21
|
+
"haven_submit#task_budget": { task_budget_id: z.string().min(1), signature: z.string().regex(/^0x[0-9a-fA-F]+$/) },
|
|
22
|
+
"haven_submit#sub_budget": { sub_budget_id: z.string().min(1), signature: z.string().regex(/^0x[0-9a-fA-F]+$/) }
|
|
16
23
|
};
|
|
17
24
|
function target(shape) {
|
|
18
25
|
const schema = z.object(shape).strict();
|
|
@@ -159,7 +166,8 @@ function stableStringify(value) {
|
|
|
159
166
|
const object = value;
|
|
160
167
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
|
|
161
168
|
}
|
|
162
|
-
var DIRECT_RELAY_FALLBACK = "call haven_sign with payload_hash and typed_data_b64 from
|
|
169
|
+
var DIRECT_RELAY_FALLBACK = "re-run whichever of haven_send / haven_pay you called, with the SAME idempotency_key (echoed on its result) plus include_signing_payload: true, then call haven_sign with payload_hash and typed_data_b64 from THAT re-run's result, passed through unchanged";
|
|
170
|
+
var DIRECT_404_FALLBACK = "either the payment_id is not from this agent\u2019s own POST /payments call, or this Haven backend predates #3271 (which also predates #3495, so its result already carries payload_hash and typed_data_b64 unconditionally): pass those fields from the ORIGINAL haven_send / haven_pay result, unchanged, if it already carries them \u2014 or, on a newer server, re-run haven_send / haven_pay with the SAME idempotency_key plus include_signing_payload: true and pass them from THAT result instead";
|
|
163
171
|
var HavenSignContextError = class extends HavenSigningError {
|
|
164
172
|
/**
|
|
165
173
|
* #3001 / #3010 review: the recovery path, when signing OTHER bytes is
|
|
@@ -212,7 +220,7 @@ var HavenSignContextError = class extends HavenSigningError {
|
|
|
212
220
|
/**
|
|
213
221
|
* #3303: on a 426 `client_outdated` refusal, the backend's update hint —
|
|
214
222
|
* this signer is below the minimum the deployment set, and
|
|
215
|
-
* `client_update.upgrade_command`
|
|
223
|
+
* `client_update.upgrade_command` starts the update (#3412: for the signer, the doctor, then its repair line).
|
|
216
224
|
*/
|
|
217
225
|
client_update;
|
|
218
226
|
constructor(message, code, refusal, paymentId, flow = "x402") {
|
|
@@ -229,7 +237,7 @@ var HavenSignContextError = class extends HavenSigningError {
|
|
|
229
237
|
step = signerRefusalStep({
|
|
230
238
|
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
231
239
|
nextTool: null,
|
|
232
|
-
nextToolOmittedReason: `this signer is below the minimum version Haven accepts \u2014 tell the user to run ${refusal.clientUpdate?.upgrade_command ??
|
|
240
|
+
nextToolOmittedReason: `this signer is below the minimum version Haven accepts \u2014 tell the user to run ${refusal.clientUpdate?.upgrade_command ?? connectorUpgradeCommand()} and the repair line it prints, restart the agent runtime, then retry the same haven_sign / haven_sign_x402 call`
|
|
233
241
|
});
|
|
234
242
|
} else if (flow === "direct" && refusal?.httpStatus === 404) {
|
|
235
243
|
this.fallback = "typed_data_b64";
|
|
@@ -237,7 +245,7 @@ var HavenSignContextError = class extends HavenSigningError {
|
|
|
237
245
|
step = signerRefusalStep({
|
|
238
246
|
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
239
247
|
nextTool: null,
|
|
240
|
-
nextToolOmittedReason:
|
|
248
|
+
nextToolOmittedReason: DIRECT_404_FALLBACK
|
|
241
249
|
});
|
|
242
250
|
} else if (flow === "direct" && refusal?.errorCode === "expired") {
|
|
243
251
|
this.next_action = AgentPaymentNextAction.PaymentWindowExpired;
|
|
@@ -392,7 +400,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
|
|
|
392
400
|
} catch (err) {
|
|
393
401
|
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
394
402
|
throw new HavenSignContextError(
|
|
395
|
-
timedOut ? `Haven did not answer the direct-payment signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from
|
|
403
|
+
timedOut ? `Haven did not answer the direct-payment signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or re-run haven_send / haven_pay with the same idempotency_key plus include_signing_payload: true and pass typed_data_b64 from that result instead.` : `Could not reach Haven to fetch the direct-payment signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or re-run haven_send / haven_pay with the same idempotency_key plus include_signing_payload: true and pass typed_data_b64 from that result instead.`,
|
|
396
404
|
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
|
|
397
405
|
void 0,
|
|
398
406
|
paymentId,
|
|
@@ -405,7 +413,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
|
|
|
405
413
|
} catch (err) {
|
|
406
414
|
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
407
415
|
throw new HavenSignContextError(
|
|
408
|
-
`Haven did not finish sending the direct-payment signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from
|
|
416
|
+
`Haven did not finish sending the direct-payment signing context for ${paymentId} within ${timeoutMs} ms. Retry, or re-run haven_send / haven_pay with the same idempotency_key plus include_signing_payload: true and pass typed_data_b64 from that result instead.`,
|
|
409
417
|
"SIGN_CONTEXT_TIMEOUT",
|
|
410
418
|
void 0,
|
|
411
419
|
paymentId,
|
|
@@ -417,7 +425,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
|
|
|
417
425
|
if (!response.ok) {
|
|
418
426
|
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
419
427
|
throw new HavenSignContextError(
|
|
420
|
-
`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
|
|
428
|
+
`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 (which also predates #3495, so its haven_send / haven_pay result already carries payload_hash and typed_data_b64 unconditionally). Pass payload_hash and typed_data_b64, unchanged, from the original result \u2014 or, on a newer server, from a same-key re-run with include_signing_payload: true." : body.error_code === "expired" ? " The payment window has expired; call haven_send / haven_pay again with the same idempotency_key." : ""),
|
|
421
429
|
"SIGN_CONTEXT_REFUSED",
|
|
422
430
|
refusalOf(response.status, body),
|
|
423
431
|
paymentId,
|
|
@@ -428,7 +436,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
|
|
|
428
436
|
const version = body.direct_sign_context_version;
|
|
429
437
|
if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || signData.signature_scheme !== "eip712_userop" || typeof version !== "number" || !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version)) {
|
|
430
438
|
throw new HavenSignContextError(
|
|
431
|
-
typeof version === "number" && !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version) ? `The Haven direct-payment sign-context response is version ${version}, which this signer does not support (supported: ${SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.` : "The Haven direct-payment sign-context response is missing sign_data.typed_data, or its signature_scheme is not 'eip712_userop' \u2014 the backend may predate #3271.
|
|
439
|
+
typeof version === "number" && !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version) ? `The Haven direct-payment sign-context response is version ${version}, which this signer does not support (supported: ${SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.` : "The Haven direct-payment sign-context response is missing sign_data.typed_data, or its signature_scheme is not 'eip712_userop' \u2014 the backend may predate #3271. Re-run haven_send / haven_pay with the same idempotency_key plus include_signing_payload: true and pass typed_data_b64 from that result instead.",
|
|
432
440
|
"SIGN_CONTEXT_MALFORMED",
|
|
433
441
|
void 0,
|
|
434
442
|
paymentId,
|
|
@@ -444,7 +452,120 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
|
|
|
444
452
|
clientUpdate: readClientUpdate(body.client_update)
|
|
445
453
|
};
|
|
446
454
|
}
|
|
447
|
-
var SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS = [
|
|
455
|
+
var SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS = [TASK_SIGN_CONTEXT_VERSION];
|
|
456
|
+
var SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS = [1];
|
|
457
|
+
async function fetchSubBudgetSignContext(identity, subBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
|
|
458
|
+
let response;
|
|
459
|
+
try {
|
|
460
|
+
response = await fetchImpl(
|
|
461
|
+
`${identity.apiUrl}/sub-budgets/${encodeURIComponent(subBudgetId)}/sign-context`,
|
|
462
|
+
{
|
|
463
|
+
headers: signContextHeaders(identity, clientIdentity),
|
|
464
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
469
|
+
throw new HavenSignContextError(
|
|
470
|
+
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)}.`,
|
|
471
|
+
timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
|
|
472
|
+
void 0,
|
|
473
|
+
subBudgetId,
|
|
474
|
+
"task"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
let body;
|
|
478
|
+
try {
|
|
479
|
+
body = await response.json();
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
482
|
+
throw new HavenSignContextError(
|
|
483
|
+
`Haven did not finish sending the sub-budget signing context for ${subBudgetId} within ${timeoutMs} ms. Retry.`,
|
|
484
|
+
"SIGN_CONTEXT_TIMEOUT",
|
|
485
|
+
void 0,
|
|
486
|
+
subBudgetId,
|
|
487
|
+
"task"
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
body = {};
|
|
491
|
+
}
|
|
492
|
+
if (!response.ok) {
|
|
493
|
+
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
494
|
+
throw new HavenSignContextError(
|
|
495
|
+
`Haven refused the sub-budget signing-context fetch for ${subBudgetId}: ${detail}`,
|
|
496
|
+
"SIGN_CONTEXT_REFUSED",
|
|
497
|
+
refusalOf(response.status, body),
|
|
498
|
+
subBudgetId,
|
|
499
|
+
"task"
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const purpose = body.purpose;
|
|
503
|
+
const typedData = body.typed_data;
|
|
504
|
+
const version = body.sub_budget_sign_context_version;
|
|
505
|
+
const expected = body.expected;
|
|
506
|
+
const malformed = (detail) => {
|
|
507
|
+
throw new HavenSignContextError(
|
|
508
|
+
`The Haven sub-budget sign-context response for ${subBudgetId} is malformed: ${detail}.`,
|
|
509
|
+
"SIGN_CONTEXT_MALFORMED",
|
|
510
|
+
void 0,
|
|
511
|
+
subBudgetId,
|
|
512
|
+
"task"
|
|
513
|
+
);
|
|
514
|
+
};
|
|
515
|
+
if (typeof version !== "number" || !SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS.includes(version)) {
|
|
516
|
+
throw new HavenSignContextError(
|
|
517
|
+
`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.`,
|
|
518
|
+
"SIGN_CONTEXT_MALFORMED",
|
|
519
|
+
void 0,
|
|
520
|
+
subBudgetId,
|
|
521
|
+
"task"
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
if (!typedData || typeof typedData !== "object") return malformed("missing typed_data");
|
|
525
|
+
if (!expected || typeof expected !== "object") return malformed("missing expected");
|
|
526
|
+
const e = expected;
|
|
527
|
+
if (purpose === "open") {
|
|
528
|
+
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") {
|
|
529
|
+
return malformed("expected is missing a required field for purpose open");
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
purpose: "open",
|
|
533
|
+
subBudgetId: String(body.sub_budget_id ?? subBudgetId),
|
|
534
|
+
typedData,
|
|
535
|
+
expected: {
|
|
536
|
+
delegateAccount: e.delegate_account,
|
|
537
|
+
chainId: e.chain_id,
|
|
538
|
+
tokenAddress: e.token_address,
|
|
539
|
+
periodAmountAtomic: e.period_amount_atomic,
|
|
540
|
+
periodDurationSeconds: e.period_duration_seconds,
|
|
541
|
+
startDate: e.start_date,
|
|
542
|
+
recipientAddress: e.recipient_address ?? null,
|
|
543
|
+
expiresAt: e.expires_at,
|
|
544
|
+
parentDelegationHash: e.parent_delegation_hash,
|
|
545
|
+
childDelegateAccount: e.child_delegate_account
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (purpose === "close") {
|
|
550
|
+
const userOpHash = body.user_op_hash;
|
|
551
|
+
if (typeof userOpHash !== "string") return malformed("missing user_op_hash for purpose close");
|
|
552
|
+
if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.delegation_hash !== "string") {
|
|
553
|
+
return malformed("expected is missing a required field for purpose close");
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
purpose: "close",
|
|
557
|
+
subBudgetId: String(body.sub_budget_id ?? subBudgetId),
|
|
558
|
+
typedData,
|
|
559
|
+
userOpHash,
|
|
560
|
+
expected: {
|
|
561
|
+
delegateAccount: e.delegate_account,
|
|
562
|
+
chainId: e.chain_id,
|
|
563
|
+
delegationHash: e.delegation_hash
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return malformed(`unknown purpose '${String(purpose)}'`);
|
|
568
|
+
}
|
|
448
569
|
async function fetchTaskBudgetSignContext(identity, taskBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
|
|
449
570
|
let response;
|
|
450
571
|
try {
|
|
@@ -556,6 +677,9 @@ async function fetchTaskBudgetSignContext(identity, taskBudgetId, fetchImpl = fe
|
|
|
556
677
|
}
|
|
557
678
|
|
|
558
679
|
// src/tools.ts
|
|
680
|
+
var SIGNER_NAME = "@haven_ai/signer";
|
|
681
|
+
var SIGNER_VERSION = "0.8.0-alpha.0";
|
|
682
|
+
var UNSUPPORTED_ARGUMENT_MARKER = "HavenSignerUnsupportedArgument:";
|
|
559
683
|
var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
|
|
560
684
|
var HavenUserOpBindingRefusedError = class extends HavenSigningError {
|
|
561
685
|
next_action = AgentPaymentNextAction.StopAndTellUser;
|
|
@@ -568,7 +692,7 @@ var HavenUserOpBindingRefusedError = class extends HavenSigningError {
|
|
|
568
692
|
this.step = signerRefusalStep({
|
|
569
693
|
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
570
694
|
nextTool: null,
|
|
571
|
-
nextToolOmittedReason: "call haven_sign again with payment_id alone (preferred), or with typed_data copied unchanged from the payment result"
|
|
695
|
+
nextToolOmittedReason: "call haven_sign again with payment_id alone (preferred), or with typed_data copied unchanged from the payment result (or, on a compact direct-payment result, from a same-key re-run with include_signing_payload: true)"
|
|
572
696
|
});
|
|
573
697
|
}
|
|
574
698
|
};
|
|
@@ -683,7 +807,13 @@ var toolSchemas = {
|
|
|
683
807
|
// Mutually exclusive with payment_id / payload_hash (checked in the
|
|
684
808
|
// handler, since Zod's object-level refine on a raw shape used by
|
|
685
809
|
// several call sites is not the shape this schema is composed as).
|
|
686
|
-
task_budget_id: z$1.string().min(1).optional()
|
|
810
|
+
task_budget_id: z$1.string().min(1).optional(),
|
|
811
|
+
// #3330: sign a sub-budget's own child delegation or its early-close
|
|
812
|
+
// UserOp — same fetch-verify-sign channel as task_budget_id, against
|
|
813
|
+
// `GET /sub-budgets/:id/sign-context`. Mutually exclusive with
|
|
814
|
+
// payment_id / payload_hash AND with task_budget_id (checked in the
|
|
815
|
+
// handler).
|
|
816
|
+
sub_budget_id: z$1.string().min(1).optional()
|
|
687
817
|
},
|
|
688
818
|
haven_x402_sign_header: {
|
|
689
819
|
// The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
|
|
@@ -964,9 +1094,76 @@ function createToolHandlers(signer, options = {}) {
|
|
|
964
1094
|
next_arguments: { task_budget_id: ctx.taskBudgetId, signature }
|
|
965
1095
|
};
|
|
966
1096
|
}
|
|
1097
|
+
async function signSubBudget(subBudgetId) {
|
|
1098
|
+
const identity = await options.signContext?.loadIdentity() ?? null;
|
|
1099
|
+
if (!identity) {
|
|
1100
|
+
throw new HavenSigningError(
|
|
1101
|
+
`sub_budget_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${connectorRerunCommand()}\` to restore it.`
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
const ctx = await fetchSubBudgetSignContext(
|
|
1105
|
+
identity,
|
|
1106
|
+
subBudgetId,
|
|
1107
|
+
options.signContext?.fetchImpl,
|
|
1108
|
+
void 0,
|
|
1109
|
+
options.signContext?.clientIdentity
|
|
1110
|
+
);
|
|
1111
|
+
let signature;
|
|
1112
|
+
if (ctx.purpose === "open") {
|
|
1113
|
+
try {
|
|
1114
|
+
assertOwnSubBudgetChild(
|
|
1115
|
+
ctx.typedData,
|
|
1116
|
+
{
|
|
1117
|
+
chainId: ctx.expected.chainId,
|
|
1118
|
+
tokenAddress: ctx.expected.tokenAddress,
|
|
1119
|
+
periodAmountAtomic: ctx.expected.periodAmountAtomic,
|
|
1120
|
+
periodDurationSeconds: ctx.expected.periodDurationSeconds,
|
|
1121
|
+
startDate: ctx.expected.startDate,
|
|
1122
|
+
recipientAddress: ctx.expected.recipientAddress,
|
|
1123
|
+
expiresAt: ctx.expected.expiresAt,
|
|
1124
|
+
parentDelegationHash: ctx.expected.parentDelegationHash,
|
|
1125
|
+
delegateAccount: ctx.expected.delegateAccount,
|
|
1126
|
+
childDelegateAccount: ctx.expected.childDelegateAccount
|
|
1127
|
+
},
|
|
1128
|
+
signer.delegateAddress
|
|
1129
|
+
);
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
if (err instanceof HavenTypedDataRefusedError) throw err;
|
|
1132
|
+
throw new HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
|
|
1133
|
+
}
|
|
1134
|
+
signature = await signer.signDelegationTypedData(ctx.typedData);
|
|
1135
|
+
} else {
|
|
1136
|
+
try {
|
|
1137
|
+
assertUserOpTypedDataBinding(ctx.typedData, ctx.userOpHash);
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
if (err instanceof HavenUserOpBindingError) throw new HavenUserOpBindingRefusedError(err.message);
|
|
1140
|
+
throw err;
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
assertOwnSubBudgetCloseUserOp(
|
|
1144
|
+
ctx.typedData,
|
|
1145
|
+
{ delegationHash: ctx.expected.delegationHash },
|
|
1146
|
+
signer.delegateAddress
|
|
1147
|
+
);
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
if (err instanceof HavenTypedDataRefusedError) throw err;
|
|
1150
|
+
throw new HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
|
|
1151
|
+
}
|
|
1152
|
+
signature = await signer.signDelegationTypedData(ctx.typedData);
|
|
1153
|
+
}
|
|
1154
|
+
await auditSigning("haven_sign", hashTypedData(ctx.typedData));
|
|
1155
|
+
return {
|
|
1156
|
+
signature,
|
|
1157
|
+
sub_budget_id: ctx.subBudgetId,
|
|
1158
|
+
purpose: ctx.purpose,
|
|
1159
|
+
next_tool_name: "haven_submit",
|
|
1160
|
+
next_tool_server_role: "hosted",
|
|
1161
|
+
next_arguments: { sub_budget_id: ctx.subBudgetId, signature }
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
967
1164
|
return {
|
|
968
1165
|
haven_sign: async (input) => runTool(async () => {
|
|
969
|
-
const args =
|
|
1166
|
+
const args = parseStrictFor("haven_sign", coerceX402Expected(input));
|
|
970
1167
|
if (args.task_budget_id) {
|
|
971
1168
|
if (args.payment_id || args.payload_hash) {
|
|
972
1169
|
throw new z$1.ZodError([
|
|
@@ -977,8 +1174,29 @@ function createToolHandlers(signer, options = {}) {
|
|
|
977
1174
|
}
|
|
978
1175
|
]);
|
|
979
1176
|
}
|
|
1177
|
+
if (args.sub_budget_id) {
|
|
1178
|
+
throw new z$1.ZodError([
|
|
1179
|
+
{
|
|
1180
|
+
code: z$1.ZodIssueCode.custom,
|
|
1181
|
+
path: ["sub_budget_id"],
|
|
1182
|
+
message: "task_budget_id is mutually exclusive with sub_budget_id \u2014 pass exactly one."
|
|
1183
|
+
}
|
|
1184
|
+
]);
|
|
1185
|
+
}
|
|
980
1186
|
return signTaskBudget(args.task_budget_id);
|
|
981
1187
|
}
|
|
1188
|
+
if (args.sub_budget_id) {
|
|
1189
|
+
if (args.payment_id || args.payload_hash) {
|
|
1190
|
+
throw new z$1.ZodError([
|
|
1191
|
+
{
|
|
1192
|
+
code: z$1.ZodIssueCode.custom,
|
|
1193
|
+
path: ["sub_budget_id"],
|
|
1194
|
+
message: "sub_budget_id is mutually exclusive with payment_id / payload_hash \u2014 pass exactly one."
|
|
1195
|
+
}
|
|
1196
|
+
]);
|
|
1197
|
+
}
|
|
1198
|
+
return signSubBudget(args.sub_budget_id);
|
|
1199
|
+
}
|
|
982
1200
|
const resolved = await resolveSignContext(args, { allowDirectFallback: true });
|
|
983
1201
|
const typedData = resolved?.ctx.typedData ?? resolveTypedData(args);
|
|
984
1202
|
const payloadHash = resolved?.ctx.payloadHash ?? args.payload_hash;
|
|
@@ -1021,7 +1239,7 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1021
1239
|
return { signature: result.signature, x402_binding: result.x402Binding, ...clientUpdateField(resolved) };
|
|
1022
1240
|
}),
|
|
1023
1241
|
haven_x402_sign_header: async (input) => runTool(async () => {
|
|
1024
|
-
const args =
|
|
1242
|
+
const args = parseStrictFor("haven_x402_sign_header", coercePaymentRequired(input));
|
|
1025
1243
|
const result = await signer.buildX402PaymentHeader(
|
|
1026
1244
|
args.payment_required,
|
|
1027
1245
|
args.x402_binding
|
|
@@ -1033,7 +1251,7 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1033
1251
|
return { payment_header: result.paymentHeader, accepted: result.accepted };
|
|
1034
1252
|
}),
|
|
1035
1253
|
haven_sign_x402: async (input) => runTool(async () => {
|
|
1036
|
-
const args =
|
|
1254
|
+
const args = parseStrictFor("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
|
|
1037
1255
|
const resolved = await resolveSignContext(args, { allowDirectFallback: false });
|
|
1038
1256
|
const fetched = resolved?.kind === "x402" ? resolved.ctx : void 0;
|
|
1039
1257
|
const payloadHash = fetched?.payloadHash ?? args.payload_hash;
|
|
@@ -1069,7 +1287,7 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1069
1287
|
};
|
|
1070
1288
|
}),
|
|
1071
1289
|
haven_sign_sweep_delegate: async (input) => runTool(async () => {
|
|
1072
|
-
const args =
|
|
1290
|
+
const args = parseStrictFor("haven_sign_sweep_delegate", input);
|
|
1073
1291
|
const result = await signer.signSweepAuthorization({
|
|
1074
1292
|
authorization: args.authorization,
|
|
1075
1293
|
expectedAuth: args.expected_auth,
|
|
@@ -1102,8 +1320,18 @@ function createToolHandlers(signer, options = {}) {
|
|
|
1102
1320
|
}
|
|
1103
1321
|
}
|
|
1104
1322
|
}
|
|
1105
|
-
function
|
|
1106
|
-
|
|
1323
|
+
function parseStrictFor(name, input) {
|
|
1324
|
+
const result = z$1.object(toolSchemas[name]).strict().safeParse(input ?? {});
|
|
1325
|
+
if (result.success) return result.data;
|
|
1326
|
+
const topLevelUnknown = result.error.issues.flatMap(
|
|
1327
|
+
(issue) => issue.code === "unrecognized_keys" && issue.path.length === 0 ? issue.keys : []
|
|
1328
|
+
);
|
|
1329
|
+
if (topLevelUnknown.length === 0) throw result.error;
|
|
1330
|
+
const unknown = [...new Set(topLevelUnknown)];
|
|
1331
|
+
const err = new Error(
|
|
1332
|
+
`${UNSUPPORTED_ARGUMENT_MARKER} ${name} does not accept ${unknown.map((k) => `"${k}"`).join(", ")} \u2014 this signer predates that argument form. Nothing was signed or fetched.`
|
|
1333
|
+
);
|
|
1334
|
+
throw err;
|
|
1107
1335
|
}
|
|
1108
1336
|
function coerceX402Expected(input) {
|
|
1109
1337
|
if (!input || typeof input !== "object") return input;
|
|
@@ -1142,6 +1370,25 @@ async function runTool(fn) {
|
|
|
1142
1370
|
}
|
|
1143
1371
|
}
|
|
1144
1372
|
function normalizeError(err) {
|
|
1373
|
+
if (err instanceof Error && err.message.startsWith(UNSUPPORTED_ARGUMENT_MARKER)) {
|
|
1374
|
+
const unknown = [...err.message.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
1375
|
+
return {
|
|
1376
|
+
success: false,
|
|
1377
|
+
code: "UNSUPPORTED_ARGUMENT",
|
|
1378
|
+
message: err.message.replace(`${UNSUPPORTED_ARGUMENT_MARKER} `, ""),
|
|
1379
|
+
unknown_arguments: unknown,
|
|
1380
|
+
signer_version: SIGNER_VERSION,
|
|
1381
|
+
fallback: connectorUpgradeCommand(),
|
|
1382
|
+
next_action: AgentPaymentNextAction.StopAndTellUser,
|
|
1383
|
+
...nextStepWireFields(
|
|
1384
|
+
signerRefusalStep({
|
|
1385
|
+
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
1386
|
+
nextTool: null,
|
|
1387
|
+
nextToolOmittedReason: `update @haven_ai/signer: run ${connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
|
|
1388
|
+
})
|
|
1389
|
+
)
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1145
1392
|
if (err instanceof z$1.ZodError) {
|
|
1146
1393
|
return {
|
|
1147
1394
|
success: false,
|
|
@@ -1163,7 +1410,9 @@ function normalizeError(err) {
|
|
|
1163
1410
|
...nextStepWireFields(signerRefusalStep({
|
|
1164
1411
|
nextAction: AgentPaymentNextAction.StopAndTellUser,
|
|
1165
1412
|
nextTool: null,
|
|
1166
|
-
|
|
1413
|
+
// #3412: the upgrade command, not "re-run the connector" — a bare re-run
|
|
1414
|
+
// stops at "Missing --setup" on an existing install.
|
|
1415
|
+
nextToolOmittedReason: `update @haven_ai/signer: run ${connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
|
|
1167
1416
|
}))
|
|
1168
1417
|
};
|
|
1169
1418
|
}
|
|
@@ -1249,7 +1498,7 @@ function normalizeError(err) {
|
|
|
1249
1498
|
|
|
1250
1499
|
// src/consent.ts
|
|
1251
1500
|
var toolSummaries = {
|
|
1252
|
-
haven_sign: "signs a Haven-prepared payment, task-budget
|
|
1501
|
+
haven_sign: "signs a Haven-prepared payment, task-budget or sub-budget open or close (a UserOp from your own account redeeming a delegation made to it, directly or via a task-budget child; a sub-budget child narrowing your budget for another agent of yours; or an x402 funding intent against a Haven-signed context)",
|
|
1253
1502
|
haven_x402_sign_header: "builds and signs the EIP-3009 merchant payment header for an x402 payment",
|
|
1254
1503
|
haven_sign_x402: "the two above in one call: funding payload + merchant header",
|
|
1255
1504
|
haven_sign_sweep_delegate: "signs a Haven-prepared gasless sweep of stranded funds back to your own account"
|
|
@@ -1680,7 +1929,7 @@ function assertSupportedBindingVersion(received, supported, context) {
|
|
|
1680
1929
|
const highest = Math.max(...supported);
|
|
1681
1930
|
const outOfDate = received > highest;
|
|
1682
1931
|
const code = context === "x402 expected context" ? SignerRefusalCode.UnsupportedExpectedContextVersion : SignerRefusalCode.UnsupportedSweepBindingVersion;
|
|
1683
|
-
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
|
|
1932
|
+
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 \`${connectorUpgradeCommand()}\`, then the repair line it prints, which reinstalls the pinned MCP runtime.` : `Unsupported ${context} version ${received}: this signer supports ${supported.join(", ")}.`;
|
|
1684
1933
|
const fallback = outOfDate ? 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.`;
|
|
1685
1934
|
throw new HavenUnsupportedSignerVersionError(
|
|
1686
1935
|
`${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.`,
|
|
@@ -1782,7 +2031,8 @@ function signerCompatibility() {
|
|
|
1782
2031
|
x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
|
|
1783
2032
|
sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS],
|
|
1784
2033
|
direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS],
|
|
1785
|
-
task_sign_context_versions: [...SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS]
|
|
2034
|
+
task_sign_context_versions: [...SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS],
|
|
2035
|
+
sub_budget_sign_context_versions: [...SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS]
|
|
1786
2036
|
};
|
|
1787
2037
|
}
|
|
1788
2038
|
function signerCapabilityAdvertisement() {
|
|
@@ -1802,12 +2052,13 @@ function signerInstructions() {
|
|
|
1802
2052
|
`- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
|
|
1803
2053
|
`- direct-payment (haven_send / haven_pay) sign-context versions supported: ${compatibility.direct_sign_context_versions.join(", ")} \u2014 pass payment_id alone to haven_sign; this signer fetches the exact bytes`,
|
|
1804
2054
|
`- task-budget sign-context versions supported: ${compatibility.task_sign_context_versions.join(", ")} \u2014 pass task_budget_id alone to haven_sign; this signer fetches the exact bytes`,
|
|
2055
|
+
`- sub-budget sign-context versions supported: ${compatibility.sub_budget_sign_context_versions.join(", ")} \u2014 pass sub_budget_id alone to haven_sign; this signer fetches the exact bytes (a sub-budget your owner issued narrows your own budget for another agent of your account)`,
|
|
1805
2056
|
"",
|
|
1806
2057
|
"Haven quote and prepare results report the expected-context version they will emit",
|
|
1807
2058
|
"(signer_compatibility.x402_expected_context_version). If that version is not in the list",
|
|
1808
2059
|
"above, this signer is out of date: STOP before signing, and tell the user to update",
|
|
1809
|
-
`@haven_ai/signer
|
|
1810
|
-
"MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
|
|
2060
|
+
`@haven_ai/signer: run \`${connectorUpgradeCommand()}\`, then the repair line it prints,`,
|
|
2061
|
+
"which reinstalls the pinned MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
|
|
1811
2062
|
"Haven-signed binding message, so changing it invalidates the signature.",
|
|
1812
2063
|
"",
|
|
1813
2064
|
"A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
|
|
@@ -1815,7 +2066,13 @@ function signerInstructions() {
|
|
|
1815
2066
|
"fallback fields alongside the message, so you can branch on it directly. Every signer",
|
|
1816
2067
|
"refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
|
|
1817
2068
|
"hosted tool follows (resolve the role against your own server names), else",
|
|
1818
|
-
"next_tool_omitted_reason saying why not."
|
|
2069
|
+
"next_tool_omitted_reason saying why not.",
|
|
2070
|
+
"",
|
|
2071
|
+
"An undeclared top-level argument is refused, not stripped: haven_sign answers",
|
|
2072
|
+
"UNSUPPORTED_ARGUMENT with unknown_arguments, signer_version and fallback (the update",
|
|
2073
|
+
"command) instead of dropping the key and answering the generic signing error \u2014 if you see",
|
|
2074
|
+
"that refusal, this signer predates the argument form you sent. Update the signer; nothing",
|
|
2075
|
+
"was signed, fetched or audited."
|
|
1819
2076
|
].join("\n");
|
|
1820
2077
|
}
|
|
1821
2078
|
async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
|
|
@@ -1918,8 +2175,6 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
|
|
|
1918
2175
|
}
|
|
1919
2176
|
|
|
1920
2177
|
// src/server.ts
|
|
1921
|
-
var SIGNER_NAME = "@haven_ai/signer";
|
|
1922
|
-
var SIGNER_VERSION = "0.6.0-alpha.0";
|
|
1923
2178
|
async function resolveSignerRuntime(options = {}) {
|
|
1924
2179
|
assertSupportedNodeVersion(options.nodeVersion);
|
|
1925
2180
|
if (options.delegateKey) {
|
|
@@ -1970,12 +2225,14 @@ function buildSignerMcpServer(signer, options = {}) {
|
|
|
1970
2225
|
clientIdentity: havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
|
|
1971
2226
|
}
|
|
1972
2227
|
});
|
|
1973
|
-
const registerTool = server.
|
|
2228
|
+
const registerTool = server.registerTool.bind(server);
|
|
1974
2229
|
for (const name of Object.keys(toolSchemas)) {
|
|
1975
2230
|
registerTool(
|
|
1976
2231
|
name,
|
|
1977
|
-
|
|
1978
|
-
|
|
2232
|
+
{
|
|
2233
|
+
description: toolDescriptions[name],
|
|
2234
|
+
inputSchema: z.object(toolSchemas[name]).passthrough()
|
|
2235
|
+
},
|
|
1979
2236
|
async (args) => toMcpResult(await handlers[name](args))
|
|
1980
2237
|
);
|
|
1981
2238
|
}
|