import { createHash } from 'node:crypto'; import { readdir, readFile, stat } from 'node:fs/promises'; import { resolve } from 'node:path'; async function hasFiles(directory) { const entries = await readdir(directory, { withFileTypes: true }).catch(() => []); for (const entry of entries) { if (entry.isFile()) return true; if (entry.isDirectory() && await hasFiles(resolve(directory, entry.name))) return true; } return false; } export async function validateLearningPlayerArtifact(directory) { const root = resolve(directory); const [artifactText, html] = await Promise.all([ readFile(resolve(root, 'artifact.json'), 'utf8'), readFile(resolve(root, 'index.html')), ]).catch((error) => { throw new Error(`Invalid learning player artifact at ${root}: ${error.message}`); }); const artifact = JSON.parse(artifactText); if (artifact.schemaVersion !== 1 || artifact.entrypoint !== 'index.html' || typeof artifact.htmlSha256 !== 'string' || !/^[0-9a-f]{64}$/.test(artifact.htmlSha256)) { throw new Error(`Invalid learning player artifact manifest at ${root}`); } const htmlSha256 = createHash('sha256').update(html).digest('hex'); if (htmlSha256 !== artifact.htmlSha256) { throw new Error(`Learning player index hash mismatch at ${root}`); } if (!(await stat(resolve(root, '_next', 'static')).then((entry) => entry.isDirectory()).catch(() => false)) || !await hasFiles(resolve(root, '_next', 'static'))) { throw new Error(`Learning player static assets are missing at ${root}`); } if (!(await stat(resolve(root, 'avatars')).then((entry) => entry.isDirectory()).catch(() => false)) || !await hasFiles(resolve(root, 'avatars'))) { throw new Error(`Learning player avatars are missing at ${root}`); } if (!(await stat(resolve(root, 'public')).then((entry) => entry.isDirectory()).catch(() => false)) || !await hasFiles(resolve(root, 'public'))) { throw new Error(`Learning player public assets are missing at ${root}`); } return { root, artifact, htmlSha256 }; }