@haven_ai/signer 0.1.19-alpha.0 → 0.1.21-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/dist/cli.cjs +151 -29
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +152 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +151 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -3
- package/dist/index.d.ts +21 -3
- package/dist/index.js +152 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -325,8 +325,10 @@ function signerCapabilityAdvertisement() {
|
|
|
325
325
|
function signerInstructions() {
|
|
326
326
|
const compatibility = signerCompatibility();
|
|
327
327
|
return [
|
|
328
|
-
"Haven edge signer: sign-only tools bound to the local delegate key. It
|
|
329
|
-
"network
|
|
328
|
+
"Haven edge signer: sign-only tools bound to the local delegate key. It never emits the",
|
|
329
|
+
"key. Its one network capability is an authenticated READ of a signing context from",
|
|
330
|
+
"Haven by payment_id \u2014 pass payment_id to haven_sign / haven_sign_x402 (preferred for",
|
|
331
|
+
"delegation-rail x402) instead of relaying bulky typed-data payloads yourself.",
|
|
330
332
|
"",
|
|
331
333
|
"Version compatibility (check this BEFORE signing, not after):",
|
|
332
334
|
`- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
|
|
@@ -340,6 +342,55 @@ function signerInstructions() {
|
|
|
340
342
|
"Haven-signed binding message, so changing it invalidates the signature."
|
|
341
343
|
].join("\n");
|
|
342
344
|
}
|
|
345
|
+
async function loadHavenIdentity(credentialsPath) {
|
|
346
|
+
if (!credentialsPath) return null;
|
|
347
|
+
try {
|
|
348
|
+
const raw = JSON.parse(
|
|
349
|
+
await promises.readFile(path.join(path.dirname(credentialsPath), "identity.json"), "utf8")
|
|
350
|
+
);
|
|
351
|
+
const apiKey = typeof raw.api_key === "string" ? raw.api_key : void 0;
|
|
352
|
+
const apiUrl = typeof raw.api_url === "string" ? raw.api_url : void 0;
|
|
353
|
+
if (!apiKey || !apiUrl) return null;
|
|
354
|
+
return { apiKey, apiUrl: apiUrl.replace(/\/+$/, "") };
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch) {
|
|
360
|
+
let response;
|
|
361
|
+
try {
|
|
362
|
+
response = await fetchImpl(
|
|
363
|
+
`${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
|
|
364
|
+
{ headers: { Authorization: `Bearer ${identity.apiKey}` } }
|
|
365
|
+
);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
throw new sdk.HavenSigningError(
|
|
368
|
+
`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.`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const body = await response.json().catch(() => ({}));
|
|
372
|
+
if (!response.ok) {
|
|
373
|
+
const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
|
|
374
|
+
throw new sdk.HavenSigningError(
|
|
375
|
+
`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." : "")
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const signData = body.sign_data;
|
|
379
|
+
const x402Expected = body.x402_expected;
|
|
380
|
+
if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
|
|
381
|
+
throw new sdk.HavenSigningError(
|
|
382
|
+
"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."
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
paymentId: String(body.payment_id ?? paymentId),
|
|
387
|
+
payloadHash: signData.hash,
|
|
388
|
+
typedData: signData.typed_data,
|
|
389
|
+
x402Expected
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/tools.ts
|
|
343
394
|
var sweepAuthorizationSchema = v3.z.object({
|
|
344
395
|
from: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "from must be a 0x address"),
|
|
345
396
|
to: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "to must be a 0x address"),
|
|
@@ -357,7 +408,7 @@ var sweepExpectedAuthSchema = v3.z.object({
|
|
|
357
408
|
signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
|
|
358
409
|
signer: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "signer must be a 0x address")
|
|
359
410
|
});
|
|
360
|
-
var
|
|
411
|
+
var x402ExpectedShape = {
|
|
361
412
|
payment_id: v3.z.string().min(1),
|
|
362
413
|
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
|
|
363
414
|
resource_url: v3.z.string().url(),
|
|
@@ -381,7 +432,8 @@ var x402ExpectedSchema = v3.z.object({
|
|
|
381
432
|
signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
|
|
382
433
|
signer: v3.z.string().min(1)
|
|
383
434
|
})
|
|
384
|
-
}
|
|
435
|
+
};
|
|
436
|
+
var x402ExpectedSchema = v3.z.object(x402ExpectedShape);
|
|
385
437
|
var toolSchemas = {
|
|
386
438
|
haven_sign_sweep_delegate: {
|
|
387
439
|
// The authorization fields prepared by Haven's POST /sweep/prepare. Passed
|
|
@@ -392,8 +444,14 @@ var toolSchemas = {
|
|
|
392
444
|
expected_auth: sweepExpectedAuthSchema
|
|
393
445
|
},
|
|
394
446
|
haven_sign: {
|
|
447
|
+
// #1263: THE preferred x402 input — the signer fetches the exact signing
|
|
448
|
+
// payload + expected context from Haven itself (authenticated with the
|
|
449
|
+
// locally-stored agent credential), so no bulky bytes ever cross the
|
|
450
|
+
// model. Pass payment_id ALONE for a delegation-rail x402 funding intent.
|
|
451
|
+
payment_id: v3.z.string().min(1).optional(),
|
|
395
452
|
// The unsigned hash from haven_pay / haven_pay_x402_quote (payload_hash).
|
|
396
|
-
|
|
453
|
+
// Optional when payment_id is supplied (the fetch carries it).
|
|
454
|
+
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
|
|
397
455
|
// Pass x402.expected from hosted haven_pay_x402_quote when this hash funds
|
|
398
456
|
// a standard x402 merchant retry. The signer records it locally and returns
|
|
399
457
|
// an opaque x402_binding for the later header-signing step.
|
|
@@ -420,9 +478,13 @@ var toolSchemas = {
|
|
|
420
478
|
x402_binding: v3.z.string().min(1)
|
|
421
479
|
},
|
|
422
480
|
haven_sign_x402: {
|
|
481
|
+
// #1263: preferred — pass payment_id (plus payment_required) and the
|
|
482
|
+
// signer fetches the exact signing payload + expected context itself.
|
|
483
|
+
payment_id: v3.z.string().min(1).optional(),
|
|
423
484
|
// One-shot x402 signing: funding hash + merchant header in one local call.
|
|
424
|
-
|
|
425
|
-
|
|
485
|
+
// Both optional when payment_id is supplied (the fetch carries them).
|
|
486
|
+
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
|
|
487
|
+
x402_expected: x402ExpectedSchema.optional(),
|
|
426
488
|
payment_required: v3.z.record(v3.z.string(), v3.z.unknown()),
|
|
427
489
|
// #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
|
|
428
490
|
// (not z.unknown()) so MCP clients embed it as JSON rather than a string.
|
|
@@ -436,9 +498,10 @@ var SIGN_DESCRIPTION = [
|
|
|
436
498
|
"this process. Pass the payload_hash returned by haven_pay or haven_pay_x402_quote.",
|
|
437
499
|
"For x402, also pass x402_expected from haven_pay_x402_quote; the signer records it locally",
|
|
438
500
|
"and returns { signature, x402_binding }. x402_expected includes expires_at; sign before that",
|
|
439
|
-
"window closes. DELEGATION-RAIL accounts:
|
|
440
|
-
"
|
|
441
|
-
"
|
|
501
|
+
"window closes. DELEGATION-RAIL x402 accounts (#1263): pass payment_id ALONE (preferred) \u2014 this",
|
|
502
|
+
"signer fetches the exact signing payload and expected context from Haven itself, so nothing",
|
|
503
|
+
"bulky ever crosses your context. Fallback: pass typed_data_b64 through UNCHANGED (never re-type",
|
|
504
|
+
"the nested typed_data JSON); the account validates that EIP-712 payload, not payload_hash.",
|
|
442
505
|
"Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
|
|
443
506
|
"to mcp__haven-signer__haven_x402_sign_header. For plain SafeTransfer payments, just pass payload_hash and",
|
|
444
507
|
"relay the returned signature via mcp__haven__haven_submit."
|
|
@@ -458,11 +521,12 @@ var SIGN_X402_DESCRIPTION = [
|
|
|
458
521
|
"One-shot x402 signing for the fast 3-call flow: sign the funding hash AND build the EIP-3009",
|
|
459
522
|
"X-PAYMENT header in a single local call (equivalent to haven_sign followed by",
|
|
460
523
|
"haven_x402_sign_header). The delegate key never leaves this process. From the haven_pay_mcp_tool",
|
|
461
|
-
"result pass
|
|
462
|
-
"
|
|
463
|
-
"
|
|
464
|
-
"
|
|
465
|
-
"
|
|
524
|
+
"result pass payment_id and payment_required \u2014 PREFERRED (#1263): this signer fetches the exact",
|
|
525
|
+
"signing payload and expected context from Haven itself, so nothing bulky crosses your context.",
|
|
526
|
+
"Fallback for older backends: pass payload_hash, x402_expected (the nested x402.expected object \u2014",
|
|
527
|
+
"passing the whole x402 object is also accepted and unwrapped for you), and typed_data_b64",
|
|
528
|
+
"through UNCHANGED; the signer",
|
|
529
|
+
"signs the typed data instead of payload_hash and refuses the bare hash when the context commits to typed data.",
|
|
466
530
|
"Returns",
|
|
467
531
|
"{ signature, x402_binding, payment_header, accepted }; hand signature + payment_header to",
|
|
468
532
|
"mcp__haven__haven_settle_mcp_tool to fund and settle in one hosted call. The header is built now (before",
|
|
@@ -510,6 +574,15 @@ function toExpectedX402(raw) {
|
|
|
510
574
|
auth: raw.auth
|
|
511
575
|
};
|
|
512
576
|
}
|
|
577
|
+
function parseFetchedExpected(fetched) {
|
|
578
|
+
const parsed = v3.z.object(x402ExpectedShape).safeParse(fetched.x402Expected);
|
|
579
|
+
if (!parsed.success) {
|
|
580
|
+
throw new sdk.HavenSigningError(
|
|
581
|
+
`The Haven sign-context response carried a malformed x402_expected \u2014 the backend may predate #1263. Pass payload_hash + x402_expected from the quote result instead. Underlying: ${parsed.error.issues[0]?.message ?? "schema mismatch"}`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return parsed.data;
|
|
585
|
+
}
|
|
513
586
|
function resolveTypedData(args) {
|
|
514
587
|
if (!args.typed_data_b64) return args.typed_data;
|
|
515
588
|
try {
|
|
@@ -527,23 +600,51 @@ function resolveTypedData(args) {
|
|
|
527
600
|
}
|
|
528
601
|
}
|
|
529
602
|
function createToolHandlers(signer, options = {}) {
|
|
603
|
+
async function resolveSignContext(args) {
|
|
604
|
+
if (!args.payment_id) return null;
|
|
605
|
+
const identity = await options.signContext?.loadIdentity() ?? null;
|
|
606
|
+
if (!identity) {
|
|
607
|
+
throw new sdk.HavenSigningError(
|
|
608
|
+
"payment_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run `npx @haven_ai/connect@alpha` to restore it, or pass typed_data_b64 from the quote result instead."
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
const ctx = await fetchX402SignContext(
|
|
612
|
+
identity,
|
|
613
|
+
args.payment_id,
|
|
614
|
+
options.signContext?.fetchImpl
|
|
615
|
+
);
|
|
616
|
+
if (args.payload_hash && args.payload_hash.toLowerCase() !== ctx.payloadHash.toLowerCase()) {
|
|
617
|
+
throw new sdk.HavenSigningError(
|
|
618
|
+
`The supplied payload_hash does not match the signing context Haven serves for payment ${args.payment_id}. Pass payment_id alone, or check which quote the hash came from.`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return ctx;
|
|
622
|
+
}
|
|
530
623
|
return {
|
|
531
624
|
haven_sign: async (input) => runTool(async () => {
|
|
532
625
|
const args = parse("haven_sign", coerceX402Expected(input));
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
const
|
|
626
|
+
const fetched = await resolveSignContext(args);
|
|
627
|
+
const typedData = fetched?.typedData ?? resolveTypedData(args);
|
|
628
|
+
const payloadHash = fetched?.payloadHash ?? args.payload_hash;
|
|
629
|
+
if (!payloadHash) {
|
|
630
|
+
throw new sdk.HavenSigningError(
|
|
631
|
+
"Pass payment_id (preferred for delegation-rail x402) or payload_hash."
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
const expectedRaw = fetched ? parseFetchedExpected(fetched) : args.x402_expected;
|
|
635
|
+
const x402Expected = expectedRaw ? toExpectedX402(expectedRaw) : null;
|
|
636
|
+
const result = x402Expected ? await signFundingLeg(signer, x402Expected, payloadHash, typedData) : null;
|
|
536
637
|
if (!result) {
|
|
537
638
|
if (typedData) {
|
|
538
639
|
const signature2 = await signer.signDelegationTypedData(typedData);
|
|
539
|
-
await auditSigning("haven_sign",
|
|
640
|
+
await auditSigning("haven_sign", payloadHash);
|
|
540
641
|
return { signature: signature2 };
|
|
541
642
|
}
|
|
542
|
-
const signature = signer.signPaymentHash(
|
|
543
|
-
await auditSigning("haven_sign",
|
|
643
|
+
const signature = signer.signPaymentHash(payloadHash);
|
|
644
|
+
await auditSigning("haven_sign", payloadHash);
|
|
544
645
|
return { signature };
|
|
545
646
|
}
|
|
546
|
-
await auditSigning("haven_sign",
|
|
647
|
+
await auditSigning("haven_sign", payloadHash);
|
|
547
648
|
return { signature: result.signature, x402_binding: result.x402Binding };
|
|
548
649
|
}),
|
|
549
650
|
haven_x402_sign_header: async (input) => runTool(async () => {
|
|
@@ -560,17 +661,25 @@ function createToolHandlers(signer, options = {}) {
|
|
|
560
661
|
}),
|
|
561
662
|
haven_sign_x402: async (input) => runTool(async () => {
|
|
562
663
|
const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
|
|
664
|
+
const fetched = await resolveSignContext(args);
|
|
665
|
+
const payloadHash = fetched?.payloadHash ?? args.payload_hash;
|
|
666
|
+
const expectedRaw = fetched ? parseFetchedExpected(fetched) : args.x402_expected;
|
|
667
|
+
if (!payloadHash || !expectedRaw) {
|
|
668
|
+
throw new sdk.HavenSigningError(
|
|
669
|
+
"Pass payment_id (preferred \u2014 the signer fetches the signing context itself) or payload_hash + x402_expected from the quote result."
|
|
670
|
+
);
|
|
671
|
+
}
|
|
563
672
|
const funding = await signFundingLeg(
|
|
564
673
|
signer,
|
|
565
|
-
toExpectedX402(
|
|
566
|
-
|
|
567
|
-
resolveTypedData(args)
|
|
674
|
+
toExpectedX402(expectedRaw),
|
|
675
|
+
payloadHash,
|
|
676
|
+
fetched?.typedData ?? resolveTypedData(args)
|
|
568
677
|
);
|
|
569
678
|
const header = await signer.buildX402PaymentHeader(
|
|
570
679
|
args.payment_required,
|
|
571
680
|
funding.x402Binding
|
|
572
681
|
);
|
|
573
|
-
await auditSigning("haven_sign_x402",
|
|
682
|
+
await auditSigning("haven_sign_x402", payloadHash);
|
|
574
683
|
await auditSigning("haven_sign_x402", hashPayloadForAudit(args.payment_required));
|
|
575
684
|
return {
|
|
576
685
|
signature: funding.signature,
|
|
@@ -683,6 +792,7 @@ function normalizeError(err) {
|
|
|
683
792
|
|
|
684
793
|
// src/consent.ts
|
|
685
794
|
var SIGNER_ACK_ENV = "HAVEN_SIGNER_ACK";
|
|
795
|
+
var SIGNER_CONSENT_SURFACE_VERSION = 2;
|
|
686
796
|
function computeSignerConsentHash(input) {
|
|
687
797
|
const identity = [
|
|
688
798
|
input.delegateAddress.toLowerCase(),
|
|
@@ -693,7 +803,8 @@ function computeSignerConsentHash(input) {
|
|
|
693
803
|
].join("|");
|
|
694
804
|
const toolCanonical = [...input.toolNames].sort().join(",");
|
|
695
805
|
return crypto.createHash("sha256").update(`${identity}
|
|
696
|
-
${toolCanonical}
|
|
806
|
+
${toolCanonical}
|
|
807
|
+
surface:v${SIGNER_CONSENT_SURFACE_VERSION}`).digest("hex").slice(0, 16);
|
|
697
808
|
}
|
|
698
809
|
function renderSignerConsentBlock(input, hash) {
|
|
699
810
|
const lines = [
|
|
@@ -711,7 +822,10 @@ function renderSignerConsentBlock(input, hash) {
|
|
|
711
822
|
lines.push("");
|
|
712
823
|
lines.push("This local signer holds the delegate key on this machine and signs");
|
|
713
824
|
lines.push("payment payloads or x402 merchant headers for the delegate address above.");
|
|
714
|
-
lines.push("
|
|
825
|
+
lines.push("Its one network use is a read-only fetch of a pending payment's signing");
|
|
826
|
+
lines.push("payload from Haven, authenticated with the agent credential stored next to");
|
|
827
|
+
lines.push("this signer's key file - it never sends the key, a signature, or anything");
|
|
828
|
+
lines.push("else outbound, and it cannot show a live allowance summary.");
|
|
715
829
|
lines.push("On-chain Safe rules remain the real spend gate, and the wallet owner can");
|
|
716
830
|
lines.push("pause or revoke agent authority outside this signer.");
|
|
717
831
|
lines.push("");
|
|
@@ -884,7 +998,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
|
|
|
884
998
|
|
|
885
999
|
// src/server.ts
|
|
886
1000
|
var SIGNER_NAME = "@haven_ai/signer";
|
|
887
|
-
var SIGNER_VERSION = "0.1.
|
|
1001
|
+
var SIGNER_VERSION = "0.1.21-alpha.0";
|
|
888
1002
|
async function resolveSignerRuntime(options = {}) {
|
|
889
1003
|
assertSupportedNodeVersion(options.nodeVersion);
|
|
890
1004
|
if (options.delegateKey) {
|
|
@@ -917,6 +1031,14 @@ function buildSignerMcpServer(signer, options = {}) {
|
|
|
917
1031
|
delegateAddress: signer.delegateAddress,
|
|
918
1032
|
safeAddress: options.credentials?.safeAddress,
|
|
919
1033
|
chainId: options.credentials?.chainId
|
|
1034
|
+
},
|
|
1035
|
+
// #1263: the payment_id signing path — the ONLY network call this server
|
|
1036
|
+
// can make, an authenticated read of a signing context from Haven, using
|
|
1037
|
+
// the agent identity the connector stores next to the signer credential.
|
|
1038
|
+
// The signer CORE stays network-free; fetched bytes still pass the same
|
|
1039
|
+
// binding verification + digest re-derivation as tool-argument bytes.
|
|
1040
|
+
signContext: {
|
|
1041
|
+
loadIdentity: () => loadHavenIdentity(credentialsPath)
|
|
920
1042
|
}
|
|
921
1043
|
});
|
|
922
1044
|
const registerTool = server.tool.bind(server);
|