import { createHash, randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { resolveArchiveDescriptions } from "./flight-text.mjs"; import { fileURLToPath } from "node:url"; export const COA_ARCHIVE = Object.freeze({ repository: "srhinos/coa-datamine", revision: "5078f9ac22541c45c17a866c23aac6c12073de23", shortRevision: "5078f9ac2254", capturedAt: "2026-08-12T21:02:51+00:00", fetchManifestPath: "raw/talents/_fetch.json", builderSnapshotPath: "raw/talents/coa-builder-voljin.html", builderSnapshotSha256: "41a6ccd393c0648ae2b1fdc34fe94955d4fd13c0cd59040ad3860e64a8ff6c7a", }); const defaultProjectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); const expectedClassCount = 21; const cacheSchemaVersion = 1; const manifestName = "manifest.json"; function assertSafeRelativePath(value, context = "archive path") { if (typeof value !== "string" || value.length === 0) { throw new Error(`${context} must be a non-empty string`); } const normalized = value.replaceAll("\\", "/"); if (normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized)) { throw new Error(`${context} must be relative: ${value}`); } if (normalized.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) { throw new Error(`${context} contains an unsafe path segment: ${value}`); } return normalized; } function classFileFromDescriptor(descriptor) { let candidate = null; if (typeof descriptor === "string") { candidate = descriptor; } else if (descriptor && typeof descriptor === "object") { candidate = descriptor.path ?? descriptor.file ?? descriptor.filename ?? descriptor.href ?? null; if (candidate === null) candidate = descriptor.slug ?? descriptor.key ?? descriptor.id ?? null; } if (candidate === null || candidate === undefined) { throw new Error("CoA talent class index contains an entry without a file path or class key"); } const relative = assertSafeRelativePath(String(candidate), "CoA talent class file reference"); let filePath; if (relative.startsWith("data/talents/coa/classes/")) { filePath = relative; } else if (relative.startsWith("classes/")) { filePath = `data/talents/coa/${relative}`; } else if (relative.startsWith("data/talents/coa/") && !relative.slice("data/talents/coa/".length).includes("/")) { filePath = relative; } else if (relative.includes("/")) { throw new Error(`CoA talent class file is outside the indexed CoA talent data: ${candidate}`); } else { filePath = `data/talents/coa/${relative.endsWith(".json") ? relative : `${relative}.json`}`; } if (!filePath.startsWith("data/talents/coa/") || !filePath.endsWith(".json") || filePath === "data/talents/coa/index.json" || filePath === "data/talents/coa/_meta.json") { throw new Error(`CoA talent class file is outside the indexed CoA talent data: ${candidate}`); } return filePath; } function isClassArchivePath(filePath) { return filePath.startsWith("data/talents/coa/") && filePath.endsWith(".json") && filePath !== "data/talents/coa/index.json" && filePath !== "data/talents/coa/_meta.json"; } function descriptorsFromIndex(index) { const entries = index?.classes; if (Array.isArray(entries)) return entries; if (entries && typeof entries === "object") { return Object.entries(entries).map(([key, value]) => { if (typeof value === "string") return value; if (value && typeof value === "object") return { key, ...value }; return { key }; }); } throw new Error("CoA talent index has no classes array or object"); } function classPathsFromIndex(index) { const descriptors = descriptorsFromIndex(index); if (descriptors.length !== expectedClassCount) { throw new Error(`CoA talent index expected ${expectedClassCount} class files, found ${descriptors.length}`); } const paths = descriptors.map(classFileFromDescriptor); if (new Set(paths).size !== expectedClassCount) { throw new Error("CoA talent index contains duplicate class file references"); } return paths; } function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); } function gitBlobSha(bytes) { return createHash("sha1") .update(Buffer.from(`blob ${bytes.byteLength}\0`, "utf8")) .update(bytes) .digest("hex"); } function parseJson(bytes, filePath) { try { return JSON.parse(bytes.toString("utf8")); } catch (error) { throw new Error(`CoA archive JSON is invalid at ${filePath}: ${error.message}`, { cause: error }); } } function isRegularBlob(entry) { return entry?.type === "blob" && entry?.mode === "100644" && /^[0-9a-f]{40}$/i.test(entry.sha ?? ""); } export function createSupplementCacheApi(archiveInput = COA_ARCHIVE) { const archive = Object.freeze({ ...archiveInput }); for (const key of ["repository", "revision", "shortRevision", "capturedAt", "fetchManifestPath", "builderSnapshotPath", "builderSnapshotSha256"]) { if (typeof archive[key] !== "string" || archive[key].length === 0) { throw new TypeError(`archive.${key} must be a non-empty string`); } } if (!/^[0-9a-f]{40}$/i.test(archive.revision) || !/^[0-9a-f]{7,40}$/i.test(archive.shortRevision)) { throw new TypeError("archive revision values must be hexadecimal Git revisions"); } if (!/^[0-9a-f]{64}$/i.test(archive.builderSnapshotSha256)) { throw new TypeError("archive.builderSnapshotSha256 must be a SHA-256 hex digest"); } const requiredPaths = [ archive.fetchManifestPath, archive.builderSnapshotPath, "data/talents/coa/index.json", "data/talents/coa/_meta.json", "raw/provenance.json", "raw/_snapshot.json", "data/spells/_meta.json", ]; function archiveDirectory(cacheRoot) { if (typeof cacheRoot !== "string" || cacheRoot.length === 0) { throw new TypeError("cacheRoot must be a non-empty directory path"); } return path.join(path.resolve(cacheRoot), `archive-${archive.shortRevision}`); } function archiveUrls(filePath) { const apiUrl = `https://api.github.com/repos/${archive.repository}/git/trees/${archive.revision}?recursive=1`; const rawPath = filePath.split("/").map(encodeURIComponent).join("/"); const rawUrl = `https://raw.githubusercontent.com/${archive.repository}/${archive.revision}/${rawPath}`; return { apiUrl, rawUrl }; } function parseCaptureManifest(bytes) { const capture = parseJson(bytes, archive.fetchManifestPath); if (capture?.capturedUtc !== archive.capturedAt) { throw new Error(`CoA capture manifest has an unexpected capture time: ${String(capture?.capturedUtc)}`); } if (capture?.savedAs !== path.posix.basename(archive.builderSnapshotPath)) { throw new Error(`CoA capture manifest has an unexpected savedAs value: ${String(capture?.savedAs)}`); } if (capture?.sha256 !== archive.builderSnapshotSha256) { throw new Error(`CoA capture manifest does not pin builder SHA-256 ${archive.builderSnapshotSha256}`); } return capture; } function sourceRecord() { return { repository: `https://github.com/${archive.repository}`, revision: archive.revision, capturedAt: archive.capturedAt, captureManifestPath: archive.fetchManifestPath, builderSnapshotPath: archive.builderSnapshotPath, builderSnapshotSha256: archive.builderSnapshotSha256, }; } function validateManifestShape(manifest) { if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) { throw new Error("CoA supplement manifest is not an object"); } if (manifest.schemaVersion !== cacheSchemaVersion) { throw new Error(`Unsupported CoA supplement cache schema: ${String(manifest.schemaVersion)}`); } if (manifest.source?.revision !== archive.revision) { throw new Error("CoA supplement manifest points at an unexpected archive revision"); } if (typeof manifest.generation !== "string" || !/^generation-[0-9a-f-]+$/.test(manifest.generation)) { throw new Error("CoA supplement manifest has an invalid generation name"); } if (!Array.isArray(manifest.files)) throw new Error("CoA supplement manifest has no file list"); const source = manifest.source; if (source.captureManifestPath !== archive.fetchManifestPath || source.builderSnapshotPath !== archive.builderSnapshotPath || source.builderSnapshotSha256 !== archive.builderSnapshotSha256 || source.capturedAt !== archive.capturedAt) { throw new Error("CoA supplement manifest has unexpected capture metadata"); } } function expectedFilesFromManifest(manifest) { validateManifestShape(manifest); const expected = new Map(); for (const file of manifest.files) { const filePath = assertSafeRelativePath(file?.path, "CoA supplement manifest file path"); if (expected.has(filePath)) throw new Error(`CoA supplement manifest repeats ${filePath}`); if (!/^[0-9a-f]{40}$/i.test(file.gitBlobSha ?? "")) { throw new Error(`CoA supplement manifest has an invalid Git blob SHA for ${filePath}`); } if (!/^[0-9a-f]{64}$/i.test(file.sha256 ?? "")) { throw new Error(`CoA supplement manifest has an invalid SHA-256 for ${filePath}`); } if (!Number.isSafeInteger(file.size) || file.size < 0) { throw new Error(`CoA supplement manifest has an invalid size for ${filePath}`); } expected.set(filePath, file); } for (const filePath of requiredPaths) { if (!expected.has(filePath)) throw new Error(`CoA supplement manifest is missing ${filePath}`); } const classPaths = [...expected.keys()].filter(isClassArchivePath); if (classPaths.length !== expectedClassCount || expected.size !== requiredPaths.length + expectedClassCount) { throw new Error(`CoA supplement manifest must contain ${expectedClassCount} class files and ${requiredPaths.length} required files`); } return expected; } async function responseBytes(fetchImpl, url, accept) { let response; try { response = await fetchImpl(url, { headers: { Accept: accept } }); } catch (error) { throw new Error(`CoA supplemental source unavailable at ${url}: ${error.message}`, { cause: error }); } if (!response || response.ok !== true) { const status = response?.status === undefined ? "no response" : `HTTP ${response.status}`; throw new Error(`CoA supplemental source unavailable at ${url} (${status})`); } try { if (typeof response.arrayBuffer === "function") return Buffer.from(await response.arrayBuffer()); if (typeof response.text === "function") return Buffer.from(await response.text(), "utf8"); } catch (error) { throw new Error(`CoA supplemental source read failed at ${url}: ${error.message}`, { cause: error }); } throw new Error(`CoA supplemental source response has no readable body at ${url}`); } async function fetchTree(fetchImpl) { const { apiUrl } = archiveUrls(archive.fetchManifestPath); const bytes = await responseBytes(fetchImpl, apiUrl, "application/vnd.github+json"); const treeDocument = parseJson(bytes, "GitHub archive tree"); if (treeDocument.truncated === true) { throw new Error("CoA archive tree was truncated; refusing an incomplete supplemental cache"); } if (!Array.isArray(treeDocument.tree)) throw new Error("CoA archive tree response does not contain a tree array"); return new Map(treeDocument.tree.map((entry) => [entry.path, entry])); } function expectedFile(tree, filePath) { const safePath = assertSafeRelativePath(filePath); const entry = tree.get(safePath); if (!isRegularBlob(entry)) throw new Error(`Pinned CoA archive tree is missing a regular file at ${safePath}`); if (!Number.isSafeInteger(entry.size) || entry.size < 0) { throw new Error(`Pinned CoA archive tree has no valid file size for ${safePath}`); } return { path: safePath, gitBlobSha: entry.sha.toLowerCase(), size: entry.size }; } async function readManifest(archiveRoot) { try { return parseJson(await fs.readFile(path.join(archiveRoot, manifestName)), manifestName); } catch (error) { if (error.code === "ENOENT") return null; throw error; } } async function readFileUnder(root, relativePath) { const safePath = assertSafeRelativePath(relativePath); const absoluteRoot = path.resolve(root); const absolutePath = path.resolve(absoluteRoot, ...safePath.split("/")); if (!absolutePath.startsWith(`${absoluteRoot}${path.sep}`)) { throw new Error(`CoA archive path escapes its cache directory: ${relativePath}`); } return fs.readFile(absolutePath); } async function verifyFile(root, fileRecord) { const bytes = await readFileUnder(root, fileRecord.path); if (bytes.byteLength !== fileRecord.size) throw new Error(`CoA supplement size mismatch for ${fileRecord.path}`); if (gitBlobSha(bytes) !== fileRecord.gitBlobSha.toLowerCase()) { throw new Error(`CoA supplement Git blob checksum mismatch for ${fileRecord.path}`); } const actualSha256 = sha256(bytes); if (actualSha256 !== fileRecord.sha256.toLowerCase()) { throw new Error(`CoA supplement SHA-256 mismatch for ${fileRecord.path}`); } if (fileRecord.path === archive.fetchManifestPath) parseCaptureManifest(bytes); if (fileRecord.path === archive.builderSnapshotPath && actualSha256 !== archive.builderSnapshotSha256) { throw new Error(`CoA builder snapshot does not match pinned SHA-256 ${archive.builderSnapshotSha256}`); } return bytes; } async function loadPublishedCache(archiveRoot) { const manifest = await readManifest(archiveRoot); if (manifest === null) return null; const expected = expectedFilesFromManifest(manifest); const generationRoot = path.join(archiveRoot, manifest.generation); const contents = new Map(); for (const [filePath, record] of expected) contents.set(filePath, await verifyFile(generationRoot, record)); parseCaptureManifest(contents.get(archive.fetchManifestPath)); const indexPath = "data/talents/coa/index.json"; const index = parseJson(contents.get(indexPath), indexPath); const classPaths = classPathsFromIndex(index); const recordedClassPaths = [...expected.keys()] .filter(isClassArchivePath) .sort(); if (classPaths.slice().sort().join("\n") !== recordedClassPaths.join("\n")) { throw new Error("CoA supplement manifest class files do not match data/talents/coa/index.json"); } const metadata = { talents: parseJson(contents.get("data/talents/coa/_meta.json"), "data/talents/coa/_meta.json"), provenance: parseJson(contents.get("raw/provenance.json"), "raw/provenance.json"), clientSnapshot: parseJson(contents.get("raw/_snapshot.json"), "raw/_snapshot.json"), spells: parseJson(contents.get("data/spells/_meta.json"), "data/spells/_meta.json"), }; const descriptors = descriptorsFromIndex(index); const rawClasses = classPaths.map((filePath) => parseJson(contents.get(filePath), filePath)); const hasTextReferences = rawClasses.some((definition) => definition.nodes?.some((node) => ( /^\$[0-9a-f]+$/i.test(node.description ?? "") || node.rankDescriptions?.some((rank) => /^\$[0-9a-f]+$/i.test(rank.description ?? "")) ))); const classes = hasTextReferences ? resolveArchiveDescriptions(rawClasses, contents.get(archive.builderSnapshotPath).toString("utf8")) : rawClasses; const classFiles = classPaths.map((filePath, position) => ({ path: filePath, indexEntry: descriptors[position], data: classes[position] })); return { source: structuredClone(manifest.source), classes, classFiles, metadata, index, manifest: structuredClone(manifest), }; } async function atomicWrite(filePath, bytes) { await fs.mkdir(path.dirname(filePath), { recursive: true }); const temporaryPath = `${filePath}.tmp-${randomUUID()}`; try { await fs.writeFile(temporaryPath, bytes, { flag: "wx" }); await fs.rename(temporaryPath, filePath); } catch (error) { await fs.rm(temporaryPath, { force: true }).catch(() => {}); throw error; } } async function discardInvalidFile(filePath) { try { await fs.rm(filePath, { force: true }); } catch (error) { throw new Error(`Could not discard invalid cached CoA supplement ${filePath}: ${error.message}`, { cause: error }); } } async function readAndVerifyStaged(stagingRoot, expected) { let bytes; try { bytes = await readFileUnder(stagingRoot, expected.path); } catch (error) { if (error.code === "ENOENT") return null; throw error; } const stagedPath = path.join(stagingRoot, ...expected.path.split("/")); if (bytes.byteLength !== expected.size || gitBlobSha(bytes) !== expected.gitBlobSha) { await discardInvalidFile(stagedPath); return null; } const contentSha256 = sha256(bytes); if (expected.path === archive.fetchManifestPath) { try { parseCaptureManifest(bytes); } catch { await discardInvalidFile(stagedPath); return null; } } if (expected.path === archive.builderSnapshotPath && contentSha256 !== archive.builderSnapshotSha256) { await discardInvalidFile(stagedPath); return null; } return { path: expected.path, gitBlobSha: expected.gitBlobSha, sha256: contentSha256, size: bytes.byteLength }; } async function fetchAndPromoteFile(fetchImpl, stagingRoot, expected) { const { rawUrl } = archiveUrls(expected.path); const bytes = await responseBytes(fetchImpl, rawUrl, "application/octet-stream"); if (gitBlobSha(bytes) !== expected.gitBlobSha) { throw new Error(`Downloaded CoA supplement Git blob checksum mismatch for ${expected.path}`); } const contentSha256 = sha256(bytes); if (expected.path === archive.fetchManifestPath) parseCaptureManifest(bytes); if (expected.path === archive.builderSnapshotPath && contentSha256 !== archive.builderSnapshotSha256) { throw new Error(`Downloaded CoA builder snapshot SHA-256 mismatch for ${expected.path}`); } const record = { path: expected.path, gitBlobSha: expected.gitBlobSha, sha256: contentSha256, size: bytes.byteLength }; const stagedPath = path.join(stagingRoot, ...expected.path.split("/")); await atomicWrite(stagedPath, bytes); await verifyFile(stagingRoot, record); return record; } async function stageFile(fetchImpl, stagingRoot, expected) { const cached = await readAndVerifyStaged(stagingRoot, expected); if (cached) return { record: cached, downloaded: false }; return { record: await fetchAndPromoteFile(fetchImpl, stagingRoot, expected), downloaded: true }; } async function validateGeneration(generationRoot, manifest) { const expected = expectedFilesFromManifest(manifest); const contents = new Map(); for (const [filePath, record] of expected) contents.set(filePath, await verifyFile(generationRoot, record)); const index = parseJson(contents.get("data/talents/coa/index.json"), "data/talents/coa/index.json"); const indexedClassPaths = classPathsFromIndex(index).sort(); const recordedClassPaths = [...expected.keys()] .filter(isClassArchivePath) .sort(); if (indexedClassPaths.join("\n") !== recordedClassPaths.join("\n")) { throw new Error("CoA supplement generation class files do not match the pinned index"); } parseCaptureManifest(contents.get(archive.fetchManifestPath)); for (const filePath of [ "data/talents/coa/_meta.json", "raw/provenance.json", "raw/_snapshot.json", "data/spells/_meta.json", ...indexedClassPaths, ]) { parseJson(contents.get(filePath), filePath); } } async function writeManifest(archiveRoot, manifest) { await atomicWrite( path.join(archiveRoot, manifestName), Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"), ); } function cacheSummary(manifest, cacheDirectory, reused, downloadedFiles = []) { return { source: structuredClone(manifest.source), cacheDirectory, revision: archive.revision, capturedAt: archive.capturedAt, fileCount: manifest.files.length, classCount: expectedClassCount, reused, downloadedFiles, }; } async function readSupplements({ cacheRoot } = {}) { const archiveRoot = archiveDirectory(cacheRoot); try { await fs.access(archiveRoot); } catch (error) { if (error.code === "ENOENT") return null; throw error; } try { const contents = await loadPublishedCache(archiveRoot); if (contents === null) throw new Error("published manifest is missing"); return contents; } catch (error) { throw new Error(`CoA supplement cache at ${archiveRoot} is corrupt or incomplete: ${error.message}`, { cause: error }); } } async function downloadSupplements({ cacheRoot, fetchImpl = globalThis.fetch, refresh = false } = {}) { if (typeof fetchImpl !== "function") throw new TypeError("fetchImpl must be a fetch-compatible function"); const archiveRoot = archiveDirectory(cacheRoot); if (!refresh) { try { const cached = await loadPublishedCache(archiveRoot); if (cached) return cacheSummary(cached.manifest, archiveRoot, true); } catch { // Keep the previous manifest and generation until a complete replacement is ready. } } await fs.mkdir(archiveRoot, { recursive: true }); const tree = await fetchTree(fetchImpl); const expected = new Map(requiredPaths.map((filePath) => [filePath, expectedFile(tree, filePath)])); const stagingRoot = path.join(archiveRoot, "staging"); const stagedRecords = new Map(); const downloadedFiles = []; for (const filePath of requiredPaths) { const staged = await stageFile(fetchImpl, stagingRoot, expected.get(filePath)); stagedRecords.set(filePath, staged.record); if (staged.downloaded) downloadedFiles.push(filePath); } const indexPath = "data/talents/coa/index.json"; const index = parseJson(await readFileUnder(stagingRoot, indexPath), indexPath); const classPaths = classPathsFromIndex(index); for (const filePath of classPaths) expected.set(filePath, expectedFile(tree, filePath)); for (const filePath of classPaths) { const staged = await stageFile(fetchImpl, stagingRoot, expected.get(filePath)); stagedRecords.set(filePath, staged.record); if (staged.downloaded) downloadedFiles.push(filePath); } const files = [...stagedRecords.values()].sort((left, right) => left.path.localeCompare(right.path)); if (files.length !== requiredPaths.length + expectedClassCount) { throw new Error(`CoA supplemental cache expected ${requiredPaths.length + expectedClassCount} files but staged ${files.length}`); } parseCaptureManifest(await readFileUnder(stagingRoot, archive.fetchManifestPath)); const manifest = { schemaVersion: cacheSchemaVersion, source: sourceRecord(), generation: `generation-${randomUUID()}`, files, }; // Validate every staged byte before moving the resumable cache into an immutable generation. await validateGeneration(stagingRoot, manifest); const generationRoot = path.join(archiveRoot, manifest.generation); await fs.rename(stagingRoot, generationRoot); await writeManifest(archiveRoot, manifest); return cacheSummary(manifest, archiveRoot, false, downloadedFiles); } return Object.freeze({ readSupplements, downloadSupplements }); } const defaultApi = createSupplementCacheApi(); export const readSupplements = defaultApi.readSupplements; export const downloadSupplements = defaultApi.downloadSupplements; export function getDefaultSupplementCacheRoot(projectRoot = defaultProjectRoot) { return path.join(path.resolve(projectRoot), "data", "coa-reference", "cache"); }