mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 12:35:13 +02:00
298 lines
17 KiB
JavaScript
298 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
// An explicit, private materialization store. It never registers a provider or
|
|
// changes a user's install receipts, settings, hooks, or permission grants.
|
|
// Receipt, immutable-generation, lock, and recovery concepts are adapted from
|
|
// the ECC-029 activation prototype and Jeffrey Montoya's #2788 carrier work.
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { planContextCarrier } = require('./context-carriers');
|
|
const { createSourceReader, digestObject, stableStringify, validateSchema } = require('./context-profile-support');
|
|
const io = require('./context-profile-store-fs');
|
|
|
|
const DIGEST = /^[a-f0-9]{64}$/;
|
|
const CARRIER_KEYS = ['repoRoot', 'profileId', 'selectionMode', 'target', 'include', 'exclude'];
|
|
const INPUT_KEYS = new Set([...CARRIER_KEYS, 'stateRoot', 'expectedRevision', 'expectedCarrierDigest', 'onCheckpoint']);
|
|
const equal = (a, b) => stableStringify(a) === stableStringify(b);
|
|
const exists = name => Boolean(fs.lstatSync(name, { throwIfNoEntry: false }));
|
|
|
|
function rootFor(options) {
|
|
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new Error('Store options must be an object');
|
|
for (const key of Object.keys(options)) if (!INPUT_KEYS.has(key)) throw new Error(`Unknown store option: ${key}`);
|
|
const root = options.stateRoot;
|
|
if (typeof root !== 'string' || !path.isAbsolute(root) || path.resolve(root) !== root
|
|
|| root === path.parse(root).root || root === os.homedir()) throw new Error('stateRoot must name an explicit dedicated absolute directory');
|
|
if (options.expectedRevision !== undefined && (!Number.isSafeInteger(options.expectedRevision) || options.expectedRevision < 0)) throw new Error('Expected revision must be a nonnegative integer');
|
|
if (options.expectedCarrierDigest !== undefined && !DIGEST.test(options.expectedCarrierDigest)) throw new Error('Invalid expected carrier digest');
|
|
if (options.onCheckpoint !== undefined && typeof options.onCheckpoint !== 'function') throw new Error('Invalid checkpoint callback');
|
|
io.inspect(root, true);
|
|
return root;
|
|
}
|
|
|
|
function ownership(root, create = false) {
|
|
const marker = { schemaVersion: 'ecc.context-store.v1', destinationDigest: digestObject({ root }) };
|
|
if (!exists(root)) {
|
|
if (!create) return false;
|
|
io.mkdir(root);
|
|
io.writeExclusive(path.join(root, 'store.json'), io.jsonBytes(marker));
|
|
}
|
|
const stat = io.inspect(root).stat;
|
|
if (!stat.isDirectory() || (process.platform !== 'win32' && ((stat.mode & 0o077) !== 0
|
|
|| (process.getuid && stat.uid !== process.getuid())))) throw new Error('Managed store must be a private owned directory');
|
|
if (!exists(path.join(root, 'store.json')) || !equal(io.readJson(path.join(root, 'store.json')), marker)) throw new Error('Directory is not an owned ECC managed store');
|
|
return true;
|
|
}
|
|
|
|
function checkCarrier(carrier, expectedDigest) {
|
|
validateSchema(carrier, 'context-carrier.schema.json');
|
|
const { carrierDigest, ...body } = carrier;
|
|
if (carrier.status !== 'planned' || !DIGEST.test(expectedDigest) || carrierDigest !== expectedDigest
|
|
|| digestObject(body) !== expectedDigest) throw new Error('Managed carrier digest integrity mismatch');
|
|
return carrier;
|
|
}
|
|
|
|
function generationPath(root, digest) {
|
|
if (!DIGEST.test(digest)) throw new Error('Invalid generation digest');
|
|
return path.join(root, 'generations', digest);
|
|
}
|
|
|
|
function verifyGeneration(directory, carrier, partial = false) {
|
|
const expected = new Map(carrier.files.map(file => [`payload/${file.destinationPath}`, file]));
|
|
const descriptor = io.jsonBytes(carrier);
|
|
expected.set('carrier.json', { digest: io.hash(descriptor), bytes: descriptor.length });
|
|
const allowedDirectories = new Set(['payload']);
|
|
for (const name of expected.keys()) {
|
|
const parts = name.split('/');
|
|
for (let i = 1; i < parts.length; i++) allowedDirectories.add(parts.slice(0, i).join('/'));
|
|
}
|
|
const observed = io.inventory(directory);
|
|
for (const file of observed.files) {
|
|
const wanted = expected.get(file.path);
|
|
if (!wanted || file.digest !== wanted.digest || file.bytes !== wanted.bytes) throw new Error(`Managed generation file changed or has unexpected digest: ${file.path}`);
|
|
}
|
|
if (observed.directories.some(name => !allowedDirectories.has(name))) throw new Error('Managed generation contains an extra directory');
|
|
if (!partial && (observed.files.length !== expected.size || observed.directories.length !== allowedDirectories.size)) throw new Error('Managed generation integrity is incomplete');
|
|
return observed;
|
|
}
|
|
|
|
function loadGeneration(root, digest) {
|
|
const directory = generationPath(root, digest);
|
|
const carrier = checkCarrier(io.readJson(path.join(directory, 'carrier.json')), digest);
|
|
verifyGeneration(directory, carrier);
|
|
return carrier;
|
|
}
|
|
|
|
function readState(root) {
|
|
if (!exists(path.join(root, 'state.json'))) return null;
|
|
const state = io.readJson(path.join(root, 'state.json'));
|
|
if (state.schemaVersion !== 'ecc.context-store-state.v1' || !Number.isSafeInteger(state.revision)
|
|
|| state.revision < 1 || !DIGEST.test(state.receiptDigest)) throw new Error('Invalid managed state');
|
|
const receipt = io.readJson(path.join(root, 'receipts', `${state.receiptDigest}.json`));
|
|
if (digestObject(receipt) !== state.receiptDigest || receipt.destinationDigest !== digestObject({ root })
|
|
|| !equal(state, stateFor(receipt))) throw new Error('Managed receipt and state integrity mismatch');
|
|
checkSelection(receipt.selection, loadGeneration(root, state.generationDigest));
|
|
return state;
|
|
}
|
|
|
|
function selectionFor(carrier, options) {
|
|
return { profileId: carrier.profileId, target: carrier.target, selectionMode: carrier.selectionMode,
|
|
include: [...(options.include || [])].sort(), exclude: [...(options.exclude || [])].sort() };
|
|
}
|
|
|
|
function checkSelection(selection, carrier) {
|
|
if (!selection || selection.profileId !== carrier.profileId || selection.target !== carrier.target
|
|
|| selection.selectionMode !== carrier.selectionMode || !Array.isArray(selection.include)
|
|
|| selection.include.some(id => !carrier.selectedIds.includes(id))
|
|
|| !equal(selection.exclude, carrier.excludedIds)) throw new Error('Managed selection does not match its carrier');
|
|
}
|
|
|
|
function stateFor(receipt) {
|
|
return { schemaVersion: 'ecc.context-store-state.v1', revision: receipt.revision,
|
|
generationDigest: receipt.generationDigest, previousGenerationDigest: receipt.previousGenerationDigest,
|
|
selection: receipt.selection,
|
|
receiptDigest: digestObject(receipt) };
|
|
}
|
|
|
|
function result(root, state, pending = false) {
|
|
const carrier = state ? loadGeneration(root, state.generationDigest) : null;
|
|
return { schemaVersion: 'ecc.context-store-status.v1', status: pending ? 'recovery-required' : state ? 'configured' : 'unconfigured',
|
|
stateRoot: root, revision: state?.revision || 0, configured: Boolean(state), active: false,
|
|
activation: 'unobserved', recoveryRequired: pending,
|
|
profileId: carrier?.profileId || null, target: carrier?.target || null, selectionMode: carrier?.selectionMode || null,
|
|
include: state?.selection.include || [], exclude: state?.selection.exclude || [],
|
|
carrierDigest: carrier?.carrierDigest || null, selectedIds: carrier?.selectedIds || [],
|
|
generationRoot: state ? path.join(generationPath(root, state.generationDigest), 'payload') : null,
|
|
receiptDigest: state?.receiptDigest || null };
|
|
}
|
|
|
|
function getStoreStatus(options) {
|
|
const root = rootFor(options);
|
|
if (!ownership(root)) return result(root, null);
|
|
return result(root, readState(root), exists(path.join(root, 'pending.json')) || exists(path.join(root, '.lock')));
|
|
}
|
|
|
|
function selectedCarrier(options) {
|
|
const carrierOptions = Object.fromEntries(CARRIER_KEYS.filter(key => Object.hasOwn(options, key)).map(key => [key, options[key]]));
|
|
const carrier = planContextCarrier(carrierOptions);
|
|
if (carrier.status !== 'planned') throw new Error('Unsupported carrier target cannot be materialized');
|
|
if (options.expectedCarrierDigest !== undefined && options.expectedCarrierDigest !== carrier.carrierDigest) throw new Error('Carrier digest changed since preview');
|
|
return { carrier, carrierOptions };
|
|
}
|
|
|
|
function revisionCheck(options, state) {
|
|
if (options.expectedRevision !== undefined && options.expectedRevision !== (state?.revision || 0)) throw new Error('Managed state revision changed since preview');
|
|
}
|
|
|
|
function previewStore(options) {
|
|
const root = rootFor(options);
|
|
const { carrier } = selectedCarrier(options);
|
|
const state = ownership(root) ? readState(root) : null;
|
|
revisionCheck(options, state);
|
|
return { ...result(root, state, exists(path.join(root, 'pending.json'))), status: 'proposed',
|
|
carrierDigest: carrier.carrierDigest, proposedProfileId: carrier.profileId,
|
|
proposedSelectedIds: carrier.selectedIds, proposedGenerationRoot: path.join(generationPath(root, carrier.carrierDigest), 'payload') };
|
|
}
|
|
|
|
function withLock(root, recover, run) {
|
|
const lockPath = path.join(root, '.lock');
|
|
if (exists(lockPath)) {
|
|
const lock = io.readJson(lockPath);
|
|
if (!recover || lock.hostname !== os.hostname() || !Number.isSafeInteger(lock.pid) || lock.pid < 1) throw new Error('Managed store lock requires recovery');
|
|
try { process.kill(lock.pid, 0); throw new Error('Managed store lock is held by a live process'); }
|
|
catch (error) { if (error.code !== 'ESRCH') throw error; }
|
|
if (!equal(io.readJson(lockPath), lock)) throw new Error('Managed store lock changed');
|
|
fs.unlinkSync(lockPath);
|
|
}
|
|
const lock = { pid: process.pid, hostname: os.hostname(), nonce: crypto.randomUUID() };
|
|
io.writeExclusive(lockPath, io.jsonBytes(lock));
|
|
try { return run(); }
|
|
finally {
|
|
if (equal(io.readJson(lockPath), lock)) { fs.unlinkSync(lockPath); io.syncDirectory(root); }
|
|
}
|
|
}
|
|
|
|
function checkpoint(options, name, detail = {}) { if (options.onCheckpoint) options.onCheckpoint(name, detail); }
|
|
|
|
function publishGeneration(root, pending, options, carrierOptions) {
|
|
const final = generationPath(root, pending.carrier.carrierDigest);
|
|
if (exists(final)) { loadGeneration(root, pending.carrier.carrierDigest); return; }
|
|
const staging = path.join(root, 'generations', `stage-${pending.transactionDigest}`);
|
|
io.mkdir(staging); io.mkdir(path.join(staging, 'payload'));
|
|
const reader = createSourceReader(options.repoRoot);
|
|
for (const file of pending.carrier.files) {
|
|
const resource = file.kind === 'copy' ? reader.read(file.sourcePath) : { content: Buffer.from(file.content, 'utf8') };
|
|
if (io.hash(resource.content) !== file.digest || resource.content.length !== file.bytes) throw new Error('Canonical source digest changed during materialization');
|
|
const relative = `payload/${file.destinationPath}`;
|
|
io.ensureParents(staging, relative);
|
|
const destination = path.join(staging, relative);
|
|
io.writeExclusive(destination, resource.content);
|
|
checkpoint(options, 'file-written', { path: destination });
|
|
}
|
|
if (!equal(planContextCarrier(carrierOptions), pending.carrier)) throw new Error('Canonical source changed during materialization');
|
|
io.writeExclusive(path.join(staging, 'carrier.json'), io.jsonBytes(pending.carrier));
|
|
verifyGeneration(staging, pending.carrier);
|
|
io.inspect(final, true);
|
|
if (exists(final)) throw new Error('Generation appeared during materialization');
|
|
fs.renameSync(staging, final); io.syncDirectory(path.dirname(final));
|
|
}
|
|
|
|
function publishReceipt(root, receipt) {
|
|
const file = path.join(root, 'receipts', `${digestObject(receipt)}.json`);
|
|
if (exists(file)) {
|
|
if (!equal(io.readJson(file), receipt)) throw new Error('Managed immutable receipt changed');
|
|
} else io.writeExclusive(file, io.jsonBytes(receipt));
|
|
}
|
|
|
|
function transaction(root, before, carrier, operation, options, carrierOptions) {
|
|
const receipt = { schemaVersion: 'ecc.context-store-receipt.v1', destinationDigest: digestObject({ root }),
|
|
operation, revision: (before?.revision || 0) + 1, generationDigest: carrier.carrierDigest,
|
|
previousGenerationDigest: before?.generationDigest || null, previousReceiptDigest: before?.receiptDigest || null,
|
|
selection: selectionFor(carrier, carrierOptions) };
|
|
const body = { schemaVersion: 'ecc.context-store-transaction.v1', before, after: stateFor(receipt), receipt, carrier };
|
|
const pending = { ...body, transactionDigest: digestObject(body) };
|
|
io.atomicJson(path.join(root, 'pending.json'), pending); checkpoint(options, 'prepared');
|
|
publishGeneration(root, pending, options, carrierOptions); checkpoint(options, 'generation-published');
|
|
publishReceipt(root, receipt); checkpoint(options, 'receipt-published');
|
|
if (!equal(readState(root), before)) throw new Error('Managed state changed during transaction');
|
|
loadGeneration(root, carrier.carrierDigest);
|
|
io.atomicJson(path.join(root, 'state.json'), pending.after); checkpoint(options, 'state-published');
|
|
fs.unlinkSync(path.join(root, 'pending.json')); io.syncDirectory(root);
|
|
return result(root, readState(root));
|
|
}
|
|
|
|
function applyStore(options) {
|
|
const root = rootFor(options);
|
|
const { carrier, carrierOptions } = selectedCarrier(options);
|
|
if (ownership(root)) { revisionCheck(options, readState(root)); }
|
|
else revisionCheck(options, null);
|
|
ownership(root, true);
|
|
return withLock(root, false, () => {
|
|
if (exists(path.join(root, 'pending.json'))) throw new Error('Managed transaction requires recovery');
|
|
const before = readState(root); revisionCheck(options, before);
|
|
if (!equal(planContextCarrier(carrierOptions), carrier)) throw new Error('Canonical source digest changed before apply');
|
|
if (before?.generationDigest === carrier.carrierDigest
|
|
&& equal(before.selection, selectionFor(carrier, carrierOptions))) return result(root, before);
|
|
io.mkdir(path.join(root, 'generations')); io.mkdir(path.join(root, 'receipts'));
|
|
return transaction(root, before, carrier, 'apply', options, carrierOptions);
|
|
});
|
|
}
|
|
|
|
function rollbackStore(options) {
|
|
const root = rootFor(options);
|
|
if (!ownership(root)) throw new Error('Managed store has no previous generation');
|
|
return withLock(root, false, () => {
|
|
if (exists(path.join(root, 'pending.json'))) throw new Error('Managed transaction requires recovery');
|
|
const before = readState(root); revisionCheck(options, before);
|
|
if (!before?.previousGenerationDigest) throw new Error('Managed store has no previous generation');
|
|
const carrier = loadGeneration(root, before.previousGenerationDigest);
|
|
const receipt = io.readJson(path.join(root, 'receipts', `${before.receiptDigest}.json`));
|
|
if (!DIGEST.test(receipt.previousReceiptDigest)) throw new Error('Previous receipt digest is invalid');
|
|
const previous = io.readJson(path.join(root, 'receipts', `${receipt.previousReceiptDigest}.json`));
|
|
if (digestObject(previous) !== receipt.previousReceiptDigest || previous.generationDigest !== carrier.carrierDigest) throw new Error('Previous receipt integrity mismatch');
|
|
return transaction(root, before, carrier, 'rollback', options, previous.selection);
|
|
});
|
|
}
|
|
|
|
function readPending(root) {
|
|
const pending = io.readJson(path.join(root, 'pending.json'));
|
|
const { transactionDigest, ...body } = pending;
|
|
if (!DIGEST.test(transactionDigest) || digestObject(body) !== transactionDigest
|
|
|| pending.schemaVersion !== 'ecc.context-store-transaction.v1'
|
|
|| pending.receipt.destinationDigest !== digestObject({ root })
|
|
|| !equal(pending.after, stateFor(pending.receipt))
|
|
|| pending.after.revision !== (pending.before?.revision || 0) + 1
|
|
|| pending.receipt.previousGenerationDigest !== (pending.before?.generationDigest || null)
|
|
|| pending.receipt.previousReceiptDigest !== (pending.before?.receiptDigest || null)) throw new Error('Pending transaction integrity mismatch');
|
|
checkCarrier(pending.carrier, pending.after.generationDigest);
|
|
checkSelection(pending.receipt.selection, pending.carrier);
|
|
return pending;
|
|
}
|
|
|
|
function recoverStore(options) {
|
|
const root = rootFor(options);
|
|
if (!ownership(root)) return result(root, null);
|
|
return withLock(root, true, () => {
|
|
const before = readState(root); revisionCheck(options, before);
|
|
if (!exists(path.join(root, 'pending.json'))) return result(root, before);
|
|
const pending = readPending(root);
|
|
if (!equal(before, pending.before) && !equal(before, pending.after)) throw new Error('State changed outside the pending transaction');
|
|
const final = generationPath(root, pending.after.generationDigest);
|
|
const staging = path.join(root, 'generations', `stage-${pending.transactionDigest}`);
|
|
if (exists(final)) {
|
|
loadGeneration(root, pending.after.generationDigest);
|
|
if (exists(staging)) throw new Error('Ambiguous pending generation requires inspection');
|
|
publishReceipt(root, pending.receipt);
|
|
io.atomicJson(path.join(root, 'state.json'), pending.after);
|
|
} else {
|
|
if (!equal(before, pending.before)) throw new Error('Committed generation is missing');
|
|
if (exists(staging)) io.removeTree(staging, verifyGeneration(staging, pending.carrier, true));
|
|
}
|
|
fs.unlinkSync(path.join(root, 'pending.json')); io.syncDirectory(root);
|
|
return result(root, readState(root));
|
|
});
|
|
}
|
|
|
|
module.exports = { applyStore, getStoreStatus, previewStore, recoverStore, rollbackStore };
|