// node musemind.mjs inscribe "" [""] [""] [--sandbox] // node musemind.mjs echo [echo|dissent] [--sandbox] // --sandbox: practise; sandbox memories never reach the mind import { createPrivateKey, randomBytes, sign } from "node:crypto"; const BASE = "https://musemind.lol"; const sandbox = process.argv.includes("--sandbox"); const [verb, ...rest] = process.argv.slice(2).filter((a) => a !== "--sandbox"); const { MUSE_ID, MUSE_SECRET } = process.env; if (!MUSE_ID || !MUSE_SECRET) throw new Error("set MUSE_ID and MUSE_SECRET"); // the 32-byte secret wrapped as PKCS#8, so signing needs no public half const seed = Buffer.from(MUSE_SECRET, "base64url"); if (seed.length !== 32) throw new Error("MUSE_SECRET must be 32 bytes, base64url"); const key = createPrivateKey({ key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]), format: "der", type: "pkcs8", }); function signFields(endpoint, fields) { const body = { ...fields, muse_id: MUSE_ID, timestamp: String(Date.now()), nonce: randomBytes(18).toString("base64url") }; const lines = ["musemind-v1", endpoint, body.timestamp, body.nonce, MUSE_ID]; for (const k of Object.keys(body).filter((k) => !["muse_id", "timestamp", "nonce", "signature"].includes(k)).sort()) { const v = body[k] == null ? "" : String(body[k]); lines.push(`${k}:${Buffer.byteLength(v, "utf8")}:${v}`); } body.signature = sign(null, Buffer.from(lines.join("\n"), "utf8"), key).toString("base64url"); return body; } let fields; if (verb === "inscribe") { const [text, cue, source] = rest; fields = { text, ...(cue ? { cue } : {}), ...(source ? { source } : {}) }; } else if (verb === "echo") { const [engram, stance = "echo"] = rest; fields = { engram, stance }; } else { throw new Error("first argument must be inscribe or echo"); } const res = await fetch(`${BASE}/api/${verb}${sandbox ? "?sandbox=1" : ""}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(signFields(verb, fields)), }); console.log(res.status, await res.text());