import { createHash } from "node:crypto"; export const migrationObjectId = (executionId, port, logicalKey) => `obj:migration:${createHash("sha256") .update(JSON.stringify([executionId, port, logicalKey])) .digest("hex")}`; /** No ordinary RuntimeContext or network/database clients are supplied here. * Process isolation belongs to the host, not this convenience API. */ export const createMigrationContext = (input) => { if (input.schemaVersion !== 1 || !input.executionId || new Set(input.ports.map((entry) => entry.name)).size !== input.ports.length) throw new Error("Invalid migration input"); const output = { schemaVersion: 1, executionId: input.executionId, writes: [], creates: [], edgeReplacements: [], }; const port = (name, access) => { const selected = input.ports.find((entry) => entry.name === name); if (!selected?.access.includes(access) || (selected.view === "old" && access !== "read")) throw new Error(`Migration port ${name} does not grant ${access}`); return selected; }; const context = { enumerate(name) { const selected = port(name, "read"), states = structuredClone(selected.states ?? []); if (selected.view === "new" && Object.hasOwn(selected, "defaultValue")) for (const helper of output.creates) { if (input.ports.find((entry) => entry.name === helper.port)?.atomId === selected.attachedAtomId && !states.some((entry) => entry.objectId === helper.objectId)) states.push({ objectId: helper.objectId, value: structuredClone(selected.defaultValue) }); } if (selected.view === "new") for (const write of output.writes) { if (input.ports.find((entry) => entry.name === write.port)?.binding !== selected.binding) continue; const existing = states.findIndex((entry) => entry.objectId === write.objectId), entry = { objectId: write.objectId, value: structuredClone(write.value) }; if (existing < 0) states.push(entry); else states[existing] = entry; } return states.sort((a, b) => (a.objectId < b.objectId ? -1 : a.objectId > b.objectId ? 1 : 0)); }, read(name, objectId) { return context.enumerate(name).find((entry) => entry.objectId === objectId)?.value; }, write(name, objectId, value) { port(name, "write"); const previous = output.writes.findIndex((entry) => entry.port === name && entry.objectId === objectId); const entry = { port: name, objectId, value: structuredClone(value) }; if (previous < 0) output.writes.push(entry); else output.writes[previous] = entry; }, create(name, logicalKey) { port(name, "create"); if (!logicalKey || logicalKey.length > 1024) throw new Error("Migration creation requires a bounded stable logical key"); const objectId = migrationObjectId(input.executionId, name, logicalKey); if (!output.creates.some((entry) => entry.objectId === objectId)) output.creates.push({ port: name, logicalKey, objectId }); return objectId; }, edges(name) { const selected = port(name, "read"); const replacement = selected.view === "new" ? output.edgeReplacements.find((entry) => input.ports.find((candidate) => candidate.name === entry.port)?.binding === selected.binding) : undefined; return structuredClone(replacement?.edges ?? selected.edges ?? []); }, replaceEdges(name, edges) { port(name, "edge"); const previous = output.edgeReplacements.findIndex((entry) => entry.port === name); const entry = { port: name, edges: structuredClone(edges) }; if (previous < 0) output.edgeReplacements.push(entry); else output.edgeReplacements[previous] = entry; }, }; return { context, result: () => structuredClone(output) }; }; /** Entrypoint for an immutable package's dedicated bin/migrate executable. * stdout is protocol-only; send diagnostics to stderr. The host independently * validates every write, helper identity, contract, and completion receipt. */ export const serveMigration = async (exports) => { const chunks = []; let bytes = 0; for await (const chunk of process.stdin) { bytes += chunk.length; if (bytes > 16 * 1024 * 1024) throw new Error("Migration input exceeds 16 MiB"); chunks.push(Buffer.from(chunk)); } const input = JSON.parse(Buffer.concat(chunks).toString("utf8")); const implementation = Object.hasOwn(exports, input.exportId) ? exports[input.exportId] : undefined; if (!implementation) throw new Error("Unknown migration export"); const execution = createMigrationContext(input); await implementation(execution.context); const result = JSON.stringify(execution.result()); if (Buffer.byteLength(result) > 16 * 1024 * 1024) throw new Error("Migration output exceeds 16 MiB"); process.stdout.write(`${result}\n`); };