@haven_ai/signer 0.0.0-dev.202609031523.fd49e1a

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 ADDED
@@ -0,0 +1,1397 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
5
+ var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
6
+ var crypto = require('crypto');
7
+ var promises = require('fs/promises');
8
+ var os = require('os');
9
+ var path = require('path');
10
+ var sdk = require('@haven_ai/sdk');
11
+ var viem = require('viem');
12
+ var accounts = require('viem/accounts');
13
+ var schemes = require('x402/schemes');
14
+ var v3 = require('zod/v3');
15
+
16
+ function defaultSigningAuditPath(credentialsPath) {
17
+ if (credentialsPath) return path.resolve(`${credentialsPath}.signer-audit.jsonl`);
18
+ return path.resolve(os.homedir(), ".haven", "signer-audit.jsonl");
19
+ }
20
+ async function appendSigningAuditEntry(entry, path$1) {
21
+ await promises.mkdir(path.dirname(path$1), { recursive: true });
22
+ await promises.appendFile(path$1, `${JSON.stringify(entry)}
23
+ `, "utf8");
24
+ }
25
+ function createSigningAuditEntry(tool, payloadHash, context, now = /* @__PURE__ */ new Date()) {
26
+ const entry = {
27
+ version: 1,
28
+ timestamp: now.toISOString(),
29
+ tool,
30
+ payload_hash: payloadHash,
31
+ delegate_address: context.delegateAddress
32
+ };
33
+ if (context.safeAddress) entry.safe_address = context.safeAddress;
34
+ if (typeof context.chainId === "number") entry.chain_id = context.chainId;
35
+ return entry;
36
+ }
37
+ function hashPayloadForAudit(payload) {
38
+ return `0x${crypto.createHash("sha256").update(stableStringify(payload)).digest("hex")}`;
39
+ }
40
+ function stableStringify(value) {
41
+ if (value === null || typeof value !== "object") {
42
+ const primitive = JSON.stringify(value);
43
+ return primitive === void 0 ? "undefined" : primitive;
44
+ }
45
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
46
+ const object = value;
47
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
48
+ }
49
+ var DELEGATION_MANAGER = "0xdb9B1e94B5b69Df7e401DDbedE43491141047dB3";
50
+ var CAVEAT_ENFORCERS = {
51
+ erc20TransferAmount: "0xf100b0819427117EcF76Ed94B358B1A5b5C6D2Fc",
52
+ allowedCalldata: "0xc2b0d624c1c4319760C96503BA27C347F3260f55",
53
+ timestamp: "0x1046bb45C8d673d4ea75321280DB34899413c069"};
54
+ var MAX_SETTLEMENT_WINDOW_SECONDS = 600;
55
+ function isSettlementChildTypedData(value) {
56
+ const td = value;
57
+ return !!td && td.primaryType === "Delegation" && typeof td.domain?.verifyingContract === "string" && Array.isArray(td.message?.caveats);
58
+ }
59
+ function same(a, b) {
60
+ return !!a && !!b && a.toLowerCase() === b.toLowerCase();
61
+ }
62
+ function hex(value) {
63
+ return value.startsWith("0x") ? value.slice(2).toLowerCase() : value.toLowerCase();
64
+ }
65
+ function slice(terms, start, end) {
66
+ const body = hex(terms);
67
+ return end === void 0 ? body.slice(start * 2) : body.slice(start * 2, end * 2);
68
+ }
69
+ function findCaveat(caveats, enforcer) {
70
+ return caveats.find((c) => same(c.enforcer, enforcer));
71
+ }
72
+ function refuse(what, detail) {
73
+ throw new sdk.HavenSigningError(
74
+ `Refusing to sign the x402 settlement child: ${what}. ${detail} The signature on this child is what lets a merchant pull from the treasury, so it is verified locally against the Haven-signed expected context rather than trusted.`
75
+ );
76
+ }
77
+ function chainIdForNetwork(network) {
78
+ if (!network) return void 0;
79
+ const caip = /^eip155:(\d+)$/.exec(network);
80
+ if (caip) return Number(caip[1]);
81
+ if (network === "base") return 8453;
82
+ if (network === "base-sepolia") return 84532;
83
+ return void 0;
84
+ }
85
+ function verifySettlementChild(typedData, expected, now = Date.now()) {
86
+ if (typedData.primaryType !== "Delegation") {
87
+ refuse("it is not a Delegation payload", `primaryType was '${typedData.primaryType}'.`);
88
+ }
89
+ if (!same(typedData.domain?.verifyingContract, DELEGATION_MANAGER)) {
90
+ refuse(
91
+ "the EIP-712 domain names an unknown DelegationManager",
92
+ `Expected ${DELEGATION_MANAGER}, got ${typedData.domain?.verifyingContract}.`
93
+ );
94
+ }
95
+ const domainChain = Number(typedData.domain?.chainId);
96
+ if (!Number.isFinite(domainChain) || domainChain !== expected.chainId) {
97
+ refuse(
98
+ "it is scoped to the wrong chain",
99
+ `The expected context says chain ${expected.chainId}; the child says ${typedData.domain?.chainId}.`
100
+ );
101
+ }
102
+ const caveats = typedData.message?.caveats ?? [];
103
+ if (caveats.length === 0) {
104
+ refuse("it carries no caveats at all", "An unconstrained delegation is not a payment.");
105
+ }
106
+ const transfer = findCaveat(caveats, CAVEAT_ENFORCERS.erc20TransferAmount);
107
+ if (!transfer) {
108
+ refuse(
109
+ "it has no ERC-20 transfer-amount caveat",
110
+ "Without it the delegation is not bounded to an amount."
111
+ );
112
+ }
113
+ const token = `0x${slice(transfer.terms, 0, 20)}`;
114
+ if (!same(token, expected.asset)) {
115
+ refuse("it spends a different token", `Expected ${expected.asset}, child pins ${token}.`);
116
+ }
117
+ const amount = BigInt(`0x${slice(transfer.terms, 20, 52)}`);
118
+ if (amount !== BigInt(expected.amount)) {
119
+ refuse(
120
+ "the amount does not match",
121
+ `The expected context says ${expected.amount}; the child allows ${amount.toString()}.`
122
+ );
123
+ }
124
+ const calldata = findCaveat(caveats, CAVEAT_ENFORCERS.allowedCalldata);
125
+ if (!calldata) {
126
+ refuse(
127
+ "it has no payee pin",
128
+ "Without an allowed-calldata caveat the merchant could redeem it to any address."
129
+ );
130
+ }
131
+ const startIndex = BigInt(`0x${slice(calldata.terms, 0, 32)}`);
132
+ if (startIndex !== 4n) {
133
+ refuse(
134
+ "the payee pin points at the wrong calldata offset",
135
+ `Expected offset 4 (the transfer's \`to\` word), got ${startIndex.toString()}.`
136
+ );
137
+ }
138
+ const paddedPayee = slice(calldata.terms, 32, 64);
139
+ if (paddedPayee.slice(0, 24) !== "0".repeat(24)) {
140
+ refuse("the pinned payee is not a plain address", "Its 32-byte word is not a padded address.");
141
+ }
142
+ const payee = `0x${paddedPayee.slice(24)}`;
143
+ if (!same(payee, expected.merchantTo)) {
144
+ refuse(
145
+ "it pays a different address than Haven declared",
146
+ `The expected context says ${expected.merchantTo}; the child pins ${payee}.`
147
+ );
148
+ }
149
+ const timestamp = findCaveat(caveats, CAVEAT_ENFORCERS.timestamp);
150
+ if (!timestamp) {
151
+ refuse("it never expires", "A settlement child without a timestamp caveat is open-ended.");
152
+ }
153
+ const beforeThreshold = Number(BigInt(`0x${slice(timestamp.terms, 16, 32)}`));
154
+ if (beforeThreshold <= 0) {
155
+ refuse("its expiry is unset", "The timestamp caveat has no upper bound.");
156
+ }
157
+ const nowSec = Math.floor(now / 1e3);
158
+ if (beforeThreshold <= nowSec) {
159
+ refuse("it has already expired", `Expiry ${beforeThreshold} is not in the future.`);
160
+ }
161
+ if (beforeThreshold > nowSec + MAX_SETTLEMENT_WINDOW_SECONDS) {
162
+ refuse(
163
+ "its window is longer than a settlement may live",
164
+ `Expiry is ${beforeThreshold - nowSec}s out; the ceiling is ${MAX_SETTLEMENT_WINDOW_SECONDS}s.`
165
+ );
166
+ }
167
+ if (expected.expiresAt) {
168
+ const declared = Math.floor(Date.parse(expected.expiresAt) / 1e3);
169
+ if (Number.isFinite(declared) && beforeThreshold > declared) {
170
+ refuse(
171
+ "it outlives the payment window Haven declared",
172
+ `The expected context expires at ${expected.expiresAt}; the child lives to ${beforeThreshold}.`
173
+ );
174
+ }
175
+ }
176
+ }
177
+ function createEdgeSigner(delegateKey, options = {}) {
178
+ let delegateAddress;
179
+ try {
180
+ delegateAddress = sdk.addressFromKey(delegateKey);
181
+ } catch (err) {
182
+ throw new sdk.HavenSigningError(
183
+ `Invalid delegate key: ${err instanceof Error ? err.message : String(err)}`
184
+ );
185
+ }
186
+ const x402Bindings = /* @__PURE__ */ new Map();
187
+ const retiredX402Bindings = /* @__PURE__ */ new Map();
188
+ const RETIRED_BINDING_MEMORY = 64;
189
+ function retireX402Binding(id, reason) {
190
+ retiredX402Bindings.set(id, reason);
191
+ if (retiredX402Bindings.size > RETIRED_BINDING_MEMORY) {
192
+ const oldest = retiredX402Bindings.keys().next();
193
+ if (!oldest.done) retiredX402Bindings.delete(oldest.value);
194
+ }
195
+ }
196
+ function signAndVerify(hash) {
197
+ const signature = sdk.signHash(delegateKey, hash);
198
+ if (!sdk.verifySignature(hash, signature, delegateAddress)) {
199
+ throw new sdk.HavenSigningError(
200
+ "Local signature verification failed \u2014 recovered address does not match the delegate key."
201
+ );
202
+ }
203
+ return signature;
204
+ }
205
+ return {
206
+ delegateAddress,
207
+ signPaymentHash(hash) {
208
+ return signAndVerify(hash);
209
+ },
210
+ async signDelegationTypedData(typedData) {
211
+ const account = accounts.privateKeyToAccount(delegateKey);
212
+ return account.signTypedData(typedData);
213
+ },
214
+ signX402FundingHash(hash, expected) {
215
+ assertExpectedBinding(hash, expected, options.x402BindingSigner, "hash");
216
+ assertPayerMatchesDelegate(expected, delegateAddress, options.agentId);
217
+ const signature = signAndVerify(hash);
218
+ const x402Binding = crypto.randomUUID();
219
+ x402Bindings.set(x402Binding, { ...expected });
220
+ return { signature, x402Binding };
221
+ },
222
+ async signX402FundingTypedData(typedData, expected) {
223
+ assertExpectedBinding(expected.payloadHash, expected, options.x402BindingSigner, "typed-data");
224
+ assertPayerMatchesDelegate(expected, delegateAddress, options.agentId);
225
+ const digest = viem.hashTypedData(typedData);
226
+ if (digest.toLowerCase() !== expected.typedDataHash?.toLowerCase()) {
227
+ throw new sdk.HavenSigningError(
228
+ "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. The most common cause is the typed data being truncated or reshaped while being copied between tool calls (#1255): re-run the hosted quote and pass its typed_data_b64 string through UNCHANGED instead of re-emitting the nested JSON."
229
+ );
230
+ }
231
+ if (isSettlementChildTypedData(typedData)) {
232
+ const settlementChainId = chainIdForNetwork(expected.network);
233
+ if (settlementChainId === void 0) {
234
+ throw new sdk.HavenSigningError(
235
+ `Refusing to sign the x402 settlement child: this signer cannot map the network '${expected.network}' to a chain id, so it cannot check which chain the child is scoped to. Update @haven_ai/signer.`
236
+ );
237
+ }
238
+ verifySettlementChild(typedData, {
239
+ merchantTo: expected.merchantTo,
240
+ amount: expected.amount,
241
+ asset: expected.asset,
242
+ // From the SIGNED network, not the payload's own claim. Passing
243
+ // Number(typedData.domain.chainId) here made the check compare a
244
+ // value to itself — vacuous, and precisely the "declared one thing,
245
+ // signed another" class this file exists to catch (#1455 review).
246
+ chainId: settlementChainId,
247
+ expiresAt: expected.expiresAt
248
+ });
249
+ }
250
+ const account = accounts.privateKeyToAccount(delegateKey);
251
+ const signature = await account.signTypedData(
252
+ typedData
253
+ );
254
+ const x402Binding = crypto.randomUUID();
255
+ x402Bindings.set(x402Binding, { ...expected });
256
+ return { signature, x402Binding };
257
+ },
258
+ async buildX402PaymentHeader(paymentRequired, x402Binding) {
259
+ const expected = x402Bindings.get(x402Binding);
260
+ if (!expected) {
261
+ const retired = retiredX402Bindings.get(x402Binding);
262
+ if (retired === "header_built") {
263
+ throw new sdk.HavenSigningError(
264
+ "This x402 binding was already used to build a merchant header. Bindings are single-use. If you called haven_sign_x402, it already returned the payment_header \u2014 retry the merchant with THAT header instead of building another; haven_x402_sign_header is the follow-up to haven_sign, not to haven_sign_x402. If you no longer have the header, re-run the quote tool with the same idempotency_key."
265
+ );
266
+ }
267
+ if (retired === "window_expired") {
268
+ throw new sdk.HavenSigningError(
269
+ "This x402 binding was retired because its payment window closed before a merchant header could be built \u2014 no header exists to retry with. Re-run the quote tool with the same idempotency_key to get a fresh window, then sign again."
270
+ );
271
+ }
272
+ throw new sdk.HavenSigningError(
273
+ "x402 funding binding is required before signing a merchant header. Sign the hosted funding hash with x402_expected first (haven_sign returns a binding this tool can use). A binding is also lost when the signer process restarts, since bindings live in memory only \u2014 re-sign to mint a fresh one."
274
+ );
275
+ }
276
+ try {
277
+ assertX402PaymentWindowOpen(expected);
278
+ } catch (err) {
279
+ x402Bindings.delete(x402Binding);
280
+ retireX402Binding(x402Binding, "window_expired");
281
+ throw err;
282
+ }
283
+ const option = sdk.selectStandardPaymentOption(paymentRequired.accepts);
284
+ if (!option) {
285
+ throw new sdk.HavenApiError(
286
+ "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
287
+ 400
288
+ );
289
+ }
290
+ assertX402MatchesExpected(paymentRequired, option, expected);
291
+ const account = accounts.privateKeyToAccount(delegateKey);
292
+ const requirements = sdk.toStandardPaymentRequirements(paymentRequired, option);
293
+ const header = await schemes.exact.evm.createPaymentHeader(
294
+ account,
295
+ paymentRequired.x402Version,
296
+ requirements
297
+ );
298
+ if (paymentRequired.x402Version < 2) {
299
+ x402Bindings.delete(x402Binding);
300
+ retireX402Binding(x402Binding, "header_built");
301
+ return { paymentHeader: header, accepted: option };
302
+ }
303
+ try {
304
+ const payment = sdk.decodeBase64Json(header);
305
+ const wrapped = sdk.encodeBase64Json(
306
+ sdk.x402V2PaymentEnvelope(paymentRequired, option, payment.payload)
307
+ );
308
+ return { paymentHeader: wrapped, accepted: option };
309
+ } finally {
310
+ x402Bindings.delete(x402Binding);
311
+ retireX402Binding(x402Binding, "header_built");
312
+ }
313
+ },
314
+ async signSweepAuthorization({
315
+ authorization,
316
+ expectedAuth,
317
+ expectedSafe
318
+ }) {
319
+ assertSweepBinding(authorization, expectedAuth, options.x402BindingSigner);
320
+ if (!sameAddress(authorization.from, delegateAddress)) {
321
+ throw new sdk.HavenSigningError(
322
+ "Sweep authorization `from` does not match this delegate address."
323
+ );
324
+ }
325
+ if (expectedSafe && !sameAddress(authorization.to, expectedSafe)) {
326
+ throw new sdk.HavenSigningError(
327
+ "Sweep authorization `to` does not match the Safe in the local credential."
328
+ );
329
+ }
330
+ const typedData = sdk.buildSweepTypedData(authorization);
331
+ const viemTypedData = {
332
+ domain: {
333
+ ...typedData.domain,
334
+ verifyingContract: typedData.domain.verifyingContract
335
+ },
336
+ types: typedData.types,
337
+ primaryType: typedData.primaryType,
338
+ message: {
339
+ ...typedData.message,
340
+ from: typedData.message.from,
341
+ to: typedData.message.to,
342
+ nonce: typedData.message.nonce
343
+ }
344
+ };
345
+ const account = accounts.privateKeyToAccount(delegateKey);
346
+ const signature = await account.signTypedData(viemTypedData);
347
+ const recovered = await viem.recoverTypedDataAddress({ ...viemTypedData, signature });
348
+ if (!sameAddress(recovered, delegateAddress)) {
349
+ throw new sdk.HavenSigningError(
350
+ "Local sweep signature verification failed \u2014 recovered address does not match the delegate key."
351
+ );
352
+ }
353
+ return { signature };
354
+ }
355
+ };
356
+ }
357
+ function assertSweepBinding(authorization, expectedAuth, trustedSigner) {
358
+ if (!expectedAuth || typeof expectedAuth !== "object") {
359
+ throw new sdk.HavenSigningError("Sweep authorization binding is required before signing.");
360
+ }
361
+ if (!trustedSigner) {
362
+ throw new sdk.HavenSigningError(
363
+ "Sweep binding verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing sweep authorizations."
364
+ );
365
+ }
366
+ assertSupportedBindingVersion(
367
+ expectedAuth.version,
368
+ SUPPORTED_SWEEP_BINDING_VERSIONS,
369
+ "sweep authorization binding"
370
+ );
371
+ const message = sdk.buildSweepAuthorizationMessage(authorization);
372
+ if (expectedAuth.message !== message) {
373
+ throw new sdk.HavenSigningError("Sweep authorization binding does not match the authorization being signed.");
374
+ }
375
+ if (!sameAddress(expectedAuth.signer, trustedSigner)) {
376
+ throw new sdk.HavenSigningError("Sweep authorization binding was not signed by the configured Haven signer.");
377
+ }
378
+ if (!sdk.verifySignature(viem.hashMessage(message), expectedAuth.signature, trustedSigner)) {
379
+ throw new sdk.HavenSigningError("Sweep authorization binding signature could not be verified.");
380
+ }
381
+ }
382
+ function assertX402MatchesExpected(paymentRequired, option, expected) {
383
+ assertExpectedShape(expected);
384
+ const headerResource = option.resource ?? paymentRequired.resource.url;
385
+ if (headerResource !== expected.resourceUrl) {
386
+ throw new sdk.HavenSigningError("x402 payment_required resource does not match the funded intent.");
387
+ }
388
+ if (!sameAddress(option.payTo, expected.merchantTo)) {
389
+ throw new sdk.HavenSigningError("x402 merchant recipient does not match the funded intent.");
390
+ }
391
+ if (sdk.x402AuthorizationAmount(option) !== expected.amount) {
392
+ throw new sdk.HavenSigningError("x402 amount does not match the funded intent.");
393
+ }
394
+ if (!sameAddress(option.asset, expected.asset)) {
395
+ throw new sdk.HavenSigningError("x402 asset does not match the funded intent.");
396
+ }
397
+ if (option.network !== expected.network) {
398
+ throw new sdk.HavenSigningError("x402 network does not match the funded intent.");
399
+ }
400
+ }
401
+ function assertExpectedShape(expected) {
402
+ if (!expected || typeof expected !== "object") {
403
+ throw new sdk.HavenSigningError("x402 expected funding context is required before signing a merchant header.");
404
+ }
405
+ }
406
+ var SUPPORTED_X402_EXPECTED_VERSIONS = [1, 2, 3];
407
+ var SUPPORTED_SWEEP_BINDING_VERSIONS = [1];
408
+ function assertSupportedBindingVersion(received, supported, context) {
409
+ if (supported.includes(received)) return;
410
+ const highest = Math.max(...supported);
411
+ const outOfDate = received > highest;
412
+ const code = context === "x402 expected context" ? sdk.SignerRefusalCode.UnsupportedExpectedContextVersion : sdk.SignerRefusalCode.UnsupportedSweepBindingVersion;
413
+ 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 rerun the Haven connector (\`${sdk.connectorRerunCommand()}\`), which reinstalls the pinned MCP runtime.` : `Unsupported ${context} version ${received}: this signer supports ${supported.join(", ")}.`;
414
+ const fallback = outOfDate ? sdk.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.`;
415
+ throw new sdk.HavenUnsupportedSignerVersionError(
416
+ `${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.`,
417
+ code,
418
+ supported,
419
+ received,
420
+ fallback
421
+ );
422
+ }
423
+ function assertExpectedBinding(payloadHash, expected, trustedSigner, mode = "hash") {
424
+ assertExpectedShape(expected);
425
+ if (!trustedSigner) {
426
+ throw new sdk.HavenSigningError(
427
+ "x402 expected-context verifier is not configured. Set HAVEN_X402_BINDING_SIGNER before signing x402 funding hashes."
428
+ );
429
+ }
430
+ if (expected.auth) {
431
+ assertSupportedBindingVersion(
432
+ expected.auth.version,
433
+ SUPPORTED_X402_EXPECTED_VERSIONS,
434
+ "x402 expected context"
435
+ );
436
+ }
437
+ if (expected.payloadHash.toLowerCase() !== payloadHash.toLowerCase()) {
438
+ throw new sdk.HavenSigningError("x402 expected context does not match the funding hash being signed.");
439
+ }
440
+ if (mode === "hash" && expected.typedDataHash) {
441
+ throw new sdk.HavenSigningError(
442
+ "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."
443
+ );
444
+ }
445
+ if (mode === "typed-data" && !expected.typedDataHash) {
446
+ throw new sdk.HavenSigningError(
447
+ "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."
448
+ );
449
+ }
450
+ const message = sdk.buildX402ExpectedMessage({
451
+ paymentId: expected.paymentId,
452
+ payloadHash: expected.payloadHash,
453
+ resourceUrl: expected.resourceUrl,
454
+ merchantTo: expected.merchantTo,
455
+ amount: expected.amount,
456
+ asset: expected.asset,
457
+ network: expected.network,
458
+ expiresAt: expected.expiresAt,
459
+ typedDataHash: expected.typedDataHash,
460
+ payerDelegate: expected.payerDelegate,
461
+ payerAgentId: expected.payerAgentId
462
+ });
463
+ const expectedVersion = expected.payerDelegate ? 3 : expected.typedDataHash ? 2 : 1;
464
+ if (expected.auth?.version !== expectedVersion || expected.auth.message !== message) {
465
+ throw new sdk.HavenSigningError("x402 expected context authentication message is invalid.");
466
+ }
467
+ if (!sameAddress(expected.auth.signer, trustedSigner)) {
468
+ throw new sdk.HavenSigningError("x402 expected context was not signed by the configured Haven signer.");
469
+ }
470
+ if (!sdk.verifySignature(viem.hashMessage(message), expected.auth.signature, trustedSigner)) {
471
+ throw new sdk.HavenSigningError("x402 expected context signature could not be verified.");
472
+ }
473
+ }
474
+ function assertPayerMatchesDelegate(expected, delegateAddress, localAgentId) {
475
+ if (!expected.payerDelegate) return;
476
+ if (sameAddress(expected.payerDelegate, delegateAddress)) return;
477
+ const quoteAgent = expected.payerAgentId ?? "unknown";
478
+ const localAgent = localAgentId ?? "unknown";
479
+ throw new sdk.HavenSigningError(
480
+ `This quote belongs to a DIFFERENT agent: the quote was created for agent ${quoteAgent} (delegate ${expected.payerDelegate}), but this signer holds agent ${localAgent} (delegate ${delegateAddress}). A long-lived host is holding stale credentials \u2014 its session authenticates as the old agent while the signer on disk belongs to the new one. Nothing was signed. Restart the host so it re-reads its wiring, then re-quote.`
481
+ );
482
+ }
483
+ function assertX402PaymentWindowOpen(expected) {
484
+ if (!expected.expiresAt) return;
485
+ const expiresAtMs = Date.parse(expected.expiresAt);
486
+ if (Number.isNaN(expiresAtMs)) {
487
+ throw new sdk.HavenSigningError("x402 expected context expiresAt is not a valid ISO timestamp.");
488
+ }
489
+ if (expiresAtMs <= Date.now()) {
490
+ throw new sdk.HavenError(
491
+ "The x402 payment window expired before the merchant header could be signed. Re-quote with haven_pay_mcp_tool using the same idempotency_key before trying again.",
492
+ sdk.AgentPaymentFailureCode.PaymentWindowExpired,
493
+ 410,
494
+ expected.paymentId
495
+ );
496
+ }
497
+ }
498
+ function sameAddress(a, b) {
499
+ return a.toLowerCase() === b.toLowerCase();
500
+ }
501
+
502
+ // src/capabilities.ts
503
+ var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
504
+ function signerCompatibility() {
505
+ return {
506
+ x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
507
+ sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS]
508
+ };
509
+ }
510
+ function signerCapabilityAdvertisement() {
511
+ return { experimental: { [SIGNER_CAPABILITY_KEY]: signerCompatibility() } };
512
+ }
513
+ function signerInstructions() {
514
+ const compatibility = signerCompatibility();
515
+ return [
516
+ "Haven edge signer: sign-only tools bound to the local delegate key. It never emits the",
517
+ "key. Its one network capability is an authenticated READ of a signing context from",
518
+ "Haven by payment_id \u2014 pass payment_id to haven_sign / haven_sign_x402 (preferred for",
519
+ "delegation-rail x402) instead of relaying bulky typed-data payloads yourself.",
520
+ "",
521
+ "Version compatibility (check this BEFORE signing, not after):",
522
+ `- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
523
+ `- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
524
+ "",
525
+ "Haven quote and prepare results report the expected-context version they will emit",
526
+ "(signer_compatibility.x402_expected_context_version). If that version is not in the list",
527
+ "above, this signer is out of date: STOP before signing, and tell the user to update",
528
+ `@haven_ai/signer by rerunning \`${sdk.connectorRerunCommand()}\`, which reinstalls the pinned`,
529
+ "MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
530
+ "Haven-signed binding message, so changing it invalidates the signature.",
531
+ "",
532
+ "A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
533
+ "machine-readable, not just prose: it carries code, supported_versions, received_version, and",
534
+ "fallback fields alongside the message, so you can branch on it directly."
535
+ ].join("\n");
536
+ }
537
+ async function loadHavenIdentity(credentialsPath) {
538
+ if (!credentialsPath) return null;
539
+ try {
540
+ const raw = JSON.parse(
541
+ await promises.readFile(path.join(path.dirname(credentialsPath), "identity.json"), "utf8")
542
+ );
543
+ const apiKey = typeof raw.api_key === "string" ? raw.api_key : void 0;
544
+ const apiUrl = typeof raw.api_url === "string" ? raw.api_url : void 0;
545
+ if (!apiKey || !apiUrl) return null;
546
+ return { apiKey, apiUrl: apiUrl.replace(/\/+$/, "") };
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+ async function fetchX402SignContext(identity, paymentId, fetchImpl = fetch) {
552
+ let response;
553
+ try {
554
+ response = await fetchImpl(
555
+ `${identity.apiUrl}/x402/${encodeURIComponent(paymentId)}/sign-context`,
556
+ { headers: { Authorization: `Bearer ${identity.apiKey}` } }
557
+ );
558
+ } catch (err) {
559
+ throw new sdk.HavenSigningError(
560
+ `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.`
561
+ );
562
+ }
563
+ const body = await response.json().catch(() => ({}));
564
+ if (!response.ok) {
565
+ const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
566
+ throw new sdk.HavenSigningError(
567
+ `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." : "")
568
+ );
569
+ }
570
+ const signData = body.sign_data;
571
+ const x402Expected = body.x402_expected;
572
+ if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || !x402Expected) {
573
+ throw new sdk.HavenSigningError(
574
+ "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."
575
+ );
576
+ }
577
+ const paymentRequired = body.payment_required;
578
+ return {
579
+ paymentId: String(body.payment_id ?? paymentId),
580
+ payloadHash: signData.hash,
581
+ typedData: signData.typed_data,
582
+ x402Expected,
583
+ paymentRequired: paymentRequired && typeof paymentRequired === "object" && !Array.isArray(paymentRequired) ? paymentRequired : null
584
+ };
585
+ }
586
+
587
+ // src/tools.ts
588
+ var sweepAuthorizationSchema = v3.z.object({
589
+ from: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "from must be a 0x address"),
590
+ to: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "to must be a 0x address"),
591
+ value: v3.z.string().regex(/^[0-9]+$/, "value must be a decimal atomic amount"),
592
+ validAfter: v3.z.string().regex(/^[0-9]+$/, "validAfter must be a decimal unix time"),
593
+ validBefore: v3.z.string().regex(/^[0-9]+$/, "validBefore must be a decimal unix time"),
594
+ nonce: v3.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "nonce must be a 0x-prefixed 32-byte hex string"),
595
+ token: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "token must be a 0x address"),
596
+ chainId: v3.z.number().int().positive()
597
+ });
598
+ var bindingVersionSchema = v3.z.number().int().positive();
599
+ var sweepExpectedAuthSchema = v3.z.object({
600
+ version: bindingVersionSchema,
601
+ message: v3.z.string().min(1),
602
+ signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
603
+ signer: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "signer must be a 0x address")
604
+ });
605
+ var x402ExpectedShape = {
606
+ payment_id: v3.z.string().min(1),
607
+ payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string"),
608
+ resource_url: v3.z.string().url(),
609
+ merchant_to: v3.z.string().min(1),
610
+ amount: v3.z.string().min(1),
611
+ asset: v3.z.string().min(1),
612
+ network: v3.z.string().min(1),
613
+ // Required: expires_at is folded into the Haven-signed binding message, so the
614
+ // signer must receive the exact same value to reconstruct a matching message.
615
+ // Omitting it used to fail downstream with a cryptic "authentication message is
616
+ // invalid" — making it required surfaces a clear INVALID_INPUT at the boundary.
617
+ expires_at: v3.z.string().min(1),
618
+ // #1138: present on a delegation-rail (v2) context. It commits to the EIP-712
619
+ // typed data the account validates; the signer refuses to raw-sign the bare
620
+ // hash when it is present, and refuses to sign typed data when it is absent.
621
+ typed_data_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "typed_data_hash must be a 0x-prefixed hex string").optional(),
622
+ // #1690: present on a v3 context. The delegate this quote was created FOR —
623
+ // the signer refuses to sign when it is not its own. Inside the Haven-signed
624
+ // message, so stripping it here would only break the binding signature.
625
+ payer_delegate: v3.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "payer_delegate must be a 0x-prefixed address").optional(),
626
+ payer_agent_id: v3.z.string().min(1).optional(),
627
+ auth: v3.z.object({
628
+ // Open at the boundary, enforced in the signer — see bindingVersionSchema.
629
+ version: bindingVersionSchema,
630
+ message: v3.z.string().min(1),
631
+ signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string"),
632
+ signer: v3.z.string().min(1)
633
+ })
634
+ };
635
+ var x402ExpectedSchema = v3.z.object(x402ExpectedShape);
636
+ var toolSchemas = {
637
+ haven_sign_sweep_delegate: {
638
+ // The authorization fields prepared by Haven's POST /sweep/prepare. Passed
639
+ // through verbatim from the hosted haven_sweep_delegate tool — the signer
640
+ // re-derives the binding message from these exact values.
641
+ authorization: sweepAuthorizationSchema,
642
+ // Haven's signature over the authorization context (the binding).
643
+ expected_auth: sweepExpectedAuthSchema
644
+ },
645
+ haven_sign: {
646
+ // #1263: THE preferred x402 input — the signer fetches the exact signing
647
+ // payload + expected context from Haven itself (authenticated with the
648
+ // locally-stored agent credential), so no bulky bytes ever cross the
649
+ // model. Pass payment_id ALONE for a delegation-rail x402 funding intent.
650
+ payment_id: v3.z.string().min(1).optional(),
651
+ // The unsigned hash from haven_pay / haven_pay_x402_quote (payload_hash).
652
+ // Optional when payment_id is supplied (the fetch carries it).
653
+ payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
654
+ // Pass x402.expected from hosted haven_pay_x402_quote when this hash funds
655
+ // a standard x402 merchant retry. The signer records it locally and returns
656
+ // an opaque x402_binding for the later header-signing step.
657
+ x402_expected: x402ExpectedSchema.optional(),
658
+ // #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
659
+ // (not z.unknown()) so MCP clients embed it as JSON rather than a string.
660
+ typed_data: v3.z.record(v3.z.string(), v3.z.unknown()).optional(),
661
+ // #1255: the same payload as ONE opaque base64 string, exactly as returned
662
+ // by the hosted tools. Preferred over typed_data when both are present —
663
+ // an agent re-emitting multi-KB nested JSON between tool calls is the
664
+ // failure mode this field removes (a truncated/reshaped payload fails the
665
+ // digest check and the payment refuses, correctly but pointlessly).
666
+ // Bounded: a realistic redemption payload is ~10KB encoded; 256KB is
667
+ // generous headroom while keeping the offline signer from materializing
668
+ // arbitrarily large caller input.
669
+ typed_data_b64: v3.z.string().min(1).max(262144).optional()
670
+ },
671
+ haven_x402_sign_header: {
672
+ // The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
673
+ // (not z.unknown(), which becomes empty JSON Schema `{}`) so MCP clients
674
+ // embed it as JSON rather than serialising the object to a string.
675
+ payment_required: v3.z.record(v3.z.string(), v3.z.unknown()),
676
+ // Opaque binding returned by haven_sign when x402_expected was supplied.
677
+ x402_binding: v3.z.string().min(1)
678
+ },
679
+ haven_sign_x402: {
680
+ // #1263: preferred — pass payment_id (plus payment_required) and the
681
+ // signer fetches the exact signing payload + expected context itself.
682
+ payment_id: v3.z.string().min(1).optional(),
683
+ // One-shot x402 signing: funding hash + merchant header in one local call.
684
+ // Both optional when payment_id is supplied (the fetch carries them).
685
+ payload_hash: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "payload_hash must be a 0x-prefixed hex string").optional(),
686
+ x402_expected: x402ExpectedSchema.optional(),
687
+ // #1355: optional when payment_id is supplied — the signer's context fetch
688
+ // carries the stored 402 PaymentRequired, so `{ payment_id }` alone is the
689
+ // preferred call. Still required (via the runtime check in the handler)
690
+ // for the quote-based fallback path without a payment_id.
691
+ payment_required: v3.z.record(v3.z.string(), v3.z.unknown()).optional(),
692
+ // #1138: delegation-rail intents sign THIS, not payload_hash. Object-typed
693
+ // (not z.unknown()) so MCP clients embed it as JSON rather than a string.
694
+ typed_data: v3.z.record(v3.z.string(), v3.z.unknown()).optional(),
695
+ // #1255: see haven_sign.typed_data_b64 — the copy-through-safe form.
696
+ typed_data_b64: v3.z.string().min(1).max(262144).optional()
697
+ }
698
+ };
699
+ var SIGN_DESCRIPTION = [
700
+ "Sign an unsigned Haven payment hash with the local delegate key. The delegate key never leaves",
701
+ "this process. Pass the payload_hash returned by haven_pay or haven_pay_x402_quote.",
702
+ "For x402, also pass x402_expected from haven_pay_x402_quote; the signer records it locally",
703
+ "and returns { signature, x402_binding }. x402_expected includes expires_at; sign before that",
704
+ "window closes. DELEGATION-RAIL x402 accounts (#1263): pass payment_id ALONE (preferred) \u2014 this",
705
+ "signer fetches the exact signing payload and expected context from Haven itself, so nothing",
706
+ "bulky ever crosses your context. Fallback: pass typed_data_b64 through UNCHANGED (never re-type",
707
+ "the nested typed_data JSON); the account validates that EIP-712 payload, not payload_hash.",
708
+ "Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
709
+ "to mcp__haven-signer__haven_x402_sign_header. For plain SafeTransfer payments, just pass payload_hash and",
710
+ "relay the returned signature via mcp__haven__haven_submit."
711
+ ].join(" ");
712
+ var X402_SIGN_HEADER_DESCRIPTION = [
713
+ "Build and sign the EIP-3009 merchant payment header for the merchant leg of an x402 payment.",
714
+ "The delegate key stays local \u2014 only the signed header crosses any boundary.",
715
+ "Pass the payment_required from the original merchant 402 response and the x402_binding",
716
+ "returned by haven_sign. It must be haven_sign \u2014 NOT haven_sign_x402, which is a",
717
+ "one-shot that builds the header itself and spends its own binding doing so. If you called",
718
+ "haven_sign_x402, its result already carries payment_header; retry the merchant with that and",
719
+ "do not call this tool. The signer validates the merchant, amount, resource, asset, and",
720
+ "network against the recorded funding context before signing, checks expires_at when present,",
721
+ "and rejects mismatches or expired payment windows.",
722
+ "Returns { payment_header, accepted }. On your retry set BOTH PAYMENT-SIGNATURE (x402 v2) and",
723
+ "X-PAYMENT (v1) to <payment_header>; a strict v2 merchant reads only the first.",
724
+ "Only call after haven_submit has confirmed the funding step (nextAction=none or",
725
+ "the funding tx has a confirmed status). Next for paid MCP tools: call mcp__haven__haven_complete_mcp_tool."
726
+ ].join(" ");
727
+ var SIGN_X402_DESCRIPTION = [
728
+ "One-shot x402 signing for the fast 3-call flow: sign the funding hash AND build the EIP-3009",
729
+ "merchant payment header in a single local call (equivalent to haven_sign followed by",
730
+ "haven_x402_sign_header). The delegate key never leaves this process. From the haven_pay_mcp_tool",
731
+ "result pass JUST payment_id \u2014 PREFERRED (#1263, #1355): this signer fetches the exact signing",
732
+ "payload, expected context, and merchant payment_required from Haven itself, so nothing bulky",
733
+ "crosses your context. If the signer reports the context carried no payment_required (older",
734
+ "backend), re-call with payment_id plus payment_required verbatim from the pay result.",
735
+ "Fallback for older backends: pass payload_hash, x402_expected (the nested x402.expected object \u2014",
736
+ "passing the whole x402 object is also accepted and unwrapped for you), and typed_data_b64",
737
+ "through UNCHANGED; the signer",
738
+ "signs the typed data instead of payload_hash and refuses the bare hash when the context commits to typed data.",
739
+ "Returns",
740
+ "{ signature, x402_binding, payment_header, accepted }; hand signature + payment_header to",
741
+ "mcp__haven__haven_settle_mcp_tool to fund and settle in one hosted call. The header is built now (before",
742
+ "funding confirms), so its short validity window starts here \u2014 call mcp__haven__haven_settle_mcp_tool promptly,",
743
+ "and re-run mcp__haven__haven_pay_mcp_tool with the same idempotency_key if a tool returns PAYMENT_WINDOW_EXPIRED.",
744
+ "The returned x402_binding is ALREADY SPENT \u2014 this tool consumed it building payment_header \u2014",
745
+ "so never pass it to mcp__haven-signer__haven_x402_sign_header; that tool is the follow-up to",
746
+ "haven_sign, not to this one. payment_header IS the header to use.",
747
+ "Next: for a paid MCP tool, call mcp__haven__haven_settle_mcp_tool. For a direct plain-HTTP x402",
748
+ "merchant (the haven_pay_x402_quote path), relay signature via mcp__haven__haven_submit and then",
749
+ "retry the original merchant URL YOURSELF, setting BOTH PAYMENT-SIGNATURE (x402 v2) and",
750
+ "X-PAYMENT (v1) to payment_header \u2014 Haven never contacts that merchant."
751
+ ].join(" ");
752
+ var SIGN_SWEEP_DELEGATE_DESCRIPTION = [
753
+ "Sign a Haven-prepared gasless USDC sweep that recovers stranded funds from the delegate",
754
+ "wallet back to your Haven wallet. The delegate key never leaves this process and this tool",
755
+ "never broadcasts \u2014 it returns only an EIP-3009 signature that Haven's relayer submits and",
756
+ "pays gas for. Pass the authorization and expected_auth returned by the hosted",
757
+ "haven_sweep_delegate tool. The signer verifies Haven authored the authorization and that it",
758
+ "pays out to your own Safe before signing, then returns { signature } to hand back to",
759
+ "mcp__haven__haven_sweep_delegate to complete recovery."
760
+ ].join(" ");
761
+ var toolDescriptions = {
762
+ haven_sign: SIGN_DESCRIPTION,
763
+ haven_x402_sign_header: X402_SIGN_HEADER_DESCRIPTION,
764
+ haven_sign_x402: SIGN_X402_DESCRIPTION,
765
+ haven_sign_sweep_delegate: SIGN_SWEEP_DELEGATE_DESCRIPTION
766
+ };
767
+ async function signFundingLeg(signer, expected, payloadHash, typedData) {
768
+ if (!expected.typedDataHash) {
769
+ return signer.signX402FundingHash(payloadHash, expected);
770
+ }
771
+ if (!typedData) {
772
+ throw new sdk.HavenSigningError(
773
+ "This x402 funding intent is on the delegation rail: it commits to EIP-712 typed data, which was not supplied. Pass typed_data_b64 from haven_pay_mcp_tool / haven_pay_x402_quote through unchanged (or typed_data verbatim) \u2014 the bare payload_hash is not what the account validates."
774
+ );
775
+ }
776
+ return signer.signX402FundingTypedData(typedData, expected);
777
+ }
778
+ function toExpectedX402(raw) {
779
+ return {
780
+ paymentId: raw.payment_id,
781
+ payloadHash: raw.payload_hash,
782
+ typedDataHash: raw.typed_data_hash,
783
+ payerDelegate: raw.payer_delegate,
784
+ payerAgentId: raw.payer_agent_id,
785
+ resourceUrl: raw.resource_url,
786
+ merchantTo: raw.merchant_to,
787
+ amount: raw.amount,
788
+ asset: raw.asset,
789
+ network: raw.network,
790
+ expiresAt: raw.expires_at,
791
+ auth: raw.auth
792
+ };
793
+ }
794
+ function parseFetchedExpected(fetched) {
795
+ const parsed = v3.z.object(x402ExpectedShape).safeParse(fetched.x402Expected);
796
+ if (!parsed.success) {
797
+ throw new sdk.HavenSigningError(
798
+ `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"}`
799
+ );
800
+ }
801
+ return parsed.data;
802
+ }
803
+ function resolveTypedData(args) {
804
+ if (!args.typed_data_b64) return args.typed_data;
805
+ try {
806
+ const decoded = JSON.parse(
807
+ Buffer.from(args.typed_data_b64, "base64").toString("utf8")
808
+ );
809
+ if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
810
+ throw new Error("decoded value is not an object");
811
+ }
812
+ return decoded;
813
+ } catch (err) {
814
+ throw new sdk.HavenSigningError(
815
+ `typed_data_b64 did not decode to a JSON object. Pass the exact string returned by the hosted Haven tool, unchanged \u2014 do not re-encode, trim, or reformat it. Underlying error: ${err instanceof Error ? err.message : String(err)}`
816
+ );
817
+ }
818
+ }
819
+ function createToolHandlers(signer, options = {}) {
820
+ async function resolveSignContext(args) {
821
+ if (!args.payment_id) return null;
822
+ const identity = await options.signContext?.loadIdentity() ?? null;
823
+ if (!identity) {
824
+ throw new sdk.HavenSigningError(
825
+ `payment_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${sdk.connectorRerunCommand()}\` to restore it, or pass typed_data_b64 from the quote result instead.`
826
+ );
827
+ }
828
+ const ctx = await fetchX402SignContext(
829
+ identity,
830
+ args.payment_id,
831
+ options.signContext?.fetchImpl
832
+ );
833
+ if (args.payload_hash && args.payload_hash.toLowerCase() !== ctx.payloadHash.toLowerCase()) {
834
+ throw new sdk.HavenSigningError(
835
+ `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.`
836
+ );
837
+ }
838
+ return ctx;
839
+ }
840
+ return {
841
+ haven_sign: async (input) => runTool(async () => {
842
+ const args = parse("haven_sign", coerceX402Expected(input));
843
+ const fetched = await resolveSignContext(args);
844
+ const typedData = fetched?.typedData ?? resolveTypedData(args);
845
+ const payloadHash = fetched?.payloadHash ?? args.payload_hash;
846
+ if (!payloadHash) {
847
+ throw new sdk.HavenSigningError(
848
+ "Pass payment_id (preferred for delegation-rail x402) or payload_hash."
849
+ );
850
+ }
851
+ const expectedRaw = fetched ? parseFetchedExpected(fetched) : args.x402_expected;
852
+ const x402Expected = expectedRaw ? toExpectedX402(expectedRaw) : null;
853
+ const result = x402Expected ? await signFundingLeg(signer, x402Expected, payloadHash, typedData) : null;
854
+ if (!result) {
855
+ if (typedData) {
856
+ if (isSettlementChildTypedData(typedData)) {
857
+ throw new sdk.HavenSigningError(
858
+ "Refusing to sign a delegation payload with no expected context. This typed data is a DELEGATION \u2014 signing it grants a third party authority to move funds \u2014 so it is only ever signed against a Haven-signed context that the caveats are verified against. Call this tool with { payment_id } instead (the signer then fetches and verifies the context itself), or use haven_sign_x402."
859
+ );
860
+ }
861
+ const signature2 = await signer.signDelegationTypedData(typedData);
862
+ await auditSigning("haven_sign", payloadHash);
863
+ return { signature: signature2 };
864
+ }
865
+ const signature = signer.signPaymentHash(payloadHash);
866
+ await auditSigning("haven_sign", payloadHash);
867
+ return { signature };
868
+ }
869
+ await auditSigning("haven_sign", payloadHash);
870
+ return { signature: result.signature, x402_binding: result.x402Binding };
871
+ }),
872
+ haven_x402_sign_header: async (input) => runTool(async () => {
873
+ const args = parse("haven_x402_sign_header", coercePaymentRequired(input));
874
+ const result = await signer.buildX402PaymentHeader(
875
+ args.payment_required,
876
+ args.x402_binding
877
+ );
878
+ await auditSigning(
879
+ "haven_x402_sign_header",
880
+ hashPayloadForAudit(args.payment_required)
881
+ );
882
+ return { payment_header: result.paymentHeader, accepted: result.accepted };
883
+ }),
884
+ haven_sign_x402: async (input) => runTool(async () => {
885
+ const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
886
+ const fetched = await resolveSignContext(args);
887
+ const payloadHash = fetched?.payloadHash ?? args.payload_hash;
888
+ const expectedRaw = fetched ? parseFetchedExpected(fetched) : args.x402_expected;
889
+ if (!payloadHash || !expectedRaw) {
890
+ throw new sdk.HavenSigningError(
891
+ "Pass payment_id (preferred \u2014 the signer fetches the signing context itself) or payload_hash + x402_expected from the quote result."
892
+ );
893
+ }
894
+ const funding = await signFundingLeg(
895
+ signer,
896
+ toExpectedX402(expectedRaw),
897
+ payloadHash,
898
+ fetched?.typedData ?? resolveTypedData(args)
899
+ );
900
+ const paymentRequired = fetched?.paymentRequired ?? args.payment_required;
901
+ if (!paymentRequired) {
902
+ throw new sdk.HavenSigningError(
903
+ fetched ? "The Haven sign-context for this payment_id carried no payment_required (backend predates #1355). Pass payment_required from the pay result explicitly, verbatim." : "payment_required is required on the quote-based path (no payment_id to fetch it by). Pass it verbatim from the quote result."
904
+ );
905
+ }
906
+ const header = await signer.buildX402PaymentHeader(
907
+ paymentRequired,
908
+ funding.x402Binding
909
+ );
910
+ await auditSigning("haven_sign_x402", payloadHash);
911
+ await auditSigning("haven_sign_x402", hashPayloadForAudit(paymentRequired));
912
+ return {
913
+ signature: funding.signature,
914
+ x402_binding: funding.x402Binding,
915
+ payment_header: header.paymentHeader,
916
+ accepted: header.accepted
917
+ };
918
+ }),
919
+ haven_sign_sweep_delegate: async (input) => runTool(async () => {
920
+ const args = parse("haven_sign_sweep_delegate", input);
921
+ const result = await signer.signSweepAuthorization({
922
+ authorization: args.authorization,
923
+ expectedAuth: args.expected_auth,
924
+ // Cross-check `to` against the Safe in the local credential when present.
925
+ expectedSafe: options.audit?.safeAddress
926
+ });
927
+ await auditSigning(
928
+ "haven_sign_sweep_delegate",
929
+ hashPayloadForAudit(args.authorization)
930
+ );
931
+ return { signature: result.signature };
932
+ })
933
+ };
934
+ async function auditSigning(tool, payloadHash) {
935
+ if (!options.audit) return;
936
+ const { auditPath, ...context } = options.audit;
937
+ await appendSigningAuditEntry(
938
+ createSigningAuditEntry(tool, payloadHash, {
939
+ ...context,
940
+ delegateAddress: signer.delegateAddress
941
+ }),
942
+ auditPath
943
+ );
944
+ }
945
+ }
946
+ function parse(name, input) {
947
+ return v3.z.object(toolSchemas[name]).parse(input ?? {});
948
+ }
949
+ function coerceX402Expected(input) {
950
+ if (!input || typeof input !== "object") return input;
951
+ const record = input;
952
+ let value = record.x402_expected;
953
+ if (typeof value === "string") {
954
+ try {
955
+ value = JSON.parse(value);
956
+ } catch {
957
+ return input;
958
+ }
959
+ }
960
+ if (!value || typeof value !== "object") return input;
961
+ const obj = value;
962
+ if (obj.auth === void 0 && obj.expected && typeof obj.expected === "object") {
963
+ return { ...record, x402_expected: obj.expected };
964
+ }
965
+ if (value !== record.x402_expected) return { ...record, x402_expected: value };
966
+ return input;
967
+ }
968
+ function coercePaymentRequired(input) {
969
+ if (!input || typeof input !== "object") return input;
970
+ const record = input;
971
+ if (typeof record.payment_required !== "string") return input;
972
+ try {
973
+ return { ...record, payment_required: JSON.parse(record.payment_required) };
974
+ } catch {
975
+ return input;
976
+ }
977
+ }
978
+ async function runTool(fn) {
979
+ try {
980
+ return { success: true, data: await fn() };
981
+ } catch (err) {
982
+ return normalizeError(err);
983
+ }
984
+ }
985
+ function normalizeError(err) {
986
+ if (err instanceof v3.z.ZodError) {
987
+ return {
988
+ success: false,
989
+ code: "INVALID_INPUT",
990
+ message: err.errors.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join("; "),
991
+ statusCode: 400
992
+ };
993
+ }
994
+ if (err instanceof sdk.HavenUnsupportedSignerVersionError) {
995
+ return {
996
+ success: false,
997
+ code: err.code,
998
+ message: err.message,
999
+ supported_versions: [...err.supportedVersions],
1000
+ received_version: err.receivedVersion,
1001
+ fallback: err.fallback,
1002
+ next_action: sdk.AgentPaymentNextAction.StopAndTellUser
1003
+ };
1004
+ }
1005
+ if (err instanceof sdk.HavenSigningError) {
1006
+ return { success: false, code: err.code, message: err.message };
1007
+ }
1008
+ if (err instanceof sdk.HavenApiError) {
1009
+ return { success: false, code: err.code, message: err.message, statusCode: err.statusCode };
1010
+ }
1011
+ if (err instanceof sdk.HavenError) {
1012
+ return {
1013
+ success: false,
1014
+ code: err.code,
1015
+ message: err.message,
1016
+ statusCode: err.statusCode,
1017
+ paymentId: err.paymentId,
1018
+ ...err.code === sdk.AgentPaymentFailureCode.PaymentWindowExpired ? {
1019
+ next_action: sdk.AgentPaymentNextAction.PaymentWindowExpired,
1020
+ retry_with_new_quote: true,
1021
+ suggested_tool: "haven_pay_mcp_tool"
1022
+ } : {}
1023
+ };
1024
+ }
1025
+ return {
1026
+ success: false,
1027
+ code: "UNKNOWN_ERROR",
1028
+ message: err instanceof Error ? err.message : String(err)
1029
+ };
1030
+ }
1031
+
1032
+ // src/consent.ts
1033
+ var SIGNER_ACK_ENV = "HAVEN_SIGNER_ACK";
1034
+ var SIGNER_CONSENT_SURFACE_VERSION = 2;
1035
+ function computeSignerConsentHash(input) {
1036
+ const identity = [
1037
+ input.delegateAddress.toLowerCase(),
1038
+ (input.safeAddress ?? "").toLowerCase(),
1039
+ input.agentId ?? "",
1040
+ input.chainId ?? "",
1041
+ input.network ?? ""
1042
+ ].join("|");
1043
+ const toolCanonical = [...input.toolNames].sort().join(",");
1044
+ return crypto.createHash("sha256").update(`${identity}
1045
+ ${toolCanonical}
1046
+ surface:v${SIGNER_CONSENT_SURFACE_VERSION}`).digest("hex").slice(0, 16);
1047
+ }
1048
+ function renderSignerConsentBlock(input, hash) {
1049
+ const lines = [
1050
+ "",
1051
+ "------------------------------------------------------------",
1052
+ "Haven edge signer - first-launch consent",
1053
+ "------------------------------------------------------------",
1054
+ "",
1055
+ `Delegate address: ${input.delegateAddress}`
1056
+ ];
1057
+ lines.push(`Haven wallet: ${input.safeAddress ?? "not provided to this signer"}`);
1058
+ if (input.agentId) lines.push(`Agent ID: ${input.agentId}`);
1059
+ if (typeof input.chainId === "number") lines.push(`Chain ID: ${input.chainId}`);
1060
+ if (input.network) lines.push(`Network: ${input.network}`);
1061
+ lines.push("");
1062
+ lines.push("This local signer holds the delegate key on this machine and signs");
1063
+ lines.push("payment payloads or x402 merchant headers for the delegate address above.");
1064
+ lines.push("Its one network use is a read-only fetch of a pending payment's signing");
1065
+ lines.push("payload from Haven, authenticated with the agent credential stored next to");
1066
+ lines.push("this signer's key file - it never sends the key, a signature, or anything");
1067
+ lines.push("else outbound, and it cannot show a live allowance summary.");
1068
+ lines.push("On-chain Safe rules remain the real spend gate, and the wallet owner can");
1069
+ lines.push("pause or revoke agent authority outside this signer.");
1070
+ lines.push("");
1071
+ lines.push("Tools this signer will expose to your agent runtime:");
1072
+ for (const name of input.toolNames) {
1073
+ lines.push(` - ${name}`);
1074
+ lines.push(` ${toolDescriptions[name]}`);
1075
+ }
1076
+ lines.push("");
1077
+ lines.push("A local audit entry is appended for every signing operation. Audit entries");
1078
+ lines.push("record timestamp, tool, payload hash, and delegate address; never the key,");
1079
+ lines.push("signature, or x402 payment header.");
1080
+ lines.push("");
1081
+ lines.push(`Consent hash: ${hash}`);
1082
+ lines.push("");
1083
+ lines.push("To acknowledge, EITHER:");
1084
+ lines.push(` - set ${SIGNER_ACK_ENV}=${hash} in this process's environment, OR`);
1085
+ lines.push(" - re-run with --ack to write the acknowledgement next to your");
1086
+ lines.push(" credential file (sidecar <credentials>.signer-ack.json).");
1087
+ lines.push("");
1088
+ lines.push("------------------------------------------------------------");
1089
+ lines.push("");
1090
+ return lines.join("\n");
1091
+ }
1092
+ async function ensureSignerConsent(input, options = {}) {
1093
+ const env = options.env ?? process.env;
1094
+ const out = options.out ?? process.stderr;
1095
+ const hash = computeSignerConsentHash(input);
1096
+ const envAck = env[SIGNER_ACK_ENV];
1097
+ if (typeof envAck === "string" && envAck.length > 0) {
1098
+ if (envAck === hash) return { ok: true, hash, reason: "env_var_match" };
1099
+ out.write(renderSignerConsentBlock(input, hash));
1100
+ out.write(
1101
+ `${SIGNER_ACK_ENV} was set but did not match the current signer consent hash.
1102
+ Expected: ${hash}
1103
+ Got: ${envAck}
1104
+ Re-acknowledge with the new hash above, or run with --ack.
1105
+
1106
+ `
1107
+ );
1108
+ return { ok: false, hash, reason: "env_var_mismatch" };
1109
+ }
1110
+ const ackPath = sidecarPath(options.credentialsPath);
1111
+ if (ackPath) {
1112
+ const stored = await readAckFile(ackPath);
1113
+ if (stored?.ack === hash) return { ok: true, hash, reason: "ack_file_match" };
1114
+ }
1115
+ if (options.writeAck && ackPath) {
1116
+ out.write(renderSignerConsentBlock(input, hash));
1117
+ await writeAckFile(ackPath, hash);
1118
+ out.write(`Wrote acknowledgement to ${ackPath}
1119
+
1120
+ `);
1121
+ return { ok: true, hash, reason: "wrote_ack_file" };
1122
+ }
1123
+ out.write(renderSignerConsentBlock(input, hash));
1124
+ return { ok: false, hash, reason: "no_acknowledgement" };
1125
+ }
1126
+ function registeredSignerToolNames() {
1127
+ return Object.keys(toolSchemas);
1128
+ }
1129
+ function sidecarPath(credentialsPath) {
1130
+ if (!credentialsPath) return null;
1131
+ return path.resolve(`${credentialsPath}.signer-ack.json`);
1132
+ }
1133
+ async function readAckFile(path) {
1134
+ try {
1135
+ const raw = await promises.readFile(path, "utf8");
1136
+ const parsed = JSON.parse(raw);
1137
+ return { ack: typeof parsed.ack === "string" ? parsed.ack : void 0 };
1138
+ } catch {
1139
+ return null;
1140
+ }
1141
+ }
1142
+ async function writeAckFile(path$1, hash) {
1143
+ await promises.mkdir(path.dirname(path$1), { recursive: true });
1144
+ await promises.writeFile(
1145
+ path$1,
1146
+ JSON.stringify({ ack: hash, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
1147
+ "utf8"
1148
+ );
1149
+ }
1150
+ async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
1151
+ if (path) return loadFromFile(path);
1152
+ const envKey = stringField(process.env.HAVEN_DELEGATE_KEY);
1153
+ if (envKey) {
1154
+ return {
1155
+ delegateKey: envKey,
1156
+ agentId: stringField(process.env.HAVEN_AGENT_ID),
1157
+ safeAddress: stringField(process.env.HAVEN_SAFE_ADDRESS),
1158
+ chainId: chainIdField(process.env.HAVEN_CHAIN_ID, "HAVEN_CHAIN_ID"),
1159
+ network: stringField(process.env.HAVEN_NETWORK),
1160
+ x402BindingSigner: stringField(process.env.HAVEN_X402_BINDING_SIGNER)
1161
+ };
1162
+ }
1163
+ throw new Error(
1164
+ "No delegate key found. Set HAVEN_DELEGATE_KEY, pass --credentials <path>, or set HAVEN_CREDENTIALS to a Haven agent credential JSON file."
1165
+ );
1166
+ }
1167
+ async function loadFromFile(path) {
1168
+ let rawText;
1169
+ try {
1170
+ rawText = await promises.readFile(path, "utf8");
1171
+ } catch (err) {
1172
+ throw new Error(
1173
+ `Could not read Haven credentials at ${path}: ${err instanceof Error ? err.message : String(err)}`
1174
+ );
1175
+ }
1176
+ await warnIfCredentialFilePermissive(path);
1177
+ let raw;
1178
+ try {
1179
+ raw = JSON.parse(rawText);
1180
+ } catch {
1181
+ throw new Error("Haven credentials must be JSON with a delegate_key field.");
1182
+ }
1183
+ const delegateKey = stringField(raw.delegate_key ?? raw.delegateKey);
1184
+ if (!delegateKey) {
1185
+ throw new Error("Haven credentials are missing delegate_key \u2014 the edge signer needs it to sign.");
1186
+ }
1187
+ return {
1188
+ delegateKey,
1189
+ agentId: stringField(raw.agent_id ?? raw.agentId),
1190
+ safeAddress: stringField(raw.safe_address ?? raw.safeAddress),
1191
+ chainId: chainIdField(raw.chain_id ?? raw.chainId, "chain_id"),
1192
+ network: stringField(raw.network),
1193
+ x402BindingSigner: stringField(
1194
+ raw.x402_binding_signer ?? raw.x402BindingSigner ?? process.env.HAVEN_X402_BINDING_SIGNER
1195
+ ),
1196
+ sourcePath: path
1197
+ };
1198
+ }
1199
+ function stringField(value) {
1200
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1201
+ }
1202
+ function chainIdField(value, label) {
1203
+ if (value === void 0 || value === null) return void 0;
1204
+ if (typeof value === "number") {
1205
+ if (Number.isSafeInteger(value) && value > 0) return value;
1206
+ throw invalidChainIdError(label);
1207
+ }
1208
+ if (typeof value === "string") {
1209
+ const trimmed = value.trim();
1210
+ if (/^[1-9]\d*$/.test(trimmed)) {
1211
+ const parsed = Number(trimmed);
1212
+ if (Number.isSafeInteger(parsed)) return parsed;
1213
+ }
1214
+ throw invalidChainIdError(label);
1215
+ }
1216
+ throw invalidChainIdError(label);
1217
+ }
1218
+ function invalidChainIdError(label) {
1219
+ return new Error(`Haven signer credentials ${label} must be a positive integer.`);
1220
+ }
1221
+ async function warnIfCredentialFilePermissive(path, log = (message) => process.stderr.write(`${message}
1222
+ `), platform = process.platform) {
1223
+ if (platform === "win32") return;
1224
+ let mode;
1225
+ try {
1226
+ mode = (await promises.stat(path)).mode;
1227
+ } catch {
1228
+ return;
1229
+ }
1230
+ if ((mode & 63) !== 0) {
1231
+ const octal = (mode & 511).toString(8).padStart(4, "0");
1232
+ log(
1233
+ `haven-signer: warning: credential file at ${path} is readable beyond the owner (mode ${octal}). Run: chmod 600 ${path}`
1234
+ );
1235
+ }
1236
+ }
1237
+
1238
+ // src/server.ts
1239
+ var SIGNER_NAME = "@haven_ai/signer";
1240
+ var SIGNER_VERSION = "0.0.0-dev.202609031523.fd49e1a";
1241
+ async function resolveSignerRuntime(options = {}) {
1242
+ assertSupportedNodeVersion(options.nodeVersion);
1243
+ if (options.delegateKey) {
1244
+ return {
1245
+ signer: createEdgeSigner(options.delegateKey, {
1246
+ x402BindingSigner: options.x402BindingSigner ?? process.env.HAVEN_X402_BINDING_SIGNER
1247
+ })
1248
+ };
1249
+ }
1250
+ const creds = await loadSignerCredentials(options.credentialsPath);
1251
+ return {
1252
+ signer: createEdgeSigner(creds.delegateKey, {
1253
+ x402BindingSigner: options.x402BindingSigner ?? creds.x402BindingSigner ?? process.env.HAVEN_X402_BINDING_SIGNER,
1254
+ // #1690: the signer's own agent id, so a payer-mismatch refusal can name
1255
+ // both sides. The bare-delegateKey path above has no credential file and
1256
+ // therefore no agent id — the guard still works there, on addresses.
1257
+ agentId: creds.agentId
1258
+ }),
1259
+ credentials: creds
1260
+ };
1261
+ }
1262
+ function buildSignerMcpServer(signer, options = {}) {
1263
+ const server = new mcp_js.McpServer(
1264
+ { name: SIGNER_NAME, version: SIGNER_VERSION },
1265
+ {
1266
+ capabilities: signerCapabilityAdvertisement(),
1267
+ instructions: signerInstructions()
1268
+ }
1269
+ );
1270
+ const credentialsPath = options.credentials?.sourcePath;
1271
+ const handlers = createToolHandlers(signer, {
1272
+ audit: {
1273
+ auditPath: options.auditPath ?? defaultSigningAuditPath(credentialsPath),
1274
+ delegateAddress: signer.delegateAddress,
1275
+ safeAddress: options.credentials?.safeAddress,
1276
+ chainId: options.credentials?.chainId
1277
+ },
1278
+ // #1263: the payment_id signing path — the ONLY network call this server
1279
+ // can make, an authenticated read of a signing context from Haven, using
1280
+ // the agent identity the connector stores next to the signer credential.
1281
+ // The signer CORE stays network-free; fetched bytes still pass the same
1282
+ // binding verification + digest re-derivation as tool-argument bytes.
1283
+ signContext: {
1284
+ loadIdentity: () => loadHavenIdentity(credentialsPath)
1285
+ }
1286
+ });
1287
+ const registerTool = server.tool.bind(server);
1288
+ for (const name of Object.keys(toolSchemas)) {
1289
+ registerTool(
1290
+ name,
1291
+ toolDescriptions[name],
1292
+ toolSchemas[name],
1293
+ async (args) => toMcpResult(await handlers[name](args))
1294
+ );
1295
+ }
1296
+ return server;
1297
+ }
1298
+ function assertSupportedNodeVersion(nodeVersion = process.versions.node) {
1299
+ if (sdk.isSupportedNodeVersion(nodeVersion)) return;
1300
+ const err = new Error(
1301
+ sdk.unsupportedNodeVersionMessage({ subject: "The Haven signer", nodeVersion })
1302
+ );
1303
+ err.code = "HAVEN_SIGNER_UNSUPPORTED_NODE";
1304
+ throw err;
1305
+ }
1306
+ async function runSignerStdioServer(options = {}) {
1307
+ const { signer, credentials } = await resolveSignerRuntime(options);
1308
+ if (!options.skipConsent) {
1309
+ const decision = await runSignerConsentGate(signer, credentials, options);
1310
+ if (!decision.ok) {
1311
+ const err = new Error(
1312
+ decision.reason === "env_var_mismatch" ? "Haven edge signer consent acknowledgement does not match the current configuration." : "Haven edge signer requires a one-time consent acknowledgement before starting."
1313
+ );
1314
+ err.code = "HAVEN_SIGNER_NO_CONSENT";
1315
+ throw err;
1316
+ }
1317
+ }
1318
+ const server = buildSignerMcpServer(signer, { credentials, auditPath: options.auditPath });
1319
+ await server.connect(new stdio_js.StdioServerTransport());
1320
+ }
1321
+ async function runSignerConsentGate(signer, credentials, options) {
1322
+ return ensureSignerConsent(
1323
+ {
1324
+ delegateAddress: signer.delegateAddress,
1325
+ safeAddress: credentials?.safeAddress,
1326
+ agentId: credentials?.agentId,
1327
+ chainId: credentials?.chainId,
1328
+ network: credentials?.network,
1329
+ toolNames: registeredSignerToolNames()
1330
+ },
1331
+ {
1332
+ credentialsPath: options.credentialsPath ?? credentials?.sourcePath,
1333
+ writeAck: options.writeAck,
1334
+ env: options.consentEnv,
1335
+ out: options.consentOut
1336
+ }
1337
+ );
1338
+ }
1339
+ function toMcpResult(payload) {
1340
+ return {
1341
+ isError: !payload.success,
1342
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
1343
+ };
1344
+ }
1345
+
1346
+ // src/cli.ts
1347
+ function parseArgs(argv) {
1348
+ const options = {};
1349
+ for (let i = 0; i < argv.length; i += 1) {
1350
+ const arg = argv[i];
1351
+ if (arg === "--credentials" || arg === "--credentials-path") {
1352
+ options.credentialsPath = argv[i + 1];
1353
+ i += 1;
1354
+ } else if (arg === "--ack") {
1355
+ options.writeAck = true;
1356
+ } else if (arg === "--help" || arg === "-h") {
1357
+ process.stdout.write(
1358
+ [
1359
+ "Haven edge signer (local, holds the delegate key)",
1360
+ "",
1361
+ "Runs a local stdio MCP server exposing sign-only tools (haven_sign,",
1362
+ "haven_x402_sign_header). Pair it with the hosted, keyless Haven MCP",
1363
+ "server: the hosted server constructs and relays, this one signs.",
1364
+ "",
1365
+ "Usage:",
1366
+ " npx @haven_ai/signer --credentials /path/to/agent.json",
1367
+ "",
1368
+ "Options:",
1369
+ " --credentials <path> Haven credential JSON (delegate_key is read from it).",
1370
+ " Also supported: HAVEN_CREDENTIALS, or HAVEN_DELEGATE_KEY.",
1371
+ " --ack Acknowledge the first-launch consent block and write",
1372
+ " a signer sidecar acknowledgement next to the credential.",
1373
+ "",
1374
+ "Consent:",
1375
+ " On first launch the signer prints the sign-only tool list and delegate",
1376
+ " address, then refuses to start unless acknowledged. Its only Haven API call",
1377
+ " is a read-only fetch of the signing payload for one pending payment, so it",
1378
+ " cannot show a live allowance summary.",
1379
+ " Acknowledge with EITHER --ack OR HAVEN_SIGNER_ACK=<hash> in your environment.",
1380
+ ""
1381
+ ].join("\n")
1382
+ );
1383
+ process.exit(0);
1384
+ }
1385
+ }
1386
+ return options;
1387
+ }
1388
+ async function main() {
1389
+ await runSignerStdioServer(parseArgs(process.argv.slice(2)));
1390
+ }
1391
+ main().catch((err) => {
1392
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
1393
+ `);
1394
+ process.exit(1);
1395
+ });
1396
+ //# sourceMappingURL=cli.cjs.map
1397
+ //# sourceMappingURL=cli.cjs.map