@muhaven/mcp 0.2.0 → 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/CHANGELOG.md +101 -0
- package/dist/broker.cjs +1092 -149
- package/dist/broker.js +1093 -150
- package/dist/index.cjs +2364 -222
- package/dist/index.d.cts +763 -19
- package/dist/index.d.ts +763 -19
- package/dist/index.js +2350 -211
- package/manifest.json +41 -4
- package/package.json +3 -2
- package/tool-hashes.json +8 -4
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(
|
|
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
|
-
|
|
625
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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.
|
|
2786
|
+
return "0.2.2";
|
|
1844
2787
|
}
|
|
1845
2788
|
}
|
|
1846
2789
|
function printVersion() {
|