@muhaven/mcp 0.2.1 → 0.2.3

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/broker.js CHANGED
@@ -3,8 +3,9 @@ import { fileURLToPath } from 'url';
3
3
  import { platform, release, hostname, homedir } from 'os';
4
4
  import { exec, spawn } from 'child_process';
5
5
  import { connect, createServer } from 'net';
6
- import { mkdir, chmod, writeFile, readFile, unlink, stat } from 'fs/promises';
6
+ import { mkdir, chmod, writeFile, readFile, unlink, rename, readdir, stat } from 'fs/promises';
7
7
  import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
8
+ import { randomBytes } from 'crypto';
8
9
 
9
10
  var getFilename = () => fileURLToPath(import.meta.url);
10
11
  var getDirname = () => path.dirname(getFilename());
@@ -15,6 +16,10 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
15
16
  var DEFAULT_BROKER_TIMEOUT_MS = 5e3;
16
17
  var DEFAULT_BROKER_MAX_BYTES = 64 * 1024;
17
18
  var DEFAULT_JWT_CACHE_TTL_SEC = 30;
19
+ var DEFAULT_BUNDLER_TIMEOUT_MS = 2e4;
20
+ var DEFAULT_CHAIN_ID = 421614;
21
+ var DEFAULT_ENTRY_POINT_ADDRESS = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
22
+ var ADDRESS_HEX_RE = /^0x[0-9a-fA-F]{40}$/;
18
23
  function defaultBrokerEndpoint() {
19
24
  if (platform() === "win32") {
20
25
  const user = process.env.USERNAME ?? "default";
@@ -90,6 +95,35 @@ function loadMcpConfig(env = process.env) {
90
95
  const requestTimeoutMs = readEnvInt("MUHAVEN_REQUEST_TIMEOUT_MS", DEFAULT_REQUEST_TIMEOUT_MS, env);
91
96
  const brokerTimeoutMs = readEnvInt("MUHAVEN_BROKER_TIMEOUT_MS", DEFAULT_BROKER_TIMEOUT_MS, env);
92
97
  const jwtCacheTtlSec = readEnvInt("MUHAVEN_JWT_CACHE_TTL_SEC", DEFAULT_JWT_CACHE_TTL_SEC, env);
98
+ const bundlerUrlRaw = readEnv("MUHAVEN_BUNDLER_URL", env);
99
+ let bundlerUrl;
100
+ if (bundlerUrlRaw !== void 0) {
101
+ const validationErr = validatePublicUrlEnv("MUHAVEN_BUNDLER_URL", bundlerUrlRaw);
102
+ if (validationErr) throw new Error(validationErr);
103
+ bundlerUrl = trimTrailingSlash(bundlerUrlRaw);
104
+ }
105
+ const bundlerTimeoutMs = readEnvInt("MUHAVEN_BUNDLER_TIMEOUT_MS", DEFAULT_BUNDLER_TIMEOUT_MS, env);
106
+ const chainId = readEnvInt("MUHAVEN_CHAIN_ID", DEFAULT_CHAIN_ID, env);
107
+ const subscriptionAddressRaw = readEnv("MUHAVEN_SUBSCRIPTION_ADDRESS", env);
108
+ let subscriptionAddress;
109
+ if (subscriptionAddressRaw !== void 0) {
110
+ if (!ADDRESS_HEX_RE.test(subscriptionAddressRaw)) {
111
+ throw new Error(
112
+ `MUHAVEN_SUBSCRIPTION_ADDRESS must be a 0x-prefixed 20-byte hex string (got ${JSON.stringify(subscriptionAddressRaw)})`
113
+ );
114
+ }
115
+ subscriptionAddress = subscriptionAddressRaw.toLowerCase();
116
+ }
117
+ const entryPointAddressRaw = readEnv("MUHAVEN_ENTRY_POINT", env);
118
+ let entryPointAddress = DEFAULT_ENTRY_POINT_ADDRESS;
119
+ if (entryPointAddressRaw !== void 0) {
120
+ if (!ADDRESS_HEX_RE.test(entryPointAddressRaw)) {
121
+ throw new Error(
122
+ `MUHAVEN_ENTRY_POINT must be a 0x-prefixed 20-byte hex string (got ${JSON.stringify(entryPointAddressRaw)})`
123
+ );
124
+ }
125
+ entryPointAddress = entryPointAddressRaw.toLowerCase();
126
+ }
93
127
  return {
94
128
  backendBaseUrl,
95
129
  dashboardBaseUrl,
@@ -98,7 +132,12 @@ function loadMcpConfig(env = process.env) {
98
132
  requestTimeoutMs,
99
133
  brokerTimeoutMs,
100
134
  allowedBackendHosts: deriveAllowedHosts(backendBaseUrl),
101
- jwtCacheTtlSec
135
+ jwtCacheTtlSec,
136
+ bundlerUrl,
137
+ bundlerTimeoutMs,
138
+ chainId,
139
+ subscriptionAddress,
140
+ entryPointAddress
102
141
  };
103
142
  }
104
143
  var PRIVKEY_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
@@ -133,16 +172,366 @@ function loadBrokerConfig(env = process.env) {
133
172
  dashboardBaseUrl
134
173
  };
135
174
  }
175
+
176
+ // src/broker/protocol.ts
177
+ var BROKER_PROTOCOL_VERSION = "0.4.0";
178
+ var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
179
+ var ADDRESS_HEX_RE2 = /^0x[0-9a-fA-F]{40}$/;
180
+ var SELECTOR_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
181
+ var HEX_PREFIXED_RE = /^0x[0-9a-fA-F]*$/;
182
+ var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
183
+ var SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
184
+ var UINT256_DEC_RE = /^(0|[1-9][0-9]{0,77})$/;
185
+ var MAX_CALLDATA_HEX_LEN = 2e5;
186
+ function isHashHex(value) {
187
+ return typeof value === "string" && HASH_HEX_RE.test(value);
188
+ }
189
+ function isAddressHex(value) {
190
+ return typeof value === "string" && ADDRESS_HEX_RE2.test(value);
191
+ }
192
+ function isSelectorHex(value) {
193
+ return typeof value === "string" && SELECTOR_HEX_RE.test(value);
194
+ }
195
+ function isHexPrefixed(value) {
196
+ return typeof value === "string" && HEX_PREFIXED_RE.test(value);
197
+ }
198
+ function isJwtShape(value) {
199
+ return typeof value === "string" && value.length <= 8192 && JWT_RE.test(value);
200
+ }
201
+ function isSessionIdShape(value) {
202
+ return typeof value === "string" && SESSION_ID_RE.test(value);
203
+ }
204
+ function isUint256DecString(value) {
205
+ return typeof value === "string" && UINT256_DEC_RE.test(value);
206
+ }
207
+ var UINT256_MAX = (1n << 256n) - 1n;
208
+ function isUint256InRange(value) {
209
+ if (!isUint256DecString(value)) return false;
210
+ return BigInt(value) <= UINT256_MAX;
211
+ }
212
+ function parseSelectorCap(raw) {
213
+ if (typeof raw !== "object" || raw === null) {
214
+ return { error: "selectorCap must be an object" };
215
+ }
216
+ const obj = raw;
217
+ if (!isSelectorHex(obj.selector)) {
218
+ return { error: "selectorCap.selector must be a 4-byte 0x-hex" };
219
+ }
220
+ const capArgIndex = obj.capArgIndex;
221
+ const maxAmount = obj.maxAmount;
222
+ const indexIsNull = capArgIndex === null;
223
+ const amountIsNull = maxAmount === null;
224
+ if (indexIsNull !== amountIsNull) {
225
+ return {
226
+ error: "selectorCap.capArgIndex and selectorCap.maxAmount must both be null or both non-null"
227
+ };
228
+ }
229
+ if (!indexIsNull) {
230
+ if (typeof capArgIndex !== "number" || !Number.isInteger(capArgIndex) || capArgIndex < 0 || capArgIndex > 31) {
231
+ return {
232
+ error: "selectorCap.capArgIndex must be an integer in [0, 31] (max 32 ABI words)"
233
+ };
234
+ }
235
+ if (typeof maxAmount !== "string" || !isUint256InRange(maxAmount)) {
236
+ return { error: "selectorCap.maxAmount must be a uint256 decimal string \u2264 2^256-1" };
237
+ }
238
+ }
239
+ return {
240
+ selector: obj.selector.toLowerCase(),
241
+ capArgIndex: indexIsNull ? null : capArgIndex,
242
+ maxAmount: indexIsNull ? null : maxAmount
243
+ };
244
+ }
245
+ function isOptionalHash32(value) {
246
+ return value === void 0 || isHashHex(value);
247
+ }
248
+ function isOptionalPermissionId(value) {
249
+ return value === void 0 || isSelectorHex(value);
250
+ }
251
+ function parsePolicySnapshot(raw) {
252
+ if (typeof raw !== "object" || raw === null) {
253
+ return { error: "snapshot must be a JSON object" };
254
+ }
255
+ const obj = raw;
256
+ if (!isSessionIdShape(obj.sessionId)) {
257
+ return { error: "snapshot.sessionId must be 1-128 chars [A-Za-z0-9_-]" };
258
+ }
259
+ if (obj.mode !== "scoped") {
260
+ return { error: "snapshot.mode must be 'scoped' (wildcard ships in Slice 4)" };
261
+ }
262
+ if (!isAddressHex(obj.signerAddress)) {
263
+ return { error: "snapshot.signerAddress must be a 0x-prefixed 20-byte hex" };
264
+ }
265
+ const targetContracts = obj.targetContracts;
266
+ if (!Array.isArray(targetContracts) || targetContracts.length === 0 || targetContracts.length > 32 || !targetContracts.every(isAddressHex)) {
267
+ return {
268
+ error: "snapshot.targetContracts must be a 1..32-element array of 0x-addresses"
269
+ };
270
+ }
271
+ const rawCaps = obj.selectorCaps;
272
+ if (!Array.isArray(rawCaps) || rawCaps.length === 0 || rawCaps.length > 32) {
273
+ return { error: "snapshot.selectorCaps must be a 1..32-element array" };
274
+ }
275
+ const parsedCaps = [];
276
+ const seenSelectors = /* @__PURE__ */ new Set();
277
+ for (const c of rawCaps) {
278
+ const parsed = parseSelectorCap(c);
279
+ if ("error" in parsed) return { error: `selectorCaps: ${parsed.error}` };
280
+ if (seenSelectors.has(parsed.selector)) {
281
+ return { error: `selectorCaps: duplicate selector ${parsed.selector}` };
282
+ }
283
+ seenSelectors.add(parsed.selector);
284
+ parsedCaps.push(parsed);
285
+ }
286
+ if (typeof obj.validUntilSec !== "number" || !Number.isFinite(obj.validUntilSec) || obj.validUntilSec <= 0) {
287
+ return { error: "snapshot.validUntilSec must be a positive number" };
288
+ }
289
+ if (typeof obj.mintedAtSec !== "number" || !Number.isFinite(obj.mintedAtSec) || obj.mintedAtSec <= 0) {
290
+ return { error: "snapshot.mintedAtSec must be a positive number" };
291
+ }
292
+ if (!isOptionalHash32(obj.consentActionHash)) {
293
+ return { error: "snapshot.consentActionHash must be a 0x-prefixed 32-byte hex when provided" };
294
+ }
295
+ if (!isOptionalHash32(obj.consentTextSha256)) {
296
+ return { error: "snapshot.consentTextSha256 must be a 0x-prefixed 32-byte hex when provided" };
297
+ }
298
+ if (!isOptionalPermissionId(obj.permissionId)) {
299
+ return { error: "snapshot.permissionId must be a 0x-prefixed 4-byte hex when provided" };
300
+ }
301
+ return {
302
+ sessionId: obj.sessionId,
303
+ mode: "scoped",
304
+ signerAddress: obj.signerAddress.toLowerCase(),
305
+ targetContracts: targetContracts.map(
306
+ (a) => a.toLowerCase()
307
+ ),
308
+ selectorCaps: parsedCaps,
309
+ validUntilSec: obj.validUntilSec,
310
+ mintedAtSec: obj.mintedAtSec,
311
+ ...obj.consentActionHash === void 0 ? {} : { consentActionHash: obj.consentActionHash.toLowerCase() },
312
+ ...obj.consentTextSha256 === void 0 ? {} : { consentTextSha256: obj.consentTextSha256.toLowerCase() },
313
+ ...obj.permissionId === void 0 ? {} : { permissionId: obj.permissionId.toLowerCase() }
314
+ };
315
+ }
316
+ function parseBrokerRequest(line) {
317
+ let parsed;
318
+ try {
319
+ parsed = JSON.parse(line);
320
+ } catch {
321
+ return { type: "error", code: "invalid_request", message: "request is not valid JSON" };
322
+ }
323
+ if (typeof parsed !== "object" || parsed === null) {
324
+ return { type: "error", code: "invalid_request", message: "request must be a JSON object" };
325
+ }
326
+ const obj = parsed;
327
+ switch (obj.type) {
328
+ case "hello":
329
+ return { type: "hello" };
330
+ case "sign_hash": {
331
+ const hash = obj.hash;
332
+ if (!isHashHex(hash)) {
333
+ return {
334
+ type: "error",
335
+ code: "invalid_request",
336
+ message: "sign_hash.hash must be a 0x-prefixed 32-byte hex string"
337
+ };
338
+ }
339
+ const intent = obj.intent;
340
+ const intentValid = intent === void 0 || typeof intent === "object" && intent !== null && typeof intent.tool === "string";
341
+ if (!intentValid) {
342
+ return {
343
+ type: "error",
344
+ code: "invalid_request",
345
+ message: "sign_hash.intent.tool must be a string when provided"
346
+ };
347
+ }
348
+ return {
349
+ type: "sign_hash",
350
+ hash,
351
+ ...intent === void 0 ? {} : { intent }
352
+ };
353
+ }
354
+ case "store_jwt": {
355
+ const jwt = obj.jwt;
356
+ if (!isJwtShape(jwt)) {
357
+ return {
358
+ type: "error",
359
+ code: "invalid_request",
360
+ message: "store_jwt.jwt must be a JWT-shaped string \u22648192 chars"
361
+ };
362
+ }
363
+ const expiresAtSec = obj.expiresAtSec;
364
+ const expiresValid = expiresAtSec === void 0 || typeof expiresAtSec === "number" && Number.isFinite(expiresAtSec) && expiresAtSec > 0;
365
+ if (!expiresValid) {
366
+ return {
367
+ type: "error",
368
+ code: "invalid_request",
369
+ message: "store_jwt.expiresAtSec must be a positive number when provided"
370
+ };
371
+ }
372
+ return {
373
+ type: "store_jwt",
374
+ jwt,
375
+ ...expiresAtSec === void 0 ? {} : { expiresAtSec }
376
+ };
377
+ }
378
+ case "get_jwt":
379
+ return { type: "get_jwt" };
380
+ case "clear_jwt":
381
+ return { type: "clear_jwt" };
382
+ case "sign_userop": {
383
+ const sessionId = obj.sessionId;
384
+ if (!isSessionIdShape(sessionId)) {
385
+ return {
386
+ type: "error",
387
+ code: "invalid_request",
388
+ message: "sign_userop.sessionId must be 1-128 chars [A-Za-z0-9_-]"
389
+ };
390
+ }
391
+ const userOpHash = obj.userOpHash;
392
+ if (!isHashHex(userOpHash)) {
393
+ return {
394
+ type: "error",
395
+ code: "invalid_request",
396
+ message: "sign_userop.userOpHash must be a 0x-prefixed 32-byte hex string"
397
+ };
398
+ }
399
+ const innerCall = obj.innerCall;
400
+ if (typeof innerCall !== "object" || innerCall === null) {
401
+ return {
402
+ type: "error",
403
+ code: "invalid_request",
404
+ message: "sign_userop.innerCall must be an object with { target, callData }"
405
+ };
406
+ }
407
+ const ic = innerCall;
408
+ if (!isAddressHex(ic.target)) {
409
+ return {
410
+ type: "error",
411
+ code: "invalid_request",
412
+ message: "sign_userop.innerCall.target must be a 0x-prefixed 20-byte hex"
413
+ };
414
+ }
415
+ if (!isHexPrefixed(ic.callData) || ic.callData.length < 74 || ic.callData.length % 2 !== 0 || ic.callData.length > MAX_CALLDATA_HEX_LEN) {
416
+ return {
417
+ type: "error",
418
+ code: "invalid_request",
419
+ message: "sign_userop.innerCall.callData must be 0x-prefixed even-length hex \u226574 chars (selector + first uint256 arg) and \u2264200000 chars"
420
+ };
421
+ }
422
+ const intent = obj.intent;
423
+ let safeIntent;
424
+ if (intent !== void 0) {
425
+ if (typeof intent !== "object" || intent === null) {
426
+ return {
427
+ type: "error",
428
+ code: "invalid_request",
429
+ message: "sign_userop.intent must be an object when provided"
430
+ };
431
+ }
432
+ const intentObj = intent;
433
+ if (typeof intentObj.tool !== "string" || intentObj.tool.length > 64) {
434
+ return {
435
+ type: "error",
436
+ code: "invalid_request",
437
+ message: "sign_userop.intent.tool must be a string \u226464 chars"
438
+ };
439
+ }
440
+ if (intentObj.summary !== void 0 && (typeof intentObj.summary !== "string" || intentObj.summary.length > 256)) {
441
+ return {
442
+ type: "error",
443
+ code: "invalid_request",
444
+ message: "sign_userop.intent.summary must be a string \u2264256 chars when provided"
445
+ };
446
+ }
447
+ safeIntent = {
448
+ tool: intentObj.tool,
449
+ ...typeof intentObj.summary === "string" ? { summary: intentObj.summary } : {}
450
+ };
451
+ }
452
+ return {
453
+ type: "sign_userop",
454
+ sessionId,
455
+ userOpHash,
456
+ innerCall: {
457
+ target: ic.target.toLowerCase(),
458
+ callData: ic.callData.toLowerCase()
459
+ },
460
+ ...safeIntent === void 0 ? {} : { intent: safeIntent }
461
+ };
462
+ }
463
+ case "store_policy_snapshot": {
464
+ const parsed2 = parsePolicySnapshot(obj.snapshot);
465
+ if ("error" in parsed2) {
466
+ return { type: "error", code: "invalid_request", message: parsed2.error };
467
+ }
468
+ return { type: "store_policy_snapshot", snapshot: parsed2 };
469
+ }
470
+ case "get_policy_snapshot": {
471
+ if (!isSessionIdShape(obj.sessionId)) {
472
+ return {
473
+ type: "error",
474
+ code: "invalid_request",
475
+ message: "get_policy_snapshot.sessionId must be 1-128 chars [A-Za-z0-9_-]"
476
+ };
477
+ }
478
+ return { type: "get_policy_snapshot", sessionId: obj.sessionId };
479
+ }
480
+ case "clear_policy_snapshot": {
481
+ if (!isSessionIdShape(obj.sessionId)) {
482
+ return {
483
+ type: "error",
484
+ code: "invalid_request",
485
+ message: "clear_policy_snapshot.sessionId must be 1-128 chars [A-Za-z0-9_-]"
486
+ };
487
+ }
488
+ return { type: "clear_policy_snapshot", sessionId: obj.sessionId };
489
+ }
490
+ case "get_active_session_id":
491
+ return { type: "get_active_session_id" };
492
+ default:
493
+ return {
494
+ type: "error",
495
+ code: "unsupported_type",
496
+ message: `unsupported request type: ${String(obj.type)}`
497
+ };
498
+ }
499
+ }
500
+ function serializeResponse(res) {
501
+ return JSON.stringify(res) + "\n";
502
+ }
503
+
504
+ // src/clients/broker-client.ts
136
505
  var BrokerClientError = class extends Error {
137
- constructor(code, message, cause) {
506
+ constructor(code, message, cause, brokerCode) {
138
507
  super(message);
139
508
  this.code = code;
140
509
  this.cause = cause;
510
+ this.brokerCode = brokerCode;
141
511
  this.name = "BrokerClientError";
142
512
  }
143
513
  code;
144
514
  cause;
515
+ brokerCode;
145
516
  };
517
+ function semverGte(a, b) {
518
+ const re = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
519
+ const ma = re.exec(a);
520
+ const mb = re.exec(b);
521
+ if (!ma || !mb) {
522
+ throw new BrokerClientError(
523
+ "protocol_error",
524
+ `semverGte: malformed version (got ${JSON.stringify(a)} vs ${JSON.stringify(b)})`
525
+ );
526
+ }
527
+ for (let i = 1; i <= 3; i++) {
528
+ const ai = Number(ma[i]);
529
+ const bi = Number(mb[i]);
530
+ if (ai > bi) return true;
531
+ if (ai < bi) return false;
532
+ }
533
+ return true;
534
+ }
146
535
  var BrokerClient = class {
147
536
  constructor(options) {
148
537
  this.options = options;
@@ -202,6 +591,109 @@ var BrokerClient = class {
202
591
  );
203
592
  }
204
593
  }
594
+ // ── Wave 5 Path D Slice 1 (Commit 3) — policy snapshot CRUD + sign_userop ──
595
+ async signUserOp(args) {
596
+ const res = await this.exchange({
597
+ type: "sign_userop",
598
+ sessionId: args.sessionId,
599
+ userOpHash: args.userOpHash,
600
+ innerCall: args.innerCall,
601
+ ...args.intent ? { intent: args.intent } : {}
602
+ });
603
+ if (res.type !== "sign_userop") {
604
+ throw new BrokerClientError(
605
+ "protocol_error",
606
+ `expected sign_userop response, got ${res.type}`
607
+ );
608
+ }
609
+ return res;
610
+ }
611
+ async storePolicySnapshot(snapshot) {
612
+ const res = await this.exchange({ type: "store_policy_snapshot", snapshot });
613
+ if (res.type !== "store_policy_snapshot") {
614
+ throw new BrokerClientError(
615
+ "protocol_error",
616
+ `expected store_policy_snapshot response, got ${res.type}`
617
+ );
618
+ }
619
+ return res;
620
+ }
621
+ async getPolicySnapshot(sessionId) {
622
+ const res = await this.exchange({ type: "get_policy_snapshot", sessionId });
623
+ if (res.type !== "get_policy_snapshot") {
624
+ throw new BrokerClientError(
625
+ "protocol_error",
626
+ `expected get_policy_snapshot response, got ${res.type}`
627
+ );
628
+ }
629
+ return res;
630
+ }
631
+ async clearPolicySnapshot(sessionId) {
632
+ const res = await this.exchange({ type: "clear_policy_snapshot", sessionId });
633
+ if (res.type !== "clear_policy_snapshot") {
634
+ throw new BrokerClientError(
635
+ "protocol_error",
636
+ `expected clear_policy_snapshot response, got ${res.type}`
637
+ );
638
+ }
639
+ return res;
640
+ }
641
+ async getActiveSessionId() {
642
+ const res = await this.exchange({ type: "get_active_session_id" });
643
+ if (res.type !== "get_active_session_id") {
644
+ throw new BrokerClientError(
645
+ "protocol_error",
646
+ `expected get_active_session_id response, got ${res.type}`
647
+ );
648
+ }
649
+ return res;
650
+ }
651
+ /**
652
+ * Detect whether the running daemon speaks Path D (protocol 0.4.0+).
653
+ * Wraps `hello()` with a semver-gte comparison so the MCP tool layer
654
+ * can short-circuit to Path C with a clear `version_too_old` reason
655
+ * instead of surfacing the opaque `unsupported_type` error a stale
656
+ * 0.3.0 daemon would emit on `sign_userop` / `get_active_session_id`
657
+ * (Backend Architect H-2, round 2).
658
+ *
659
+ * Returns `{ supported: false }` on broker connect failure too — the
660
+ * caller treats "daemon down" identically to "version too old": Path D
661
+ * not available, fall through to Path C.
662
+ */
663
+ async preflight() {
664
+ let hello;
665
+ try {
666
+ hello = await this.hello();
667
+ } catch (err) {
668
+ return {
669
+ supported: false,
670
+ reason: "broker_unreachable",
671
+ message: err instanceof BrokerClientError ? `broker.${err.code}: ${err.message}` : err instanceof Error ? err.message : "broker unreachable",
672
+ requiredVersion: BROKER_PROTOCOL_VERSION
673
+ };
674
+ }
675
+ if (!semverGte(hello.version, BROKER_PROTOCOL_VERSION)) {
676
+ return {
677
+ supported: false,
678
+ reason: "version_too_old",
679
+ daemonVersion: hello.version,
680
+ requiredVersion: BROKER_PROTOCOL_VERSION
681
+ };
682
+ }
683
+ if (hello.hasSessionKey === false) {
684
+ return {
685
+ supported: false,
686
+ reason: "session_key_unavailable",
687
+ daemonVersion: hello.version,
688
+ requiredVersion: BROKER_PROTOCOL_VERSION
689
+ };
690
+ }
691
+ return {
692
+ supported: true,
693
+ daemonVersion: hello.version,
694
+ signerAddress: hello.sessionKeyAddress
695
+ };
696
+ }
205
697
  exchange(request) {
206
698
  return new Promise((resolve, reject) => {
207
699
  let socket;
@@ -242,7 +734,12 @@ var BrokerClient = class {
242
734
  const parsed = JSON.parse(line);
243
735
  if (parsed.type === "error") {
244
736
  settleErr(
245
- new BrokerClientError("broker_error", `${parsed.code}: ${parsed.message}`)
737
+ new BrokerClientError(
738
+ "broker_error",
739
+ `${parsed.code}: ${parsed.message}`,
740
+ void 0,
741
+ parsed.code
742
+ )
246
743
  );
247
744
  return;
248
745
  }
@@ -406,6 +903,71 @@ async function safeJson(res) {
406
903
  return null;
407
904
  }
408
905
  }
906
+
907
+ // src/auth/jwt-decode.ts
908
+ var JwtDecodeError = class extends Error {
909
+ code;
910
+ constructor(code, message) {
911
+ super(message);
912
+ this.name = "JwtDecodeError";
913
+ this.code = code;
914
+ }
915
+ };
916
+ function decodeJwtPayload(jwt) {
917
+ const segments = jwt.split(".");
918
+ if (segments.length !== 3) {
919
+ throw new JwtDecodeError(
920
+ "malformed_segments",
921
+ `expected 3 dot-separated segments, got ${segments.length}`
922
+ );
923
+ }
924
+ const payloadSegment = segments[1];
925
+ if (payloadSegment === void 0) {
926
+ throw new JwtDecodeError("malformed_segments", "payload segment missing");
927
+ }
928
+ let payloadJson;
929
+ try {
930
+ payloadJson = Buffer.from(payloadSegment, "base64url").toString("utf8");
931
+ } catch (err) {
932
+ throw new JwtDecodeError(
933
+ "malformed_base64",
934
+ `payload segment is not valid base64url: ${err instanceof Error ? err.message : String(err)}`
935
+ );
936
+ }
937
+ let parsed;
938
+ try {
939
+ parsed = JSON.parse(payloadJson);
940
+ } catch (err) {
941
+ throw new JwtDecodeError(
942
+ "malformed_json",
943
+ `payload is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
944
+ );
945
+ }
946
+ if (parsed === null || typeof parsed !== "object") {
947
+ throw new JwtDecodeError(
948
+ "malformed_json",
949
+ `payload is not a JSON object (got ${parsed === null ? "null" : typeof parsed})`
950
+ );
951
+ }
952
+ const obj = parsed;
953
+ return {
954
+ sub: typeof obj.sub === "string" ? obj.sub : null,
955
+ scope: Array.isArray(obj.scope) ? obj.scope.filter((s) => typeof s === "string") : [],
956
+ expSec: typeof obj.exp === "number" ? obj.exp : null,
957
+ iss: typeof obj.iss === "string" ? obj.iss : null
958
+ };
959
+ }
960
+ function sanitizeClaimForTerminal(value, maxLen = 120) {
961
+ if (!value) return "";
962
+ const stripped = value.replace(/[^\x20-\x7e]/g, "?");
963
+ return stripped.length > maxLen ? `${stripped.slice(0, maxLen)}\u2026(truncated)` : stripped;
964
+ }
965
+ function truncateSubject(sub) {
966
+ if (!sub) return "(missing)";
967
+ const sanitized = sub.replace(/[^\x20-\x7e]/g, "?");
968
+ if (sanitized.length <= 12) return sanitized;
969
+ return `${sanitized.slice(0, 8)}\u2026${sanitized.slice(-4)}`;
970
+ }
409
971
  var KEYRING_SERVICE = "muhaven.mcp";
410
972
  var KEYRING_ACCOUNT = "jwt";
411
973
  async function loadKeyringModule() {
@@ -500,154 +1062,66 @@ var KeystoreError = class extends Error {
500
1062
  this.name = "KeystoreError";
501
1063
  }
502
1064
  code;
503
- cause;
504
- };
505
- function parseRecord(raw) {
506
- try {
507
- const parsed = JSON.parse(raw);
508
- if (!parsed || typeof parsed.jwt !== "string") return null;
509
- return {
510
- jwt: parsed.jwt,
511
- expiresAtSec: typeof parsed.expiresAtSec === "number" ? parsed.expiresAtSec : null,
512
- storedAtSec: typeof parsed.storedAtSec === "number" ? parsed.storedAtSec : 0
513
- };
514
- } catch {
515
- throw new KeystoreError("malformed_record", "keystore record is not valid JSON");
516
- }
517
- }
518
- function asMessage(err) {
519
- return err instanceof Error ? err.message : String(err);
520
- }
521
- async function openKeystore(options = {}) {
522
- const envPref = process.env.MUHAVEN_KEYRING?.toLowerCase();
523
- const wantFile = options.preferred === "file" || envPref === "file";
524
- const filePath = options.filePath ?? FileKeystore.defaultPath();
525
- if (wantFile) {
526
- return { keystore: new FileKeystore(filePath), fallbackReason: null };
527
- }
528
- const mod = await loadKeyringModule();
529
- if (!mod) {
530
- return {
531
- keystore: new FileKeystore(filePath),
532
- fallbackReason: "@napi-rs/keyring not installed for this platform"
533
- };
534
- }
535
- const Entry = mod.Entry;
536
- if (!Entry) {
537
- return {
538
- keystore: new FileKeystore(filePath),
539
- fallbackReason: "@napi-rs/keyring loaded but Entry constructor missing"
540
- };
541
- }
542
- let entry;
543
- try {
544
- entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
545
- const raw = entry.getPassword();
546
- if (raw && typeof raw === "string") {
547
- const parsed = parseRecord(raw);
548
- if (parsed === null) {
549
- return {
550
- keystore: new FileKeystore(filePath),
551
- fallbackReason: "OS keychain held a record that did not contain a recognizable JWT \u2014 falling back to file. Run `muhaven-broker logout` if you want to clean it up."
552
- };
553
- }
554
- }
555
- } catch (err) {
556
- const isMalformed = err instanceof KeystoreError && err.code === "malformed_record";
557
- return {
558
- keystore: new FileKeystore(filePath),
559
- fallbackReason: isMalformed ? `OS keychain held a malformed JWT record \u2014 falling back to file. ${asMessage(err)}` : `OS keychain probe failed: ${asMessage(err)}`
560
- };
561
- }
562
- return { keystore: new OsKeystore(entry), fallbackReason: null };
563
- }
564
-
565
- // src/broker/protocol.ts
566
- var BROKER_PROTOCOL_VERSION = "0.3.0";
567
- var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
568
- var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
569
- function isHashHex(value) {
570
- return typeof value === "string" && HASH_HEX_RE.test(value);
571
- }
572
- function isJwtShape(value) {
573
- return typeof value === "string" && value.length <= 8192 && JWT_RE.test(value);
574
- }
575
- function parseBrokerRequest(line) {
576
- let parsed;
577
- try {
578
- parsed = JSON.parse(line);
579
- } catch {
580
- return { type: "error", code: "invalid_request", message: "request is not valid JSON" };
581
- }
582
- if (typeof parsed !== "object" || parsed === null) {
583
- return { type: "error", code: "invalid_request", message: "request must be a JSON object" };
584
- }
585
- const obj = parsed;
586
- switch (obj.type) {
587
- case "hello":
588
- return { type: "hello" };
589
- case "sign_hash": {
590
- const hash = obj.hash;
591
- if (!isHashHex(hash)) {
592
- return {
593
- type: "error",
594
- code: "invalid_request",
595
- message: "sign_hash.hash must be a 0x-prefixed 32-byte hex string"
596
- };
597
- }
598
- const intent = obj.intent;
599
- const intentValid = intent === void 0 || typeof intent === "object" && intent !== null && typeof intent.tool === "string";
600
- if (!intentValid) {
601
- return {
602
- type: "error",
603
- code: "invalid_request",
604
- message: "sign_hash.intent.tool must be a string when provided"
605
- };
606
- }
607
- return {
608
- type: "sign_hash",
609
- hash,
610
- ...intent === void 0 ? {} : { intent }
611
- };
612
- }
613
- case "store_jwt": {
614
- const jwt = obj.jwt;
615
- if (!isJwtShape(jwt)) {
616
- return {
617
- type: "error",
618
- code: "invalid_request",
619
- message: "store_jwt.jwt must be a JWT-shaped string \u22648192 chars"
620
- };
621
- }
622
- const expiresAtSec = obj.expiresAtSec;
623
- const expiresValid = expiresAtSec === void 0 || typeof expiresAtSec === "number" && Number.isFinite(expiresAtSec) && expiresAtSec > 0;
624
- if (!expiresValid) {
1065
+ cause;
1066
+ };
1067
+ function parseRecord(raw) {
1068
+ try {
1069
+ const parsed = JSON.parse(raw);
1070
+ if (!parsed || typeof parsed.jwt !== "string") return null;
1071
+ return {
1072
+ jwt: parsed.jwt,
1073
+ expiresAtSec: typeof parsed.expiresAtSec === "number" ? parsed.expiresAtSec : null,
1074
+ storedAtSec: typeof parsed.storedAtSec === "number" ? parsed.storedAtSec : 0
1075
+ };
1076
+ } catch {
1077
+ throw new KeystoreError("malformed_record", "keystore record is not valid JSON");
1078
+ }
1079
+ }
1080
+ function asMessage(err) {
1081
+ return err instanceof Error ? err.message : String(err);
1082
+ }
1083
+ async function openKeystore(options = {}) {
1084
+ const envPref = process.env.MUHAVEN_KEYRING?.toLowerCase();
1085
+ const wantFile = options.preferred === "file" || envPref === "file";
1086
+ const filePath = options.filePath ?? FileKeystore.defaultPath();
1087
+ if (wantFile) {
1088
+ return { keystore: new FileKeystore(filePath), fallbackReason: null };
1089
+ }
1090
+ const mod = await loadKeyringModule();
1091
+ if (!mod) {
1092
+ return {
1093
+ keystore: new FileKeystore(filePath),
1094
+ fallbackReason: "@napi-rs/keyring not installed for this platform"
1095
+ };
1096
+ }
1097
+ const Entry = mod.Entry;
1098
+ if (!Entry) {
1099
+ return {
1100
+ keystore: new FileKeystore(filePath),
1101
+ fallbackReason: "@napi-rs/keyring loaded but Entry constructor missing"
1102
+ };
1103
+ }
1104
+ let entry;
1105
+ try {
1106
+ entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
1107
+ const raw = entry.getPassword();
1108
+ if (raw && typeof raw === "string") {
1109
+ const parsed = parseRecord(raw);
1110
+ if (parsed === null) {
625
1111
  return {
626
- type: "error",
627
- code: "invalid_request",
628
- message: "store_jwt.expiresAtSec must be a positive number when provided"
1112
+ keystore: new FileKeystore(filePath),
1113
+ fallbackReason: "OS keychain held a record that did not contain a recognizable JWT \u2014 falling back to file. Run `muhaven-broker logout` if you want to clean it up."
629
1114
  };
630
1115
  }
631
- return {
632
- type: "store_jwt",
633
- jwt,
634
- ...expiresAtSec === void 0 ? {} : { expiresAtSec }
635
- };
636
1116
  }
637
- case "get_jwt":
638
- return { type: "get_jwt" };
639
- case "clear_jwt":
640
- return { type: "clear_jwt" };
641
- default:
642
- return {
643
- type: "error",
644
- code: "unsupported_type",
645
- message: `unsupported request type: ${String(obj.type)}`
646
- };
1117
+ } catch (err) {
1118
+ const isMalformed = err instanceof KeystoreError && err.code === "malformed_record";
1119
+ return {
1120
+ keystore: new FileKeystore(filePath),
1121
+ fallbackReason: isMalformed ? `OS keychain held a malformed JWT record \u2014 falling back to file. ${asMessage(err)}` : `OS keychain probe failed: ${asMessage(err)}`
1122
+ };
647
1123
  }
648
- }
649
- function serializeResponse(res) {
650
- return JSON.stringify(res) + "\n";
1124
+ return { keystore: new OsKeystore(entry), fallbackReason: null };
651
1125
  }
652
1126
  var MissingSessionKeyError = class extends Error {
653
1127
  constructor() {
@@ -663,6 +1137,9 @@ var NullSigner = class {
663
1137
  async signHash(_hash) {
664
1138
  throw new MissingSessionKeyError();
665
1139
  }
1140
+ async signRawMessage(_hash) {
1141
+ throw new MissingSessionKeyError();
1142
+ }
666
1143
  };
667
1144
  var ViemSigner = class {
668
1145
  account;
@@ -675,12 +1152,297 @@ var ViemSigner = class {
675
1152
  async signHash(hash) {
676
1153
  return this.account.sign({ hash });
677
1154
  }
1155
+ async signRawMessage(hash) {
1156
+ return this.account.signMessage({ message: { raw: hash } });
1157
+ }
1158
+ };
1159
+ var PolicyStoreError = class extends Error {
1160
+ constructor(code, message, cause) {
1161
+ super(message);
1162
+ this.code = code;
1163
+ this.cause = cause;
1164
+ this.name = "PolicyStoreError";
1165
+ }
1166
+ code;
1167
+ cause;
1168
+ };
1169
+ var SESSION_ID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
1170
+ var UINT256_MAX_LOCAL = (1n << 256n) - 1n;
1171
+ function validateSessionId(sessionId) {
1172
+ if (!SESSION_ID_RE2.test(sessionId)) {
1173
+ throw new PolicyStoreError(
1174
+ "invalid_session_id",
1175
+ `sessionId "${sessionId}" must be 1-128 chars [A-Za-z0-9_-] (path-traversal guard)`
1176
+ );
1177
+ }
1178
+ }
1179
+ var FilePolicyStore = class {
1180
+ constructor(dir) {
1181
+ this.dir = dir;
1182
+ }
1183
+ dir;
1184
+ static defaultDir() {
1185
+ return join(homedir(), ".muhaven", "policy-snapshots");
1186
+ }
1187
+ snapshotPath(sessionId) {
1188
+ return join(this.dir, `${sessionId}.json`);
1189
+ }
1190
+ async get(sessionId, nowSec) {
1191
+ validateSessionId(sessionId);
1192
+ let raw;
1193
+ try {
1194
+ raw = await readFile(this.snapshotPath(sessionId), "utf8");
1195
+ } catch (err) {
1196
+ if (err.code === "ENOENT") return null;
1197
+ throw new PolicyStoreError(
1198
+ "read_failed",
1199
+ `failed to read snapshot ${sessionId}: ${asMessage2(err)}`,
1200
+ err
1201
+ );
1202
+ }
1203
+ let parsed;
1204
+ try {
1205
+ const obj = JSON.parse(raw);
1206
+ parsed = coerceFromDisk(obj);
1207
+ } catch (err) {
1208
+ throw new PolicyStoreError(
1209
+ "malformed_record",
1210
+ `snapshot ${sessionId} is not valid JSON: ${asMessage2(err)}`,
1211
+ err
1212
+ );
1213
+ }
1214
+ if (parsed.validUntilSec <= nowSec) return null;
1215
+ return parsed;
1216
+ }
1217
+ async put(snapshot) {
1218
+ validateSessionId(snapshot.sessionId);
1219
+ const dest = this.snapshotPath(snapshot.sessionId);
1220
+ const tmp = `${dest}.tmp-${randomBytes(6).toString("hex")}`;
1221
+ try {
1222
+ await mkdir(this.dir, { recursive: true, mode: 448 });
1223
+ await chmod(this.dir, 448).catch(() => void 0);
1224
+ await writeFile(tmp, JSON.stringify(snapshot), { mode: 384 });
1225
+ await chmod(tmp, 384).catch(() => void 0);
1226
+ await rename(tmp, dest);
1227
+ } catch (err) {
1228
+ await unlink(tmp).catch(() => void 0);
1229
+ throw new PolicyStoreError(
1230
+ "write_failed",
1231
+ `failed to write snapshot ${snapshot.sessionId}: ${asMessage2(err)}`,
1232
+ err
1233
+ );
1234
+ }
1235
+ }
1236
+ async delete(sessionId) {
1237
+ validateSessionId(sessionId);
1238
+ try {
1239
+ await unlink(this.snapshotPath(sessionId));
1240
+ } catch (err) {
1241
+ if (err.code === "ENOENT") return;
1242
+ throw new PolicyStoreError(
1243
+ "delete_failed",
1244
+ `failed to delete snapshot ${sessionId}: ${asMessage2(err)}`,
1245
+ err
1246
+ );
1247
+ }
1248
+ }
1249
+ async list() {
1250
+ let entries;
1251
+ try {
1252
+ entries = await readdir(this.dir);
1253
+ } catch (err) {
1254
+ if (err.code === "ENOENT") return [];
1255
+ throw new PolicyStoreError(
1256
+ "read_failed",
1257
+ `failed to enumerate snapshot dir: ${asMessage2(err)}`,
1258
+ err
1259
+ );
1260
+ }
1261
+ const out = [];
1262
+ for (const entry of entries) {
1263
+ if (!entry.endsWith(".json")) continue;
1264
+ const sessionId = entry.slice(0, -".json".length);
1265
+ if (!SESSION_ID_RE2.test(sessionId)) continue;
1266
+ try {
1267
+ const raw = await readFile(join(this.dir, entry), "utf8");
1268
+ out.push(coerceFromDisk(JSON.parse(raw)));
1269
+ } catch {
1270
+ continue;
1271
+ }
1272
+ }
1273
+ return out;
1274
+ }
1275
+ async activeSessionId(activeSignerAddress, nowSec) {
1276
+ const all = await this.list();
1277
+ const needle = activeSignerAddress.toLowerCase();
1278
+ const matches = all.filter(
1279
+ (s) => s.validUntilSec > nowSec && s.signerAddress.toLowerCase() === needle
1280
+ );
1281
+ return matches.length === 1 ? matches[0].sessionId : null;
1282
+ }
678
1283
  };
1284
+ function coerceFromDisk(obj) {
1285
+ if (typeof obj !== "object" || obj === null) throw new Error("snapshot not an object");
1286
+ const o = obj;
1287
+ if (typeof o.sessionId !== "string" || !SESSION_ID_RE2.test(o.sessionId)) {
1288
+ throw new Error("snapshot.sessionId malformed");
1289
+ }
1290
+ if (o.mode !== "scoped") throw new Error('snapshot.mode must be "scoped"');
1291
+ if (typeof o.signerAddress !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(o.signerAddress)) {
1292
+ throw new Error("snapshot.signerAddress malformed");
1293
+ }
1294
+ if (!Array.isArray(o.targetContracts) || !o.targetContracts.every((t) => typeof t === "string" && /^0x[0-9a-fA-F]{40}$/.test(t))) {
1295
+ throw new Error("snapshot.targetContracts malformed");
1296
+ }
1297
+ if (!Array.isArray(o.selectorCaps) || o.selectorCaps.length === 0) {
1298
+ throw new Error("snapshot.selectorCaps malformed");
1299
+ }
1300
+ const caps = [];
1301
+ for (const c of o.selectorCaps) {
1302
+ if (typeof c !== "object" || c === null) throw new Error("selectorCap not an object");
1303
+ const cap = c;
1304
+ if (typeof cap.selector !== "string" || !/^0x[0-9a-fA-F]{8}$/.test(cap.selector)) {
1305
+ throw new Error("selectorCap.selector malformed");
1306
+ }
1307
+ const indexNull = cap.capArgIndex === null;
1308
+ const amountNull = cap.maxAmount === null;
1309
+ if (indexNull !== amountNull) {
1310
+ throw new Error("selectorCap.capArgIndex/maxAmount must both be null or both non-null");
1311
+ }
1312
+ if (!indexNull) {
1313
+ if (typeof cap.capArgIndex !== "number" || !Number.isInteger(cap.capArgIndex) || cap.capArgIndex < 0 || cap.capArgIndex > 31) {
1314
+ throw new Error("selectorCap.capArgIndex must be an integer in [0, 31]");
1315
+ }
1316
+ if (typeof cap.maxAmount !== "string" || !/^(0|[1-9][0-9]{0,77})$/.test(cap.maxAmount)) {
1317
+ throw new Error("selectorCap.maxAmount malformed (length)");
1318
+ }
1319
+ if (BigInt(cap.maxAmount) > UINT256_MAX_LOCAL) {
1320
+ throw new Error("selectorCap.maxAmount exceeds uint256 max");
1321
+ }
1322
+ }
1323
+ caps.push({
1324
+ selector: cap.selector.toLowerCase(),
1325
+ capArgIndex: indexNull ? null : cap.capArgIndex,
1326
+ maxAmount: indexNull ? null : cap.maxAmount
1327
+ });
1328
+ }
1329
+ if (typeof o.validUntilSec !== "number" || o.validUntilSec <= 0) {
1330
+ throw new Error("snapshot.validUntilSec malformed");
1331
+ }
1332
+ if (typeof o.mintedAtSec !== "number" || o.mintedAtSec <= 0) {
1333
+ throw new Error("snapshot.mintedAtSec malformed");
1334
+ }
1335
+ if (o.consentActionHash !== void 0) {
1336
+ if (typeof o.consentActionHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(o.consentActionHash)) {
1337
+ throw new Error("snapshot.consentActionHash malformed");
1338
+ }
1339
+ }
1340
+ if (o.consentTextSha256 !== void 0) {
1341
+ if (typeof o.consentTextSha256 !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(o.consentTextSha256)) {
1342
+ throw new Error("snapshot.consentTextSha256 malformed");
1343
+ }
1344
+ }
1345
+ if (o.permissionId !== void 0) {
1346
+ if (typeof o.permissionId !== "string" || !/^0x[0-9a-fA-F]{8}$/.test(o.permissionId)) {
1347
+ throw new Error("snapshot.permissionId malformed (must be 0x-prefixed 4-byte hex)");
1348
+ }
1349
+ }
1350
+ return {
1351
+ sessionId: o.sessionId,
1352
+ mode: "scoped",
1353
+ signerAddress: o.signerAddress.toLowerCase(),
1354
+ targetContracts: o.targetContracts.map(
1355
+ (t) => t.toLowerCase()
1356
+ ),
1357
+ selectorCaps: caps,
1358
+ validUntilSec: o.validUntilSec,
1359
+ mintedAtSec: o.mintedAtSec,
1360
+ ...o.consentActionHash === void 0 ? {} : { consentActionHash: o.consentActionHash.toLowerCase() },
1361
+ ...o.consentTextSha256 === void 0 ? {} : { consentTextSha256: o.consentTextSha256.toLowerCase() },
1362
+ ...o.permissionId === void 0 ? {} : { permissionId: o.permissionId.toLowerCase() }
1363
+ };
1364
+ }
1365
+ function asMessage2(err) {
1366
+ return err instanceof Error ? err.message : String(err);
1367
+ }
1368
+ function decodeUint256ArgAt(callDataHex, wordIndex) {
1369
+ if (!Number.isInteger(wordIndex) || wordIndex < 0) {
1370
+ throw new Error(`wordIndex must be a non-negative integer (got ${wordIndex})`);
1371
+ }
1372
+ const requiredLen = 2 + 8 + (wordIndex + 1) * 64;
1373
+ if (callDataHex.length < requiredLen) {
1374
+ throw new Error(
1375
+ `callData length ${callDataHex.length} too short to carry word ${wordIndex} (need \u2265${requiredLen} chars)`
1376
+ );
1377
+ }
1378
+ const wordStart = 2 + 8 + wordIndex * 64;
1379
+ const argHex = callDataHex.slice(wordStart, wordStart + 64);
1380
+ return BigInt(`0x${argHex}`);
1381
+ }
1382
+ function selectorOf(callDataHex) {
1383
+ return callDataHex.slice(0, 10).toLowerCase();
1384
+ }
1385
+ function checkPolicy(input) {
1386
+ const { snapshot, innerCall, activeSigner, nowSec } = input;
1387
+ if (snapshot.validUntilSec <= nowSec) {
1388
+ return {
1389
+ ok: false,
1390
+ code: "scope_violation",
1391
+ message: `snapshot ${snapshot.sessionId} expired at ${snapshot.validUntilSec} (now ${nowSec})`
1392
+ };
1393
+ }
1394
+ if (snapshot.signerAddress.toLowerCase() !== activeSigner.toLowerCase()) {
1395
+ return {
1396
+ ok: false,
1397
+ code: "policy_violation",
1398
+ message: `snapshot ${snapshot.sessionId} bound to signer ${snapshot.signerAddress}, broker active signer is ${activeSigner}`
1399
+ };
1400
+ }
1401
+ const targetLower = innerCall.target.toLowerCase();
1402
+ const targetMatch = snapshot.targetContracts.some((t) => t === targetLower);
1403
+ if (!targetMatch) {
1404
+ return {
1405
+ ok: false,
1406
+ code: "policy_violation",
1407
+ message: `target ${innerCall.target} not in allowlist for session ${snapshot.sessionId}`
1408
+ };
1409
+ }
1410
+ const selector = selectorOf(innerCall.callData);
1411
+ const rule = snapshot.selectorCaps.find((c) => c.selector === selector);
1412
+ if (!rule) {
1413
+ return {
1414
+ ok: false,
1415
+ code: "policy_violation",
1416
+ message: `selector ${selector} not in selectorCaps for session ${snapshot.sessionId}`
1417
+ };
1418
+ }
1419
+ if (rule.capArgIndex !== null && rule.maxAmount !== null) {
1420
+ let argValue;
1421
+ try {
1422
+ argValue = decodeUint256ArgAt(innerCall.callData, rule.capArgIndex);
1423
+ } catch (err) {
1424
+ return {
1425
+ ok: false,
1426
+ code: "policy_violation",
1427
+ message: `callData decode failed at wordIndex ${rule.capArgIndex}: ${asMessage2(err)}`
1428
+ };
1429
+ }
1430
+ const cap = BigInt(rule.maxAmount);
1431
+ if (argValue > cap) {
1432
+ return {
1433
+ ok: false,
1434
+ code: "max_spend_exceeded",
1435
+ message: `arg word[${rule.capArgIndex}] = ${argValue} exceeds maxAmount ${cap} for selector ${selector} (session ${snapshot.sessionId})`
1436
+ };
1437
+ }
1438
+ }
1439
+ return { ok: true };
1440
+ }
679
1441
 
680
1442
  // src/broker/daemon.ts
681
1443
  var noopLogger = (_e) => {
682
1444
  };
683
- async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}) {
1445
+ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}, policyStore) {
684
1446
  switch (req.type) {
685
1447
  case "hello": {
686
1448
  let hasJwt = false;
@@ -753,6 +1515,141 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
753
1515
  );
754
1516
  }
755
1517
  }
1518
+ case "sign_userop": {
1519
+ if (!policyStore) {
1520
+ return errorResponse(
1521
+ "internal",
1522
+ "broker daemon is not configured with a policy store"
1523
+ );
1524
+ }
1525
+ const now = nowSec();
1526
+ let snapshot;
1527
+ try {
1528
+ snapshot = await policyStore.get(req.sessionId, now);
1529
+ } catch (err) {
1530
+ return errorResponse(
1531
+ "internal",
1532
+ err instanceof Error ? err.message : "policy store read failed"
1533
+ );
1534
+ }
1535
+ if (!snapshot) {
1536
+ return errorResponse(
1537
+ "no_active_snapshot",
1538
+ `no active snapshot for session ${req.sessionId}`
1539
+ );
1540
+ }
1541
+ const check = checkPolicy({
1542
+ snapshot,
1543
+ innerCall: req.innerCall,
1544
+ activeSigner: signer.address,
1545
+ nowSec: now
1546
+ });
1547
+ if (!check.ok) {
1548
+ return errorResponse(check.code, check.message);
1549
+ }
1550
+ try {
1551
+ const signature = await signer.signRawMessage(req.userOpHash);
1552
+ return {
1553
+ type: "sign_userop",
1554
+ signature,
1555
+ signerAddress: signer.address,
1556
+ sessionId: req.sessionId
1557
+ };
1558
+ } catch (err) {
1559
+ if (err instanceof MissingSessionKeyError) {
1560
+ return errorResponse("session_key_unavailable", err.message);
1561
+ }
1562
+ throw err;
1563
+ }
1564
+ }
1565
+ case "store_policy_snapshot": {
1566
+ if (!policyStore) {
1567
+ return errorResponse(
1568
+ "internal",
1569
+ "broker daemon is not configured with a policy store"
1570
+ );
1571
+ }
1572
+ try {
1573
+ await policyStore.put(req.snapshot);
1574
+ return {
1575
+ type: "store_policy_snapshot",
1576
+ stored: true,
1577
+ sessionId: req.snapshot.sessionId
1578
+ };
1579
+ } catch (err) {
1580
+ if (err instanceof PolicyStoreError) {
1581
+ return errorResponse("internal", err.message);
1582
+ }
1583
+ return errorResponse(
1584
+ "internal",
1585
+ err instanceof Error ? err.message : "policy store write failed"
1586
+ );
1587
+ }
1588
+ }
1589
+ case "get_policy_snapshot": {
1590
+ if (!policyStore) {
1591
+ return errorResponse(
1592
+ "internal",
1593
+ "broker daemon is not configured with a policy store"
1594
+ );
1595
+ }
1596
+ try {
1597
+ const snapshot = await policyStore.get(req.sessionId, nowSec());
1598
+ return { type: "get_policy_snapshot", snapshot };
1599
+ } catch (err) {
1600
+ if (err instanceof PolicyStoreError) {
1601
+ return errorResponse("internal", err.message);
1602
+ }
1603
+ return errorResponse(
1604
+ "internal",
1605
+ err instanceof Error ? err.message : "policy store read failed"
1606
+ );
1607
+ }
1608
+ }
1609
+ case "clear_policy_snapshot": {
1610
+ if (!policyStore) {
1611
+ return errorResponse(
1612
+ "internal",
1613
+ "broker daemon is not configured with a policy store"
1614
+ );
1615
+ }
1616
+ try {
1617
+ await policyStore.delete(req.sessionId);
1618
+ return {
1619
+ type: "clear_policy_snapshot",
1620
+ cleared: true,
1621
+ sessionId: req.sessionId
1622
+ };
1623
+ } catch (err) {
1624
+ if (err instanceof PolicyStoreError) {
1625
+ return errorResponse("internal", err.message);
1626
+ }
1627
+ return errorResponse(
1628
+ "internal",
1629
+ err instanceof Error ? err.message : "policy store clear failed"
1630
+ );
1631
+ }
1632
+ }
1633
+ case "get_active_session_id": {
1634
+ if (!policyStore) {
1635
+ return errorResponse(
1636
+ "internal",
1637
+ "broker daemon is not configured with a policy store"
1638
+ );
1639
+ }
1640
+ try {
1641
+ const sessionId = await policyStore.activeSessionId(signer.address, nowSec());
1642
+ return { type: "get_active_session_id", sessionId };
1643
+ } catch (err) {
1644
+ if (err instanceof PolicyStoreError) {
1645
+ return errorResponse("internal", err.message);
1646
+ }
1647
+ return errorResponse(
1648
+ "internal",
1649
+ err instanceof Error ? err.message : "policy store enumerate failed"
1650
+ );
1651
+ }
1652
+ }
756
1653
  }
757
1654
  }
758
1655
  function errorResponse(code, message) {
@@ -782,6 +1679,7 @@ var BrokerDaemon = class {
782
1679
  log;
783
1680
  config;
784
1681
  keystore;
1682
+ policyStore;
785
1683
  /**
786
1684
  * Whether a session-key private half is actually loaded. `false` =
787
1685
  * daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
@@ -801,6 +1699,7 @@ var BrokerDaemon = class {
801
1699
  this.hasSessionKey = false;
802
1700
  }
803
1701
  this.keystore = options.keystore ?? null;
1702
+ this.policyStore = options.policyStore ?? new FilePolicyStore(FilePolicyStore.defaultDir());
804
1703
  this.log = options.logger ?? noopLogger;
805
1704
  this.server = createServer((socket) => this.onConnection(socket));
806
1705
  }
@@ -816,6 +1715,7 @@ var BrokerDaemon = class {
816
1715
  });
817
1716
  }
818
1717
  }
1718
+ if (this.policyStore.init) await this.policyStore.init();
819
1719
  await prepareEndpoint(this.config.endpoint);
820
1720
  await new Promise((resolve, reject) => {
821
1721
  const onError = (err) => {
@@ -931,7 +1831,8 @@ var BrokerDaemon = class {
931
1831
  dashboardBaseUrl: this.config.dashboardBaseUrl
932
1832
  },
933
1833
  pid: process.pid
934
- }
1834
+ },
1835
+ this.policyStore
935
1836
  );
936
1837
  socket.end(serializeResponse(res));
937
1838
  } catch (err) {
@@ -1812,6 +2713,48 @@ async function runDoctor() {
1812
2713
  print(`Daemon backend URL: ${h.effectiveConfig.backendBaseUrl}`);
1813
2714
  print(`Daemon dashboard : ${h.effectiveConfig.dashboardBaseUrl}`);
1814
2715
  }
2716
+ if (h.hasJwt) {
2717
+ try {
2718
+ const jwtRes = await broker.getJwt();
2719
+ if (!jwtRes.jwt) {
2720
+ print(`JWT inspection : daemon protocol violation \u2014 hello.hasJwt=true but get_jwt returned null; run \`muhaven-broker logout && muhaven-broker login\` to reset keystore`);
2721
+ } else {
2722
+ const decoded = decodeJwtPayload(jwtRes.jwt);
2723
+ print(`JWT subject : ${truncateSubject(decoded.sub)} (unverified \u2014 backend re-validates on every API call)`);
2724
+ if (decoded.scope.length === 0) {
2725
+ print(`JWT scope : (none \u2014 legacy SIWE token; all-scopes for back-compat)`);
2726
+ } else {
2727
+ const SCOPE_CAP = 20;
2728
+ const displayed = decoded.scope.slice(0, SCOPE_CAP).map((s) => sanitizeClaimForTerminal(s, 80));
2729
+ const overflow = decoded.scope.length > SCOPE_CAP ? ` (+${decoded.scope.length - SCOPE_CAP} more, suppressed)` : "";
2730
+ print(`JWT scope : ${displayed.join(", ")}${overflow}`);
2731
+ }
2732
+ if (decoded.iss !== null) {
2733
+ print(`JWT issuer : ${sanitizeClaimForTerminal(decoded.iss, 80)}`);
2734
+ }
2735
+ if (decoded.expSec !== null) {
2736
+ const nowSec = Math.floor(Date.now() / 1e3);
2737
+ const remainingSec = decoded.expSec - nowSec;
2738
+ const expiresAt = new Date(decoded.expSec * 1e3).toISOString();
2739
+ if (remainingSec <= 0) {
2740
+ print(`JWT expires : ${expiresAt} (EXPIRED \u2014 run \`muhaven-broker login\` to refresh)`);
2741
+ } else {
2742
+ const remainingHr = Math.floor(remainingSec / 3600);
2743
+ const remainingMin = Math.floor(remainingSec % 3600 / 60);
2744
+ print(`JWT expires : ${expiresAt} (in ${remainingHr}h${remainingMin}m)`);
2745
+ }
2746
+ }
2747
+ }
2748
+ } catch (err) {
2749
+ if (err instanceof JwtDecodeError) {
2750
+ print(`JWT inspection : FAILED (${err.code}: ${err.message}) \u2014 JWT is in keystore but malformed; run \`muhaven-broker logout && login\` to refresh`);
2751
+ } else {
2752
+ const errName = err instanceof Error ? err.constructor?.name ?? "Error" : "unknown";
2753
+ const errMsg = err instanceof Error ? err.message : String(err);
2754
+ print(`JWT inspection : FAILED (${errName}: ${errMsg})`);
2755
+ }
2756
+ }
2757
+ }
1815
2758
  return 0;
1816
2759
  } catch (err) {
1817
2760
  print(`Broker daemon : NOT reachable (${err.message})`);
@@ -1842,7 +2785,7 @@ function printUsage() {
1842
2785
  }
1843
2786
  function getBrokerPackageVersion() {
1844
2787
  {
1845
- return "0.2.1";
2788
+ return "0.2.3";
1846
2789
  }
1847
2790
  }
1848
2791
  function printVersion() {