@muhaven/mcp 0.2.1 → 0.2.2

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