📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-07-18 00:02:59 +00:00
parent 82f7c6e56a
commit 47ce7f78dc
1446 changed files with 141041 additions and 6442 deletions
@@ -0,0 +1,72 @@
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const crypto = require("node:crypto");
const { canonicalJson, sha256 } = require("../canonical-json");
const { transactionError } = require("./errors");
function compareStrings(left, right) {
return left < right ? -1 : (left > right ? 1 : 0);
}
function fileDigest(filePath) {
const hash = crypto.createHash("sha256");
const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
try {
const stat = fs.fstatSync(descriptor);
if (!stat.isFile()) throw transactionError("AAS_TRANSACTION_FILE_UNSAFE", "filesystem", {});
const buffer = Buffer.allocUnsafe(64 * 1024);
for (;;) {
const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, null);
if (bytes === 0) break;
hash.update(buffer.subarray(0, bytes));
}
} finally {
fs.closeSync(descriptor);
}
return `sha256-${hash.digest("hex")}`;
}
function treeManifest(root) {
const rootStat = fs.lstatSync(root);
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
throw transactionError("AAS_TRANSACTION_TREE_UNSAFE", "filesystem", { reason: "rootNotDirectory" });
}
const entries = [];
const collisionKeys = new Set();
function visit(directory, relativeDirectory) {
const names = fs.readdirSync(directory).sort(compareStrings);
for (const name of names) {
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
const normalized = relative.normalize("NFC").toLowerCase();
if (collisionKeys.has(normalized)) {
throw transactionError("AAS_TRANSACTION_TREE_COLLISION", "filesystem", { logicalPath: relative });
}
collisionKeys.add(normalized);
const absolute = path.join(directory, name);
const stat = fs.lstatSync(absolute);
if (stat.isSymbolicLink()) {
throw transactionError("AAS_TRANSACTION_TREE_SYMLINK", "filesystem", { logicalPath: relative });
}
if (stat.isDirectory()) {
entries.push({ path: relative, type: "directory" });
visit(absolute, relative);
} else if (stat.isFile() && stat.nlink === 1) {
entries.push({ path: relative, type: "file", size: stat.size, digest: fileDigest(absolute) });
} else {
throw transactionError("AAS_TRANSACTION_TREE_UNSAFE", "filesystem", { logicalPath: relative });
}
}
}
visit(root, "");
return { schemaVersion: 1, entries };
}
function treeDigest(root) {
return sha256(canonicalJson(treeManifest(root)));
}
module.exports = { fileDigest, treeDigest, treeManifest };
@@ -0,0 +1,11 @@
"use strict";
function transactionError(code, category, details = {}, cause) {
const error = new Error(code, cause ? { cause } : undefined);
error.code = code;
error.category = category;
error.details = details;
return error;
}
module.exports = { transactionError };
@@ -0,0 +1,9 @@
"use strict";
module.exports = {
...require("./digest"),
...require("./errors"),
...require("./journal"),
...require("./runtime"),
...require("./state"),
};
@@ -0,0 +1,219 @@
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const crypto = require("node:crypto");
const { canonicalJson, canonicalize, sha256 } = require("../canonical-json");
const { fsyncDirectory, writeFileDurable } = require("./state");
const { transactionError } = require("./errors");
const { validateInstance } = require("../schema-validator");
const MAX_JOURNAL_BYTES = 4 * 1024 * 1024;
function recoveryIdFor(planDigest, identityDigest) {
return `recovery-${sha256(`${planDigest}:${identityDigest}`).slice(7, 55)}`;
}
function journalPath(layoutRoot, recoveryId) {
if (!/^recovery-[a-f0-9]{32,64}$/.test(recoveryId)) {
throw transactionError("AAS_TRANSACTION_JOURNAL_ID_INVALID", "invalidInput", {});
}
return path.join(layoutRoot, `.aas-transaction-${recoveryId}.wal`);
}
function digestRecords(records) {
return sha256(canonicalJson(records));
}
function appendRecord(context, event, details = {}) {
if (context.tornTailDigest) {
throw transactionError("AAS_TRANSACTION_JOURNAL_TORN_TAIL", "recovery", {});
}
const previous = context.records.at(-1) || null;
const body = canonicalize({
schemaVersion: 1,
recoveryId: context.recoveryId,
sequence: context.records.length,
event,
planDigest: context.planDigest,
targetIdentityDigest: context.targetIdentityDigest,
details,
previousRecordDigest: previous ? previous.recordDigest : null,
});
const record = { ...body, recordDigest: sha256(canonicalJson(body)) };
const bytes = `${canonicalJson(record)}\n`;
const descriptor = fs.openSync(context.path, fs.constants.O_APPEND | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0));
try {
fs.writeFileSync(descriptor, bytes);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
context.records.push(record);
context.validBytes += Buffer.byteLength(bytes);
context.validBytesDigest = sha256(fs.readFileSync(context.path).subarray(0, context.validBytes));
context.digest = digestRecords(context.records);
return record;
}
function createJournal(layoutRoot, recoveryId, planDigest, targetIdentityDigest) {
const target = journalPath(layoutRoot, recoveryId);
const temporary = `${target}.pending-${crypto.randomBytes(16).toString("hex")}`;
if (fs.existsSync(target)) throw transactionError("AAS_TRANSACTION_RECOVERY_REQUIRED", "recovery", { recoveryId });
const context = {
recoveryId,
planDigest,
targetIdentityDigest,
path: temporary,
records: [],
validBytes: 0,
validBytesDigest: sha256(Buffer.alloc(0)),
tornTailDigest: null,
};
try {
writeFileDurable(temporary, "", 0o600);
appendRecord(context, "started", {});
fs.renameSync(temporary, target);
fsyncDirectory(layoutRoot);
context.path = target;
return context;
} catch (cause) {
try { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); } catch {}
throw cause;
}
}
function readJournalFile(target) {
const stat = fs.lstatSync(target);
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1 || stat.size > MAX_JOURNAL_BYTES) {
throw transactionError("AAS_TRANSACTION_JOURNAL_UNSAFE", "filesystem", {});
}
const bytes = fs.readFileSync(target);
const finalNewline = bytes.length > 0 && bytes.at(-1) === 0x0a;
const lastNewline = bytes.lastIndexOf(0x0a);
const validBytes = finalNewline ? bytes.length : (lastNewline < 0 ? 0 : lastNewline + 1);
const validText = bytes.subarray(0, validBytes).toString("utf8");
let records;
try {
records = validText.split("\n").filter(Boolean).map((line) => JSON.parse(line));
} catch (cause) {
throw transactionError("AAS_TRANSACTION_JOURNAL_CORRUPT", "integrity", {}, cause);
}
if (!records.length) throw transactionError("AAS_TRANSACTION_JOURNAL_CORRUPT", "integrity", {});
for (let index = 0; index < records.length; index += 1) {
const record = records[index];
validateInstance("journal.schema.json", record, "AAS_TRANSACTION_JOURNAL_SCHEMA_INVALID");
const { recordDigest, ...body } = record || {};
if (record.sequence !== index || record.previousRecordDigest !== (index ? records[index - 1].recordDigest : null)
|| recordDigest !== sha256(canonicalJson(body)) || record.recoveryId !== records[0].recoveryId
|| record.planDigest !== records[0].planDigest || record.targetIdentityDigest !== records[0].targetIdentityDigest) {
throw transactionError("AAS_TRANSACTION_JOURNAL_CORRUPT", "integrity", {});
}
}
const tornTail = bytes.subarray(validBytes);
return {
recoveryId: records[0].recoveryId,
planDigest: records[0].planDigest,
targetIdentityDigest: records[0].targetIdentityDigest,
path: target,
records,
digest: digestRecords(records),
validBytes,
validBytesDigest: sha256(bytes.subarray(0, validBytes)),
tornTailDigest: tornTail.length ? sha256(tornTail) : null,
};
}
function readJournal(layoutRoot, recoveryId) {
return readJournalFile(journalPath(layoutRoot, recoveryId));
}
function journalCheckpoint(journal) {
return canonicalize({
recordCount: journal.records.length,
recordsDigest: journal.digest,
validBytes: journal.validBytes,
validBytesDigest: journal.validBytesDigest,
tornTailDigest: journal.tornTailDigest,
});
}
function verifyCheckpoint(journal, checkpoint) {
if (!checkpoint || !Number.isSafeInteger(checkpoint.recordCount) || checkpoint.recordCount < 1
|| !Number.isSafeInteger(checkpoint.validBytes) || checkpoint.validBytes < 1
|| journal.records.length < checkpoint.recordCount || journal.validBytes < checkpoint.validBytes) {
throw transactionError("AAS_RECOVERY_JOURNAL_DRIFT", "drift", {});
}
const prefix = journal.records.slice(0, checkpoint.recordCount);
const raw = fs.readFileSync(journal.path).subarray(0, checkpoint.validBytes);
if (digestRecords(prefix) !== checkpoint.recordsDigest || sha256(raw) !== checkpoint.validBytesDigest) {
throw transactionError("AAS_RECOVERY_JOURNAL_DRIFT", "drift", {});
}
if (journal.records.length === checkpoint.recordCount && journal.tornTailDigest !== (checkpoint.tornTailDigest || null)) {
throw transactionError("AAS_RECOVERY_JOURNAL_DRIFT", "drift", {});
}
return true;
}
function repairTornTail(journal, checkpoint) {
verifyCheckpoint(journal, checkpoint);
if (!journal.tornTailDigest) return journal;
if (journal.records.length !== checkpoint.recordCount) {
throw transactionError("AAS_RECOVERY_JOURNAL_DRIFT", "drift", {});
}
const descriptor = fs.openSync(journal.path, fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0));
try {
fs.ftruncateSync(descriptor, checkpoint.validBytes);
fs.fsyncSync(descriptor);
} finally { fs.closeSync(descriptor); }
fsyncDirectory(path.dirname(journal.path));
return readJournalFile(journal.path);
}
function truncateTornTail(journal) {
if (!journal.tornTailDigest) return journal;
const descriptor = fs.openSync(journal.path, fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0));
try {
fs.ftruncateSync(descriptor, journal.validBytes);
fs.fsyncSync(descriptor);
} finally { fs.closeSync(descriptor); }
fsyncDirectory(path.dirname(journal.path));
return readJournalFile(journal.path);
}
function removeJournal(journal) {
const observed = readJournalFile(journal.path);
if (observed.recoveryId !== journal.recoveryId || observed.planDigest !== journal.planDigest) {
throw transactionError("AAS_TRANSACTION_JOURNAL_DRIFT", "drift", {});
}
fs.unlinkSync(journal.path);
fsyncDirectory(path.dirname(journal.path));
}
function listJournalIds(layoutRoot) {
return fs.readdirSync(layoutRoot)
.map((name) => /^\.aas-transaction-(recovery-[a-f0-9]{32,64})\.wal$/.exec(name))
.filter(Boolean)
.map((match) => match[1])
.sort();
}
function journalEvents(journal) {
return new Set(journal.records.map((record) => record.event));
}
module.exports = {
appendRecord,
createJournal,
journalCheckpoint,
journalEvents,
journalPath,
listJournalIds,
readJournal,
readJournalFile,
recoveryIdFor,
removeJournal,
repairTornTail,
truncateTornTail,
verifyCheckpoint,
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,267 @@
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const { transactionError } = require("./errors");
const { fsyncDirectory } = require("./state");
function isContained(root, candidate) {
const relative = path.relative(root, candidate);
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
}
function assertRegularDirectory(directory, code = "AAS_TRANSACTION_DIRECTORY_UNSAFE") {
const stat = fs.lstatSync(directory);
if (stat.isSymbolicLink() || !stat.isDirectory()) {
throw transactionError(code, "filesystem", {});
}
return stat;
}
function assertOwned(stat, code = "AAS_TRANSACTION_OWNERSHIP_UNSAFE") {
if (typeof process.getuid === "function" && typeof stat.uid === "number" && stat.uid !== process.getuid()) {
throw transactionError(code, "filesystem", {});
}
}
function assertNoSymlinkChain(root, candidate) {
if (!isContained(root, candidate)) {
throw transactionError("AAS_TRANSACTION_PATH_OUTSIDE_TARGET", "filesystem", {});
}
const relative = path.relative(root, candidate);
let cursor = root;
for (const part of relative.split(path.sep).filter(Boolean)) {
cursor = path.join(cursor, part);
if (!fs.existsSync(cursor)) break;
const stat = fs.lstatSync(cursor);
if (stat.isSymbolicLink()) {
throw transactionError("AAS_TRANSACTION_SYMLINK_PATH", "filesystem", {});
}
}
}
function inspectLayout(adapter, target) {
if (!adapter || typeof adapter.resolveTransactionLayout !== "function") {
throw transactionError("AAS_TRANSACTION_ADAPTER_INVALID", "invalidInput", {});
}
const layout = adapter.resolveTransactionLayout(target);
const keys = ["root", "skillsDirectory", "stateFile", "transactionDirectory"];
if (!layout || keys.some((key) => typeof layout[key] !== "string" || !path.isAbsolute(layout[key]))) {
throw transactionError("AAS_TRANSACTION_LAYOUT_INVALID", "invalidInput", {});
}
const lexicalRoot = path.resolve(layout.root);
const root = fs.realpathSync(layout.root);
const resolved = Object.fromEntries(keys.map((key) => [
key,
key === "root" ? root : path.resolve(root, path.relative(lexicalRoot, path.resolve(layout[key]))),
]));
const rootStat = assertRegularDirectory(root);
assertOwned(rootStat);
for (const key of ["skillsDirectory", "stateFile", "transactionDirectory"]) {
if (!isContained(root, resolved[key])) {
throw transactionError("AAS_TRANSACTION_PATH_OUTSIDE_TARGET", "filesystem", { logicalId: key });
}
assertNoSymlinkChain(root, resolved[key]);
}
const requiredDirectories = [...new Set([
...directoryClosure(root, resolved.skillsDirectory),
...directoryClosure(root, resolved.transactionDirectory),
])].sort((left, right) => left.split(path.sep).length - right.split(path.sep).length || (left < right ? -1 : 1));
const missingDirectories = [];
for (const directory of requiredDirectories) {
if (!fs.existsSync(directory)) {
missingDirectories.push(directory);
continue;
}
const stat = assertRegularDirectory(directory);
assertOwned(stat);
if (stat.dev !== rootStat.dev) throw transactionError("AAS_TRANSACTION_CROSS_FILESYSTEM", "filesystem", {});
}
if (fs.existsSync(resolved.stateFile)) {
const stateStat = fs.lstatSync(resolved.stateFile);
if (stateStat.isSymbolicLink() || !stateStat.isFile() || stateStat.nlink !== 1 || stateStat.dev !== rootStat.dev) {
throw transactionError("AAS_TRANSACTION_STATE_UNSAFE", "filesystem", {});
}
assertOwned(stateStat);
}
return Object.freeze({
...resolved,
device: rootStat.dev,
layoutDirectories: Object.freeze(requiredDirectories),
missingDirectories: Object.freeze(missingDirectories),
});
}
function directoryClosure(root, leaf) {
const directories = [];
let cursor = leaf;
while (cursor !== root) {
if (!isContained(root, cursor)) throw transactionError("AAS_TRANSACTION_PATH_OUTSIDE_TARGET", "filesystem", {});
directories.push(cursor);
const parent = path.dirname(cursor);
if (parent === cursor) throw transactionError("AAS_TRANSACTION_PATH_OUTSIDE_TARGET", "filesystem", {});
cursor = parent;
}
return directories.reverse();
}
function resolveLayout(adapter, target) {
const inspected = inspectLayout(adapter, target);
if (inspected.missingDirectories.length) {
throw transactionError("AAS_TRANSACTION_LAYOUT_MISSING", "filesystem", {
logicalIds: inspected.missingDirectories.map((directory) => path.relative(inspected.root, directory).split(path.sep).join("/")),
});
}
return inspected;
}
function ownershipMarker(options) {
if (!options || typeof options.markerName !== "string" || !/^\.aas-layout-recovery-[a-f0-9]{32,64}$/.test(options.markerName)
|| typeof options.markerToken !== "string" || !/^[a-f0-9]{48}$/.test(options.markerToken)) {
throw transactionError("AAS_TRANSACTION_LAYOUT_OWNERSHIP_INVALID", "integrity", {});
}
return { markerName: options.markerName, markerToken: options.markerToken };
}
function writeMarker(directory, markerName, markerToken) {
const marker = path.join(directory, markerName);
const descriptor = fs.openSync(marker, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0), 0o600);
try {
fs.writeFileSync(descriptor, `${markerToken}\n`);
fs.fsyncSync(descriptor);
} finally { fs.closeSync(descriptor); }
fsyncDirectory(directory);
}
function markerOwned(directory, markerName, markerToken) {
const marker = path.join(directory, markerName);
if (!fs.existsSync(marker)) return false;
const stat = fs.lstatSync(marker);
return !stat.isSymbolicLink() && stat.isFile() && stat.nlink === 1 && fs.readFileSync(marker, "utf8") === `${markerToken}\n`;
}
function materializeLayout(inspected, options) {
const { markerName, markerToken } = ownershipMarker(options);
const created = Array.isArray(options.createdDirectories) ? options.createdDirectories : [];
try {
for (const directory of inspected.missingDirectories) {
const parent = path.dirname(directory);
const parentStat = assertRegularDirectory(parent);
assertOwned(parentStat);
if (parentStat.dev !== inspected.device) throw transactionError("AAS_TRANSACTION_CROSS_FILESYSTEM", "filesystem", {});
assertNoSymlinkChain(inspected.root, directory);
const stage = path.join(parent, `.aas-layout-stage-${markerToken}-${path.basename(directory)}`);
try {
fs.mkdirSync(stage, { mode: 0o700 });
writeMarker(stage, markerName, markerToken);
if (fs.existsSync(directory)) throw transactionError("AAS_TRANSACTION_LAYOUT_CREATE_RACE", "conflict", {});
fs.renameSync(stage, directory);
// Publish ownership to the caller before the directory durability
// barrier. If that barrier fails, cleanup/recovery still knows the
// exact marker-bound directory that became visible.
created.push(directory);
if (typeof options.onBoundary === "function") {
options.onBoundary("layoutDirectoryPublished", {
logicalId: path.relative(inspected.root, directory).split(path.sep).join("/"),
});
}
fsyncDirectory(parent);
} catch (cause) {
try { fs.rmSync(stage, { recursive: true, force: true }); } catch {}
throw transactionError("AAS_TRANSACTION_LAYOUT_CREATE_FAILED", "filesystem", {}, cause);
}
const stat = assertRegularDirectory(directory);
assertOwned(stat);
if (stat.dev !== inspected.device) throw transactionError("AAS_TRANSACTION_CROSS_FILESYSTEM", "filesystem", {});
fsyncDirectory(parent);
}
return created;
} catch (error) {
cleanupMaterializedLayout(inspected, created, { markerName, markerToken });
throw error;
}
}
function cleanupMaterializedLayout(inspected, directories, options) {
const { markerName, markerToken } = ownershipMarker(options);
for (const directory of [...directories].reverse()) {
if (!isContained(inspected.root, directory)) continue;
const tombstone = path.join(path.dirname(directory), `.aas-layout-remove-${markerToken}-${path.basename(directory)}`);
// A prior cleanup may have published the exact token-bound tombstone and
// then failed its parent fsync. Reconcile that state before inspecting the
// original path so cleanup is retryable at every durability boundary.
if (fs.existsSync(tombstone)) {
const tombstoneStat = fs.lstatSync(tombstone);
if (tombstoneStat.isSymbolicLink() || !tombstoneStat.isDirectory() || tombstoneStat.dev !== inspected.device) continue;
try { assertOwned(tombstoneStat); } catch { continue; }
if (!markerOwned(tombstone, markerName, markerToken)) continue;
if (fs.readdirSync(tombstone).some((name) => name !== markerName)) continue;
fs.rmSync(tombstone, { recursive: true });
fsyncDirectory(path.dirname(tombstone));
continue;
}
if (!fs.existsSync(directory)) continue;
const stat = fs.lstatSync(directory);
if (stat.isSymbolicLink() || !stat.isDirectory() || stat.dev !== inspected.device) continue;
try { assertOwned(stat); } catch { continue; }
if (!markerOwned(directory, markerName, markerToken)) continue;
if (fs.readdirSync(directory).some((name) => name !== markerName)) continue;
fs.renameSync(directory, tombstone);
if (typeof options.onBoundary === "function") {
options.onBoundary("layoutDirectoryTombstoned", {
logicalId: path.relative(inspected.root, directory).split(path.sep).join("/"),
});
}
fsyncDirectory(path.dirname(directory));
fs.rmSync(tombstone, { recursive: true });
fsyncDirectory(path.dirname(tombstone));
}
}
function clearMaterializedMarkers(inspected, directories, options) {
const { markerName, markerToken } = ownershipMarker(options);
for (const directory of [...directories].reverse()) {
if (!fs.existsSync(directory) || !markerOwned(directory, markerName, markerToken)) continue;
fs.unlinkSync(path.join(directory, markerName));
fsyncDirectory(directory);
}
}
function resolveDestination(layout, skillId) {
const destination = path.resolve(layout.skillsDirectory, ...skillId.split("/"));
if (!isContained(layout.skillsDirectory, destination)) {
throw transactionError("AAS_TRANSACTION_PATH_OUTSIDE_TARGET", "filesystem", { logicalId: skillId });
}
assertNoSymlinkChain(layout.root, destination);
return destination;
}
function resolveSource(adapter, operation, layout, target) {
if (typeof adapter.resolveSourceTree !== "function") {
throw transactionError("AAS_TRANSACTION_ADAPTER_INVALID", "invalidInput", {});
}
const source = adapter.resolveSourceTree({ skillId: operation.skillId, operation, target });
if (typeof source !== "string" || !path.isAbsolute(source)) {
throw transactionError("AAS_TRANSACTION_SOURCE_INVALID", "invalidInput", { skillId: operation.skillId });
}
const real = fs.realpathSync(source);
assertRegularDirectory(real, "AAS_TRANSACTION_SOURCE_UNSAFE");
if (typeof adapter.validateSourceTree === "function" && adapter.validateSourceTree(real, operation) !== true) {
throw transactionError("AAS_TRANSACTION_SOURCE_REJECTED", "integrity", { skillId: operation.skillId });
}
return real;
}
module.exports = {
assertNoSymlinkChain,
assertOwned,
assertRegularDirectory,
cleanupMaterializedLayout,
clearMaterializedMarkers,
inspectLayout,
isContained,
materializeLayout,
resolveDestination,
resolveLayout,
resolveSource,
};
@@ -0,0 +1,98 @@
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const { fsyncDirectorySync } = require("../durability");
const { canonicalJson, sha256 } = require("../canonical-json");
const { transactionError } = require("./errors");
const { validateInstance } = require("../schema-validator");
function compareStrings(left, right) {
return left < right ? -1 : (left > right ? 1 : 0);
}
function publicEntries(entries) {
return entries.map(({ skillId, treeDigest, catalogIntegrity }) => ({ skillId, treeDigest, catalogIntegrity }))
.sort((left, right) => compareStrings(left.skillId, right.skillId));
}
function digestManagedEntries(entries) {
return sha256(canonicalJson({ schemaVersion: 1, entries: publicEntries(entries) }));
}
function buildManagedState({ target, catalog, entries, completedPlanDigests }) {
const normalizedEntries = entries.map((entry) => ({ ...entry }))
.sort((left, right) => compareStrings(left.skillId, right.skillId));
const state = {
schemaVersion: 1,
target: { ...target },
catalog: { ...catalog },
entries: normalizedEntries,
completedPlanDigests: [...new Set(completedPlanDigests)].sort(compareStrings).slice(-256),
stateDigest: digestManagedEntries(normalizedEntries),
};
return state;
}
function readManagedState(stateFile) {
if (!fs.existsSync(stateFile)) return null;
const stat = fs.lstatSync(stateFile);
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1 || stat.size > 1024 * 1024) {
throw transactionError("AAS_TRANSACTION_STATE_UNSAFE", "filesystem", {});
}
let state;
try {
state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
} catch (cause) {
throw transactionError("AAS_TRANSACTION_STATE_CORRUPT", "integrity", {}, cause);
}
if (!state || state.schemaVersion !== 1 || !Array.isArray(state.entries) || !Array.isArray(state.completedPlanDigests)) {
throw transactionError("AAS_TRANSACTION_STATE_CORRUPT", "integrity", {});
}
validateInstance("managed-state.schema.json", state, "AAS_TRANSACTION_STATE_SCHEMA_INVALID");
const ids = new Set();
for (const entry of state.entries) {
if (!entry || typeof entry.skillId !== "string" || ids.has(entry.skillId)
|| typeof entry.treeDigest !== "string" || typeof entry.catalogIntegrity !== "string"
|| typeof entry.installedByPlanDigest !== "string") {
throw transactionError("AAS_TRANSACTION_STATE_CORRUPT", "integrity", {});
}
ids.add(entry.skillId);
}
if (state.stateDigest !== digestManagedEntries(state.entries)) {
throw transactionError("AAS_TRANSACTION_STATE_DIGEST_MISMATCH", "integrity", {});
}
return state;
}
function fsyncDirectory(directory) {
fsyncDirectorySync(directory);
}
function writeFileDurable(filePath, bytes, mode = 0o600) {
const descriptor = fs.openSync(filePath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, mode);
try {
fs.writeFileSync(descriptor, bytes);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function atomicWriteManagedState(stateFile, state, transactionRoot) {
const temporary = path.join(transactionRoot, "managed-state.next.json");
if (fs.existsSync(temporary)) fs.rmSync(temporary, { force: true });
writeFileDurable(temporary, `${canonicalJson(state)}\n`);
fs.renameSync(temporary, stateFile);
fsyncDirectory(path.dirname(stateFile));
}
module.exports = {
atomicWriteManagedState,
buildManagedState,
digestManagedEntries,
fsyncDirectory,
publicEntries,
readManagedState,
writeFileDurable,
};