128 lines
7.3 KiB
JavaScript
128 lines
7.3 KiB
JavaScript
// Synthetic-only publication simulator. No caller-selected filesystem root,
|
|
// production credential adapter, real Pi runner, service or live activation.
|
|
import { mkdtemp, mkdir, open, rename, rm, lstat } from 'node:fs/promises';
|
|
import { constants } from 'node:fs';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { FixtureError, exact, refuse, resolveFixtureExecution, validateFixtureCredential } from './execution.mjs';
|
|
import { isFixtureStore } from './fixture-store.mjs';
|
|
import { refreshFixtureCredential, validateRefreshOptions } from './refresh-fixture.mjs';
|
|
const anchor = h => `/proc/self/fd/${h.fd}`;
|
|
async function openDir(path) { return open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW); }
|
|
async function verifyJson(dir, name, value) {
|
|
const expected = Buffer.from(JSON.stringify(value, null, 2) + '\n');
|
|
const h = await open(`${anchor(dir)}/${name}`, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
try {
|
|
const s = await h.stat();
|
|
if (!s.isFile() || s.uid !== process.getuid() || (s.mode & 0o777) !== 0o600 || s.size !== expected.length) refuse('generation-validation-failed');
|
|
const actual = Buffer.alloc(expected.length + 1);
|
|
let size = 0;
|
|
while (size < actual.length) {
|
|
const { bytesRead } = await h.read(actual, size, actual.length - size, null);
|
|
if (!bytesRead) break;
|
|
size += bytesRead;
|
|
}
|
|
if (size !== expected.length || !actual.subarray(0, size).equals(expected)) refuse('generation-validation-failed');
|
|
} finally { await h.close(); }
|
|
}
|
|
async function writeJson(dir, name, value) {
|
|
const content = JSON.stringify(value, null, 2) + '\n';
|
|
if (Buffer.byteLength(content) > 1024 * 1024) refuse('generation-too-large');
|
|
const h = await open(`${anchor(dir)}/${name}`, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
try { await h.writeFile(content); await h.sync(); }
|
|
finally { await h.close(); }
|
|
}
|
|
export async function createFixtureWorkspace() {
|
|
if (process.platform !== 'linux') refuse('unsupported-platform');
|
|
const path = await mkdtemp('/tmp/mosaic-materialization-fixture-');
|
|
const root = await openDir(path), identity = await root.stat();
|
|
let closed = false, active = 0;
|
|
const handles = {};
|
|
try {
|
|
for (const name of ['claims', 'pending', 'generations']) {
|
|
await mkdir(`${anchor(root)}/${name}`, { mode: 0o700 });
|
|
handles[name] = await openDir(`${anchor(root)}/${name}`);
|
|
}
|
|
} catch (e) {
|
|
for (const h of Object.values(handles)) await h.close();
|
|
await root.close(); await rm(path, { recursive: true, force: true }); throw e;
|
|
}
|
|
return Object.freeze({
|
|
fixtureOnly: true, path,
|
|
async generate(registry, request, store, options = {}) {
|
|
if (closed) refuse('workspace-closed');
|
|
if (!isFixtureStore(store)) refuse('fixture-store-required');
|
|
exact(options, [], ['fault', 'refresh']);
|
|
if (options.fault !== undefined && !['after-auth', 'before-publish', 'after-publish'].includes(options.fault)) refuse('invalid-fixture-option');
|
|
options = { fault: options.fault, refresh: options.refresh === undefined ? undefined : validateRefreshOptions(options.refresh) };
|
|
const plan = resolveFixtureExecution(registry, request), id = plan.scope.executionId;
|
|
const pending = `${anchor(handles.pending)}/${id}`, claimPath = `${anchor(handles.claims)}/${id}`;
|
|
let claim, generation, claimed = false, published = false;
|
|
active++;
|
|
try {
|
|
try { await mkdir(claimPath, { mode: 0o700 }); claimed = true; }
|
|
catch { refuse('execution-already-claimed'); }
|
|
claim = await openDir(claimPath);
|
|
await writeJson(claim, 'started.json', { fixtureOnly: true, executionId: id, state: 'started' });
|
|
const result = await store.transaction(Object.values(plan.accounts), async draft => {
|
|
const auth = Object.create(null);
|
|
for (const [provider, ref] of Object.entries(plan.accounts)) {
|
|
let credential = validateFixtureCredential(draft.get(ref), plan.types[provider]);
|
|
if (credential !== null && (options.refresh !== undefined ||
|
|
(credential.type === 'oauth' && credential.expires <= Date.now() + 300000))) {
|
|
credential = await refreshFixtureCredential(provider, credential, options.refresh ?? {});
|
|
draft.set(ref, credential);
|
|
}
|
|
if (credential !== null) auth[provider] = credential;
|
|
}
|
|
await mkdir(pending, { mode: 0o700 }); generation = await openDir(pending);
|
|
await writeJson(generation, 'auth.json', auth);
|
|
if (options.fault === 'after-auth') refuse('injected-generation-failure');
|
|
await writeJson(generation, 'models.json', plan.models);
|
|
const manifest = { manifestVersion: 1, fixtureOnly: true, manifestId: randomUUID(),
|
|
scope: plan.scope, profile: plan.profile, accounts: plan.accounts, fork: plan.fork,
|
|
artifacts: ['auth.json', 'models.json'], state: 'ready' };
|
|
// Manifest contains no token, expiry or credential-content digest.
|
|
await writeJson(generation, 'manifest.json', manifest);
|
|
if (options.fault === 'before-publish') refuse('injected-generation-failure');
|
|
await verifyJson(generation, 'auth.json', auth);
|
|
await verifyJson(generation, 'models.json', plan.models);
|
|
await verifyJson(generation, 'manifest.json', manifest);
|
|
await generation.sync(); await generation.close(); generation = undefined;
|
|
const target = `${anchor(handles.generations)}/${id}`;
|
|
try { await lstat(target); refuse('generation-exists'); }
|
|
catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
// Retained exclusive claim serializes this execution ID, including
|
|
// failed attempts. Published generations are never replaced by API.
|
|
await rename(pending, target); published = true;
|
|
if (options.fault === 'after-publish') refuse('injected-generation-failure');
|
|
await handles.generations.sync();
|
|
await writeJson(claim, 'result.json', { fixtureOnly: true, state: 'published', manifestId: manifest.manifestId });
|
|
return { fixtureOnly: true, manifest, generationPath: `${path}/generations/${id}` };
|
|
});
|
|
return result;
|
|
} catch (e) {
|
|
if (generation) { await generation.close(); generation = undefined; }
|
|
if (claimed && !published) await rm(pending, { recursive: true, force: true });
|
|
const code = published ? 'publication-uncertain' : e instanceof FixtureError ? e.code : 'generation-failed';
|
|
if (claim) {
|
|
try { await writeJson(claim, 'failure.json', { fixtureOnly: true, state: published ? 'uncertain' : 'failed', code }); }
|
|
catch { throw new FixtureError('recording-failed'); }
|
|
}
|
|
throw new FixtureError(code);
|
|
} finally {
|
|
if (claim) await claim.close(); active--;
|
|
}
|
|
},
|
|
async close() {
|
|
if (closed) return;
|
|
if (active) refuse('workspace-busy');
|
|
closed = true;
|
|
for (const h of Object.values(handles)) await h.close();
|
|
await root.close();
|
|
const current = await lstat(path);
|
|
if (current.dev !== identity.dev || current.ino !== identity.ino || current.isSymbolicLink()) refuse('workspace-path-changed');
|
|
await rm(path, { recursive: true, force: true });
|
|
},
|
|
});
|
|
}
|