Files
makelore/scripts/download-learning-player-artifact.mjs
inman 01bee3188b
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
feat: integrate learning module
2026-08-16 20:52:16 +08:00

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`);