@haven_ai/signer 0.1.17-alpha.0 → 0.1.18-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 +98 -14
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +99 -15
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +101 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -2
- package/dist/index.d.ts +60 -2
- package/dist/index.js +100 -16
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/cli.cjs
CHANGED
|
@@ -56,8 +56,9 @@ var sweepAuthorizationSchema = v3.z.object({
|
|
|
56
56
|
token: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "token must be a 0x address"),
|
|
57
57
|
chainId: v3.z.number().int().positive()
|
|
58
58
|
});
|
|
59
|
+
var bindingVersionSchema = v3.z.number().int().positive();
|
|
59
60
|
var sweepExpectedAuthSchema = v3.z.object({
|
|
60
|
-
version:
|
|
61
|
+
version: bindingVersionSchema,
|
|
61
62
|
message: v3.z.string().min(1),
|
|
62
63
|
signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
|
|
63
64
|
signer: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "signer must be a 0x address")
|
|
@@ -75,8 +76,13 @@ var x402ExpectedSchema = v3.z.object({
|
|
|
75
76
|
// Omitting it used to fail downstream with a cryptic "authentication message is
|
|
76
77
|
// invalid" — making it required surfaces a clear INVALID_INPUT at the boundary.
|
|
77
78
|
expires_at: v3.z.string().min(1),
|
|
79
|
+
// #1138: present on a delegation-rail (v2) context. It commits to the EIP-712
|
|
80
|
+
// typed data the account validates; the signer refuses to raw-sign the bare
|
|
81
|
+
// hash when it is present, and refuses to sign typed data when it is absent.
|
|
82
|
+
typed_data_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "typed_data_hash must be a 0x-prefixed hex string").optional(),
|
|
78
83
|
auth: v3.z.object({
|
|
79
|
-
|
|
84
|
+
// Open at the boundary, enforced in the signer — see bindingVersionSchema.
|
|
85
|
+
version: bindingVersionSchema,
|
|
80
86
|
message: v3.z.string().min(1),
|
|
81
87
|
signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
|
|
82
88
|
signer: v3.z.string().min(1)
|
|
@@ -97,7 +103,10 @@ var toolSchemas = {
|
|
|
97
103
|
// Pass x402.expected from hosted haven_pay_x402_quote when this hash funds
|
|
98
104
|
// a standard x402 merchant retry. The signer records it locally and returns
|
|
99
105
|
// an opaque x402_binding for the later header-signing step.
|
|
100
|
-
x402_expected: x402ExpectedSchema.optional()
|
|
106
|
+
x402_expected: x402ExpectedSchema.optional(),
|
|
107
|
+
// #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
|
|
108
|
+
// (not z.unknown()) so MCP clients embed it as JSON rather than a string.
|
|
109
|
+
typed_data: v3.z.record(v3.z.string(), v3.z.unknown()).optional()
|
|
101
110
|
},
|
|
102
111
|
haven_x402_sign_header: {
|
|
103
112
|
// The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
|
|
@@ -111,7 +120,10 @@ var toolSchemas = {
|
|
|
111
120
|
// One-shot x402 signing: funding hash + merchant header in one local call.
|
|
112
121
|
payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
|
|
113
122
|
x402_expected: x402ExpectedSchema,
|
|
114
|
-
payment_required: v3.z.record(v3.z.string(), v3.z.unknown())
|
|
123
|
+
payment_required: v3.z.record(v3.z.string(), v3.z.unknown()),
|
|
124
|
+
// #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
|
|
125
|
+
// (not z.unknown()) so MCP clients embed it as JSON rather than a string.
|
|
126
|
+
typed_data: v3.z.record(v3.z.string(), v3.z.unknown()).optional()
|
|
115
127
|
}
|
|
116
128
|
};
|
|
117
129
|
var SIGN_DESCRIPTION = [
|
|
@@ -119,7 +131,9 @@ var SIGN_DESCRIPTION = [
|
|
|
119
131
|
"this process. Pass the payload_hash returned by haven_pay or haven_pay_x402_quote.",
|
|
120
132
|
"For x402, also pass x402_expected from haven_pay_x402_quote; the signer records it locally",
|
|
121
133
|
"and returns { signature, x402_binding }. x402_expected includes expires_at; sign before that",
|
|
122
|
-
"window closes.
|
|
134
|
+
"window closes. DELEGATION-RAIL accounts: when the hosted result carries typed_data, pass it too \u2014",
|
|
135
|
+
"the account validates that EIP-712 payload, not payload_hash, and signing is refused without it.",
|
|
136
|
+
"Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
|
|
123
137
|
"to mcp__haven-signer__haven_x402_sign_header. For plain SafeTransfer payments, just pass payload_hash and",
|
|
124
138
|
"relay the returned signature via mcp__haven__haven_submit."
|
|
125
139
|
].join(" ");
|
|
@@ -139,7 +153,10 @@ var SIGN_X402_DESCRIPTION = [
|
|
|
139
153
|
"X-PAYMENT header in a single local call (equivalent to haven_sign followed by",
|
|
140
154
|
"haven_x402_sign_header). The delegate key never leaves this process. From the haven_pay_mcp_tool",
|
|
141
155
|
"result pass payload_hash, x402_expected (the nested x402.expected object \u2014 passing the whole x402",
|
|
142
|
-
"object is also accepted and unwrapped for you), and payment_required (verbatim).
|
|
156
|
+
"object is also accepted and unwrapped for you), and payment_required (verbatim). On a",
|
|
157
|
+
"delegation-rail account the result also carries typed_data \u2014 pass that verbatim too; the signer",
|
|
158
|
+
"signs it instead of payload_hash and refuses the bare hash when the context commits to typed data.",
|
|
159
|
+
"Returns",
|
|
143
160
|
"{ signature, x402_binding, payment_header, accepted }; hand signature + payment_header to",
|
|
144
161
|
"mcp__haven__haven_settle_mcp_tool to fund and settle in one hosted call. The header is built now (before",
|
|
145
162
|
"funding confirms), so its short validity window starts here \u2014 call mcp__haven__haven_settle_mcp_tool promptly,",
|
|
@@ -161,10 +178,22 @@ var toolDescriptions = {
|
|
|
161
178
|
haven_sign_x402: SIGN_X402_DESCRIPTION,
|
|
162
179
|
haven_sign_sweep_delegate: SIGN_SWEEP_DELEGATE_DESCRIPTION
|
|
163
180
|
};
|
|
181
|
+
async function signFundingLeg(signer, expected, payloadHash, typedData) {
|
|
182
|
+
if (!expected.typedDataHash) {
|
|
183
|
+
return signer.signX402FundingHash(payloadHash, expected);
|
|
184
|
+
}
|
|
185
|
+
if (!typedData) {
|
|
186
|
+
throw new sdk.HavenSigningError(
|
|
187
|
+
"This x402 funding intent is on the delegation rail: it commits to EIP-712 typed data, which was not supplied. Pass sign_data.typed_data from haven_pay_mcp_tool / haven_pay_x402_quote verbatim \u2014 the bare payload_hash is not what the account validates."
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return signer.signX402FundingTypedData(typedData, expected);
|
|
191
|
+
}
|
|
164
192
|
function toExpectedX402(raw) {
|
|
165
193
|
return {
|
|
166
194
|
paymentId: raw.payment_id,
|
|
167
195
|
payloadHash: raw.payload_hash,
|
|
196
|
+
typedDataHash: raw.typed_data_hash,
|
|
168
197
|
resourceUrl: raw.resource_url,
|
|
169
198
|
merchantTo: raw.merchant_to,
|
|
170
199
|
amount: raw.amount,
|
|
@@ -179,7 +208,7 @@ function createToolHandlers(signer, options = {}) {
|
|
|
179
208
|
haven_sign: async (input) => runTool(async () => {
|
|
180
209
|
const args = parse("haven_sign", coerceX402Expected(input));
|
|
181
210
|
const x402Expected = args.x402_expected ? toExpectedX402(args.x402_expected) : null;
|
|
182
|
-
const result = x402Expected ? signer
|
|
211
|
+
const result = x402Expected ? await signFundingLeg(signer, x402Expected, args.payload_hash, args.typed_data) : null;
|
|
183
212
|
if (!result) {
|
|
184
213
|
const signature = signer.signPaymentHash(args.payload_hash);
|
|
185
214
|
await auditSigning("haven_sign", args.payload_hash);
|
|
@@ -202,7 +231,12 @@ function createToolHandlers(signer, options = {}) {
|
|
|
202
231
|
}),
|
|
203
232
|
haven_sign_x402: async (input) => runTool(async () => {
|
|
204
233
|
const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
|
|
205
|
-
const funding =
|
|
234
|
+
const funding = await signFundingLeg(
|
|
235
|
+
signer,
|
|
236
|
+
toExpectedX402(args.x402_expected),
|
|
237
|
+
args.payload_hash,
|
|
238
|
+
args.typed_data
|
|
239
|
+
);
|
|
206
240
|
const header = await signer.buildX402PaymentHeader(
|
|
207
241
|
args.payment_required,
|
|
208
242
|
funding.x402Binding
|
|
@@ -456,12 +490,28 @@ function createEdgeSigner(delegateKey, options = {}) {
|
|
|
456
490
|
return signAndVerify(hash);
|
|
457
491
|
},
|
|
458
492
|
signX402FundingHash(hash, expected) {
|
|
459
|
-
assertExpectedBinding(hash, expected, options.x402BindingSigner);
|
|
493
|
+
assertExpectedBinding(hash, expected, options.x402BindingSigner, "hash");
|
|
460
494
|
const signature = signAndVerify(hash);
|
|
461
495
|
const x402Binding = crypto.randomUUID();
|
|
462
496
|
x402Bindings.set(x402Binding, { ...expected });
|
|
463
497
|
return { signature, x402Binding };
|
|
464
498
|
},
|
|
499
|
+
async signX402FundingTypedData(typedData, expected) {
|
|
500
|
+
assertExpectedBinding(expected.payloadHash, expected, options.x402BindingSigner, "typed-data");
|
|
501
|
+
const digest = viem.hashTypedData(typedData);
|
|
502
|
+
if (digest.toLowerCase() !== expected.typedDataHash?.toLowerCase()) {
|
|
503
|
+
throw new sdk.HavenSigningError(
|
|
504
|
+
"x402 typed data does not match the digest Haven committed to in the expected context. Refusing to sign \u2014 the payload was altered in transit or Haven declared a different one."
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const account = accounts.privateKeyToAccount(delegateKey);
|
|
508
|
+
const signature = await account.signTypedData(
|
|
509
|
+
typedData
|
|
510
|
+
);
|
|
511
|
+
const x402Binding = crypto.randomUUID();
|
|
512
|
+
x402Bindings.set(x402Binding, { ...expected });
|
|
513
|
+
return { signature, x402Binding };
|
|
514
|
+
},
|
|
465
515
|
async buildX402PaymentHeader(paymentRequired, x402Binding) {
|
|
466
516
|
const expected = x402Bindings.get(x402Binding);
|
|
467
517
|
if (!expected) {
|
|
@@ -558,8 +608,13 @@ function assertSweepBinding(authorization, expectedAuth, trustedSigner) {
|
|
|
558
608
|
"Sweep binding verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing sweep authorizations."
|
|
559
609
|
);
|
|
560
610
|
}
|
|
611
|
+
assertSupportedBindingVersion(
|
|
612
|
+
expectedAuth.version,
|
|
613
|
+
SUPPORTED_SWEEP_BINDING_VERSIONS,
|
|
614
|
+
"sweep authorization binding"
|
|
615
|
+
);
|
|
561
616
|
const message = sdk.buildSweepAuthorizationMessage(authorization);
|
|
562
|
-
if (expectedAuth.
|
|
617
|
+
if (expectedAuth.message !== message) {
|
|
563
618
|
throw new sdk.HavenSigningError("Sweep authorization binding does not match the authorization being signed.");
|
|
564
619
|
}
|
|
565
620
|
if (!sameAddress(expectedAuth.signer, trustedSigner)) {
|
|
@@ -593,16 +648,43 @@ function assertExpectedShape(expected) {
|
|
|
593
648
|
throw new sdk.HavenSigningError("x402 expected funding context is required before signing a merchant header.");
|
|
594
649
|
}
|
|
595
650
|
}
|
|
596
|
-
|
|
651
|
+
var SUPPORTED_X402_EXPECTED_VERSIONS = [1, 2];
|
|
652
|
+
var SUPPORTED_SWEEP_BINDING_VERSIONS = [1];
|
|
653
|
+
function assertSupportedBindingVersion(received, supported, context) {
|
|
654
|
+
if (supported.includes(received)) return;
|
|
655
|
+
const highest = Math.max(...supported);
|
|
656
|
+
const ceiling = received > highest ? `This signer is out of date: it supports ${context} versions up to ${highest}, and Haven sent version ${received}. Update @haven_ai/signer \u2014 rerun the Haven connector (\`npx @haven_ai/connect@alpha\`), which reinstalls the pinned MCP runtime.` : `Unsupported ${context} version ${received}: this signer supports ${supported.join(", ")}.`;
|
|
657
|
+
throw new sdk.HavenSigningError(
|
|
658
|
+
`${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 authorised.`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
function assertExpectedBinding(payloadHash, expected, trustedSigner, mode = "hash") {
|
|
597
662
|
assertExpectedShape(expected);
|
|
598
663
|
if (!trustedSigner) {
|
|
599
664
|
throw new sdk.HavenSigningError(
|
|
600
665
|
"x402 expected-context verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing x402 funding hashes."
|
|
601
666
|
);
|
|
602
667
|
}
|
|
668
|
+
if (expected.auth) {
|
|
669
|
+
assertSupportedBindingVersion(
|
|
670
|
+
expected.auth.version,
|
|
671
|
+
SUPPORTED_X402_EXPECTED_VERSIONS,
|
|
672
|
+
"x402 expected context"
|
|
673
|
+
);
|
|
674
|
+
}
|
|
603
675
|
if (expected.payloadHash.toLowerCase() !== payloadHash.toLowerCase()) {
|
|
604
676
|
throw new sdk.HavenSigningError("x402 expected context does not match the funding hash being signed.");
|
|
605
677
|
}
|
|
678
|
+
if (mode === "hash" && expected.typedDataHash) {
|
|
679
|
+
throw new sdk.HavenSigningError(
|
|
680
|
+
"This x402 funding intent commits to EIP-712 typed data, so its bare hash must not be raw-signed \u2014 the account would reject that signature on-chain. Sign sign_data.typed_data instead."
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
if (mode === "typed-data" && !expected.typedDataHash) {
|
|
684
|
+
throw new sdk.HavenSigningError(
|
|
685
|
+
"Refusing to sign typed data under an expected context that does not commit to it. Haven must return a v2 x402 expected context (with typedDataHash) for a delegation-rail intent."
|
|
686
|
+
);
|
|
687
|
+
}
|
|
606
688
|
const message = sdk.buildX402ExpectedMessage({
|
|
607
689
|
paymentId: expected.paymentId,
|
|
608
690
|
payloadHash: expected.payloadHash,
|
|
@@ -611,9 +693,11 @@ function assertExpectedBinding(payloadHash, expected, trustedSigner) {
|
|
|
611
693
|
amount: expected.amount,
|
|
612
694
|
asset: expected.asset,
|
|
613
695
|
network: expected.network,
|
|
614
|
-
expiresAt: expected.expiresAt
|
|
696
|
+
expiresAt: expected.expiresAt,
|
|
697
|
+
typedDataHash: expected.typedDataHash
|
|
615
698
|
});
|
|
616
|
-
|
|
699
|
+
const expectedVersion = expected.typedDataHash ? 2 : 1;
|
|
700
|
+
if (expected.auth?.version !== expectedVersion || expected.auth.message !== message) {
|
|
617
701
|
throw new sdk.HavenSigningError("x402 expected context authentication message is invalid.");
|
|
618
702
|
}
|
|
619
703
|
if (!sameAddress(expected.auth.signer, trustedSigner)) {
|
|
@@ -731,7 +815,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
|
|
|
731
815
|
|
|
732
816
|
// src/server.ts
|
|
733
817
|
var SIGNER_NAME = "@haven_ai/signer";
|
|
734
|
-
var SIGNER_VERSION = "0.1.
|
|
818
|
+
var SIGNER_VERSION = "0.1.18-alpha.0";
|
|
735
819
|
async function resolveSignerRuntime(options = {}) {
|
|
736
820
|
if (options.delegateKey) {
|
|
737
821
|
return {
|