midsummer-sol 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/index.js +200 -6
  2. package/package.json +1 -1
  3. package/sol-mcp.js +13029 -98
  4. package/sol-secret-mcp.js +177 -15
  5. package/sol.js +6259 -284
package/sol-secret-mcp.js CHANGED
@@ -246,6 +246,141 @@ var init_sign = __esm(() => {
246
246
  ED25519_PKCS8_PREFIX2 = Buffer.from("302e020100300506032b657004220420", "hex");
247
247
  });
248
248
 
249
+ // src/bin/mcp-http.ts
250
+ var exports_mcp_http = {};
251
+ __export(exports_mcp_http, {
252
+ serveMcpHttp: () => serveMcpHttp,
253
+ resolveMcpToken: () => resolveMcpToken,
254
+ parseStandaloneHttp: () => parseStandaloneHttp
255
+ });
256
+ import { randomUUID, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
257
+ import { createServer } from "node:http";
258
+ function tokenMatches(provided, expected) {
259
+ const a = Buffer.from(provided);
260
+ const b = Buffer.from(expected);
261
+ if (a.length !== b.length)
262
+ return false;
263
+ return timingSafeEqual2(a, b);
264
+ }
265
+ function bearer(req) {
266
+ const h = req.headers["authorization"];
267
+ if (typeof h !== "string")
268
+ return;
269
+ const m = /^Bearer\s+(.+)$/i.exec(h.trim());
270
+ return m ? m[1].trim() : undefined;
271
+ }
272
+ function writeJson(res, status, body) {
273
+ const s = JSON.stringify(body);
274
+ res.writeHead(status, { "Content-Type": "application/json" });
275
+ res.end(s);
276
+ }
277
+ async function readBody(req) {
278
+ const chunks = [];
279
+ for await (const c of req)
280
+ chunks.push(c);
281
+ if (chunks.length === 0)
282
+ return;
283
+ const raw = Buffer.concat(chunks).toString("utf8");
284
+ if (!raw.trim())
285
+ return;
286
+ return JSON.parse(raw);
287
+ }
288
+ async function serveMcpHttp(buildServer, opts) {
289
+ const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
290
+ const { isInitializeRequest } = await import("@modelcontextprotocol/sdk/types.js");
291
+ const port = opts.port ?? 8765;
292
+ const host = opts.host ?? "127.0.0.1";
293
+ const transports = new Map;
294
+ const http = createServer(async (req, res) => {
295
+ try {
296
+ const tok = bearer(req);
297
+ if (!tok || !tokenMatches(tok, opts.token)) {
298
+ res.setHeader("WWW-Authenticate", "Bearer");
299
+ writeJson(res, 401, { jsonrpc: "2.0", error: { code: -32001, message: "unauthorized: a valid Authorization: Bearer token is required" }, id: null });
300
+ return;
301
+ }
302
+ const url = (req.url ?? "").split("?")[0];
303
+ if (url !== MCP_PATH) {
304
+ writeJson(res, 404, { jsonrpc: "2.0", error: { code: -32601, message: `not found — the MCP endpoint is ${MCP_PATH}` }, id: null });
305
+ return;
306
+ }
307
+ const sessionId = req.headers["mcp-session-id"];
308
+ const sid = typeof sessionId === "string" ? sessionId : undefined;
309
+ if (req.method === "POST") {
310
+ const body = await readBody(req);
311
+ let transport = sid ? transports.get(sid) : undefined;
312
+ if (!transport) {
313
+ if (sid || !isInitializeRequest(body)) {
314
+ writeJson(res, 400, { jsonrpc: "2.0", error: { code: -32000, message: "no valid session — send an initialize request first" }, id: null });
315
+ return;
316
+ }
317
+ transport = new StreamableHTTPServerTransport({
318
+ sessionIdGenerator: () => randomUUID(),
319
+ onsessioninitialized: (id) => void transports.set(id, transport)
320
+ });
321
+ transport.onclose = () => {
322
+ if (transport.sessionId)
323
+ transports.delete(transport.sessionId);
324
+ };
325
+ const server = await buildServer();
326
+ await server.connect(transport);
327
+ }
328
+ await transport.handleRequest(req, res, body);
329
+ return;
330
+ }
331
+ if (req.method === "GET" || req.method === "DELETE") {
332
+ const transport = sid ? transports.get(sid) : undefined;
333
+ if (!transport) {
334
+ writeJson(res, 400, { jsonrpc: "2.0", error: { code: -32000, message: "unknown or missing Mcp-Session-Id" }, id: null });
335
+ return;
336
+ }
337
+ await transport.handleRequest(req, res);
338
+ return;
339
+ }
340
+ writeJson(res, 405, { jsonrpc: "2.0", error: { code: -32000, message: "method not allowed" }, id: null });
341
+ } catch (e) {
342
+ if (!res.headersSent) {
343
+ writeJson(res, 500, { jsonrpc: "2.0", error: { code: -32603, message: "internal error: " + (e instanceof Error ? e.message : String(e)) }, id: null });
344
+ } else {
345
+ try {
346
+ res.end();
347
+ } catch {}
348
+ }
349
+ }
350
+ });
351
+ await new Promise((resolve, reject) => {
352
+ http.once("error", reject);
353
+ http.listen(port, host, () => {
354
+ http.off("error", reject);
355
+ process.stderr.write(`${opts.label} MCP (HTTP) listening on http://${host}:${port}${MCP_PATH} — Authorization: Bearer required
356
+ `);
357
+ resolve();
358
+ });
359
+ });
360
+ await new Promise(() => {});
361
+ }
362
+ function resolveMcpToken(flagToken) {
363
+ const t = (flagToken ?? process.env.SOL_MCP_TOKEN ?? "").trim();
364
+ return t.length ? t : undefined;
365
+ }
366
+ function parseStandaloneHttp(argv, label = "sol") {
367
+ if (!argv.includes("--http"))
368
+ return;
369
+ const val = (name) => {
370
+ const i = argv.indexOf(name);
371
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined;
372
+ };
373
+ const token = resolveMcpToken(val("--token"));
374
+ if (!token) {
375
+ process.stderr.write("refusing to start an OPEN HTTP MCP — set SOL_MCP_TOKEN (or --token <T>). every request must send `Authorization: Bearer <token>`.\n");
376
+ process.exit(1);
377
+ }
378
+ const portRaw = val("--port");
379
+ return { token, port: portRaw ? Number(portRaw) : undefined, host: val("--host"), label };
380
+ }
381
+ var MCP_PATH = "/mcp";
382
+ var init_mcp_http = () => {};
383
+
249
384
  // src/bin/identity-store.ts
250
385
  import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
251
386
  import { homedir as homedir2 } from "node:os";
@@ -1775,9 +1910,11 @@ function resolveAll(world) {
1775
1910
  return world.manifest.order.map((e) => resolveOne(world, e));
1776
1911
  }
1777
1912
  function audienceFor(world, env, entryAud) {
1778
- if (entryAud && entryAud.length)
1779
- return entryAud;
1780
- return world.files[env]?.audience ?? [];
1913
+ const base = entryAud && entryAud.length ? entryAud : world.files[env]?.audience ?? [];
1914
+ const runtime = world.manifest.runtime[env];
1915
+ if (runtime && !base.includes(runtime))
1916
+ return [...base, runtime];
1917
+ return base;
1781
1918
  }
1782
1919
  function ensureEnvDir(solDir) {
1783
1920
  mkdirSync3(envDir(solDir), { recursive: true });
@@ -1868,7 +2005,14 @@ function validateWorld(solDir, world, opts = {}) {
1868
2005
  const actual = new Set(recipientsOf(box));
1869
2006
  const expected = new Set(resolvedAud.accountIds);
1870
2007
  const extra = [...actual].filter((a) => !expected.has(a));
1871
- const missing = [...expected].filter((a) => !actual.has(a));
2008
+ let missing = [...expected].filter((a) => !actual.has(a));
2009
+ const runtimeHandle2 = world.manifest.runtime[r.env];
2010
+ const runtimeAccts = new Set((runtimeHandle2 ? world.gate.handles[runtimeHandle2] ?? [] : []).map((rec) => rec.accountId));
2011
+ const missingRuntime = missing.filter((a) => runtimeAccts.has(a));
2012
+ missing = missing.filter((a) => !runtimeAccts.has(a));
2013
+ if (missingRuntime.length) {
2014
+ issues.push({ severity: "warn", check: "audience-integrity", message: `${r.env}/${s.name}: the runtime recipient [${missingRuntime.join(", ")}] joined the env @audience after this value was sealed — it can't inject this secret yet. reseal it (\`sol secret set ${s.name} --env ${r.env}\`) to include the runtime.` });
2015
+ }
1872
2016
  if (extra.length || missing.length) {
1873
2017
  issues.push({ severity: "error", check: "audience-integrity", message: `${r.env}/${s.name}: sealed recipients drift from the audience (missing: [${missing.join(", ")}], extra: [${extra.join(", ")}]) — reseal with \`sol secret set\` or \`sol secret audience\`` });
1874
2018
  }
@@ -2778,7 +2922,9 @@ function secretInject(ctx, args) {
2778
2922
  ctx.die("usage: sol secret inject --env E -- <cmd> [args...]");
2779
2923
  const cmd = args.slice(dashdash + 1);
2780
2924
  const world = loadWorld(ctx.solDir);
2781
- const self = ctx.loadSelfIdentity();
2925
+ const runtimeRoot = process.env.SOL_RUNTIME_RECOVERY_CODE;
2926
+ const self = runtimeRoot ? runtimeSelfFromRoot(env, runtimeRoot) : ctx.loadSelfIdentity();
2927
+ const recipientActor = self?.accountId ?? ctx.actor;
2782
2928
  const injected = {};
2783
2929
  let revealedCount = 0;
2784
2930
  for (const s of resolveOne(world, env).secrets) {
@@ -2795,7 +2941,7 @@ function secretInject(ctx, args) {
2795
2941
  revealedCount++;
2796
2942
  }
2797
2943
  if (!revealedCount)
2798
- ctx.die(`no secrets in ${env} are readable by ${ctx.actor} — are you a recipient? (set SOL_RECOVERY_CODE)`);
2944
+ ctx.die(`no secrets in ${env} are readable by ${recipientActor} — are you a recipient? (set SOL_RECOVERY_CODE, or SOL_RUNTIME_RECOVERY_CODE when injecting from the runtime)`);
2799
2945
  process.stderr.write(`sol: injecting ${revealedCount} secret(s) into the subprocess env: ${Object.keys(injected).join(", ")}
2800
2946
  `);
2801
2947
  const res = spawnSync(cmd[0], cmd.slice(1), { stdio: "inherit", env: { ...process.env, ...injected } });
@@ -2912,6 +3058,11 @@ async function runtimeProvision(ctx, args) {
2912
3058
  const { gate } = await audienceAdd(audCtx, world.gate, prov.handle, prov.accountId, prov.x25519Pub);
2913
3059
  writeAudienceDoc(ctx.solDir, gate);
2914
3060
  journal2(ctx, id, { op: "audience-add", handle: prov.handle, members: memberSnapshot(gate, prov.handle), recipientAtOp: true, prevAud, newAud: (gate.handles[prov.handle] ?? []).map((r) => r.accountId), at: Date.now() });
3061
+ const envFile = world.files[env];
3062
+ if (envFile && !(envFile.audience ?? []).includes(prov.handle)) {
3063
+ envFile.audience = [...envFile.audience ?? [], prov.handle];
3064
+ writeEnvFile(ctx.solDir, envFile);
3065
+ }
2915
3066
  world.manifest.runtime[env] = prov.handle;
2916
3067
  writeManifest(ctx.solDir, world.manifest);
2917
3068
  regenerateSchema(ctx.solDir, loadWorld(ctx.solDir));
@@ -3198,16 +3349,10 @@ var EXPORT_KDF = { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
3198
3349
  var VERIFIED_PATH = join4(homedir(), ".sol", "verified-keys.json");
3199
3350
 
3200
3351
  // src/bin/sol-secret-mcp.ts
3201
- async function startSecretMcp(opts = {}) {
3352
+ init_mcp_http();
3353
+ async function buildSecretServer(solDir) {
3202
3354
  const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
3203
- const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
3204
3355
  const { CallToolRequestSchema, ListToolsRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
3205
- const solDir = opts.solDir || process.env.SOL_DIR || join7(process.cwd(), ".sol");
3206
- if (!existsSync8(solDir)) {
3207
- process.stderr.write(`sol-secret-mcp: no .sol at ${solDir} \u2014 run \`sol init\` first (or set SOL_DIR)
3208
- `);
3209
- process.exit(1);
3210
- }
3211
3356
  const dirUrl = (process.env.SOL_REMOTE || "https://sol.midsummer.new").replace(/\/+$/, "");
3212
3357
  async function fetchKey2(account) {
3213
3358
  const { fetchKey: f } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
@@ -3237,10 +3382,27 @@ async function startSecretMcp(opts = {}) {
3237
3382
  const r = await callMcpTool(ctx, req.params.name, req.params.arguments ?? {});
3238
3383
  return { content: [{ type: "text", text: r.text }], isError: r.isError };
3239
3384
  });
3385
+ return server;
3386
+ }
3387
+ async function startSecretMcp(opts = {}) {
3388
+ const solDir = opts.solDir || process.env.SOL_DIR || join7(process.cwd(), ".sol");
3389
+ if (!existsSync8(solDir)) {
3390
+ process.stderr.write(`sol-secret-mcp: no .sol at ${solDir} \u2014 run \`sol init\` first (or set SOL_DIR)
3391
+ `);
3392
+ process.exit(1);
3393
+ }
3394
+ if (opts.http) {
3395
+ const { serveMcpHttp: serveMcpHttp2 } = await Promise.resolve().then(() => (init_mcp_http(), exports_mcp_http));
3396
+ await serveMcpHttp2(() => buildSecretServer(solDir), opts.http);
3397
+ return;
3398
+ }
3399
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
3400
+ const server = await buildSecretServer(solDir);
3240
3401
  await server.connect(new StdioServerTransport);
3241
3402
  }
3242
3403
  if (__require.main == __require.module) {
3243
- startSecretMcp().catch((e) => {
3404
+ const http = parseStandaloneHttp(process.argv.slice(2), "sol-secrets");
3405
+ startSecretMcp({ http }).catch((e) => {
3244
3406
  process.stderr.write(`sol-secret-mcp: ${e?.message ?? e}
3245
3407
  `);
3246
3408
  process.exit(1);