63 lines
2.9 KiB
JavaScript
63 lines
2.9 KiB
JavaScript
import { createHash } from 'node:crypto';
|
|
import { appendFile, cp, mkdir, readdir, rm } from 'node:fs/promises';
|
|
import { resolve } from 'node:path';
|
|
import AdmZip from 'adm-zip';
|
|
import { validateLearningPlayerArtifact } from './learning-player-artifact.mjs';
|
|
|
|
const artifactUrl = process.env.LEARNING_PLAYER_ARTIFACT_URL?.trim();
|
|
const expectedSha256 = process.env.LEARNING_PLAYER_ARTIFACT_SHA256?.trim().toLowerCase();
|
|
if (!artifactUrl || !/^https:\/\//i.test(artifactUrl)) {
|
|
throw new Error('LEARNING_PLAYER_ARTIFACT_URL must be a versioned HTTPS artifact URL');
|
|
}
|
|
if (!expectedSha256 || !/^[0-9a-f]{64}$/.test(expectedSha256)) {
|
|
throw new Error('LEARNING_PLAYER_ARTIFACT_SHA256 must be the pinned artifact digest');
|
|
}
|
|
|
|
const response = await fetch(artifactUrl, { redirect: 'follow' });
|
|
if (!response.ok) throw new Error(`Learning player download failed (HTTP ${response.status})`);
|
|
const archive = Buffer.from(await response.arrayBuffer());
|
|
const actualSha256 = createHash('sha256').update(archive).digest('hex');
|
|
if (actualSha256 !== expectedSha256) {
|
|
throw new Error(`Learning player archive digest mismatch: expected ${expectedSha256}, got ${actualSha256}`);
|
|
}
|
|
|
|
const root = resolve(import.meta.dirname, '..');
|
|
const extractionRoot = resolve(root, 'build', '.learning-player-download');
|
|
const outputRoot = resolve(root, 'build', 'learning-player-source');
|
|
await Promise.all([
|
|
rm(extractionRoot, { recursive: true, force: true }),
|
|
rm(outputRoot, { recursive: true, force: true }),
|
|
]);
|
|
await mkdir(extractionRoot, { recursive: true });
|
|
const zip = new AdmZip(archive);
|
|
for (const entry of zip.getEntries()) {
|
|
const normalized = entry.entryName.replace(/\\/g, '/');
|
|
const parts = normalized.split('/');
|
|
if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized) || parts.includes('..')) {
|
|
throw new Error('Learning player archive contains an unsafe path');
|
|
}
|
|
}
|
|
zip.extractAllTo(extractionRoot, true);
|
|
|
|
async function findArtifactRoots(directory, depth = 0) {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
if (entries.some((entry) => entry.isFile() && entry.name === 'artifact.json')) return [directory];
|
|
if (depth >= 2) return [];
|
|
const nested = await Promise.all(entries
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => findArtifactRoots(resolve(directory, entry.name), depth + 1)));
|
|
return nested.flat();
|
|
}
|
|
|
|
const candidates = await findArtifactRoots(extractionRoot);
|
|
if (candidates.length !== 1) throw new Error('Learning player archive must contain exactly one artifact.json root');
|
|
await validateLearningPlayerArtifact(candidates[0]);
|
|
await cp(candidates[0], outputRoot, { recursive: true });
|
|
await validateLearningPlayerArtifact(outputRoot);
|
|
await rm(extractionRoot, { recursive: true, force: true });
|
|
|
|
if (process.env.GITHUB_ENV) {
|
|
await appendFile(process.env.GITHUB_ENV, `MAKELORE_LEARNING_PLAYER_ARTIFACT=${outputRoot}\n`);
|
|
}
|
|
process.stdout.write(`Downloaded verified learning player ${actualSha256} to ${outputRoot}\n`);
|