130 lines
4.0 KiB
JavaScript
130 lines
4.0 KiB
JavaScript
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const repoRoot = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'..'
|
|
);
|
|
const pageFile = path.join(repoRoot, 'activity/room-reward/fami.html');
|
|
const cssFile = path.join(repoRoot, 'activity/room-reward/fami/fami.css');
|
|
const scriptFile = path.join(repoRoot, 'activity/room-reward/fami/fami.js');
|
|
const assetRoot = path.join(
|
|
repoRoot,
|
|
'activity/room-reward/fami/assets/layers'
|
|
);
|
|
const manifestFile = path.join(
|
|
repoRoot,
|
|
'activity/room-reward/fami/assets/manifest.json'
|
|
);
|
|
|
|
function fail(message) {
|
|
throw new Error(`[fami-room-reward-layer-audit] ${message}`);
|
|
}
|
|
|
|
function sha256(file) {
|
|
return crypto
|
|
.createHash('sha256')
|
|
.update(fs.readFileSync(file))
|
|
.digest('hex');
|
|
}
|
|
|
|
for (const file of [pageFile, cssFile, scriptFile, manifestFile]) {
|
|
if (!fs.existsSync(file))
|
|
fail(`missing required file: ${path.relative(repoRoot, file)}`);
|
|
}
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
const assets = Array.isArray(manifest.assets) ? manifest.assets : [];
|
|
if (!assets.length) fail('manifest has no independently traced assets');
|
|
|
|
const forbiddenNodes = new Set(manifest.policy?.forbiddenNodes || []);
|
|
const manifestFiles = new Set();
|
|
for (const asset of assets) {
|
|
if (
|
|
!asset.file ||
|
|
!asset.sha256 ||
|
|
!Array.isArray(asset.nodes) ||
|
|
!asset.nodes.length
|
|
) {
|
|
fail(`incomplete manifest entry: ${JSON.stringify(asset)}`);
|
|
}
|
|
if (asset.nodes.some((node) => forbiddenNodes.has(node))) {
|
|
fail(
|
|
`aggregate/root Figma node exported as runtime asset: ${asset.file}`
|
|
);
|
|
}
|
|
if (!String(asset.kind || '').startsWith('single-')) {
|
|
fail(`asset is not classified as a single visual layer: ${asset.file}`);
|
|
}
|
|
const file = path.join(assetRoot, asset.file);
|
|
if (!fs.existsSync(file)) fail(`manifest asset missing: ${asset.file}`);
|
|
if (sha256(file) !== asset.sha256) fail(`hash mismatch: ${asset.file}`);
|
|
if (asset.pixelSize?.[0] === 1080 && asset.pixelSize?.[1] === 1985) {
|
|
fail(`whole-page sized asset is forbidden: ${asset.file}`);
|
|
}
|
|
manifestFiles.add(asset.file);
|
|
}
|
|
|
|
for (const file of fs.readdirSync(assetRoot)) {
|
|
if (!manifestFiles.has(file))
|
|
fail(`untracked runtime layer asset: ${file}`);
|
|
}
|
|
|
|
const html = fs.readFileSync(pageFile, 'utf8');
|
|
const css = fs.readFileSync(cssFile, 'utf8');
|
|
const js = fs.readFileSync(scriptFile, 'utf8');
|
|
const runtimeSource = `${html}\n${css}\n${js}`;
|
|
|
|
for (const forbidden of [
|
|
'figma-default.png',
|
|
'/.audit/',
|
|
'822:2775',
|
|
'822:2783',
|
|
]) {
|
|
if (runtimeSource.includes(forbidden))
|
|
fail(`forbidden aggregate reference: ${forbidden}`);
|
|
}
|
|
|
|
const referencedAssets = new Set();
|
|
for (const match of runtimeSource.matchAll(
|
|
/(?:fami\/)?assets\/layers\/([\w.-]+)/g
|
|
)) {
|
|
referencedAssets.add(match[1]);
|
|
}
|
|
for (const file of referencedAssets) {
|
|
if (!manifestFiles.has(file))
|
|
fail(`runtime asset is not in manifest: ${file}`);
|
|
}
|
|
|
|
for (const requiredDOM of [
|
|
'id="rrTargetValue"',
|
|
'id="rrTierTable"',
|
|
'id="rrUserName"',
|
|
'id="rrContributionValue"',
|
|
'id="rrRewardValue"',
|
|
]) {
|
|
if (!html.includes(requiredDOM))
|
|
fail(`dynamic DOM layer missing: ${requiredDOM}`);
|
|
}
|
|
|
|
if (html.includes('<button')) {
|
|
fail(
|
|
'automatic-settlement values must not be implemented as claim buttons'
|
|
);
|
|
}
|
|
if (!js.includes("'/api/v1/activities/room-turnover-reward'")) {
|
|
fail('real room reward API is not connected');
|
|
}
|
|
if (!js.includes("params.get('mock') === '1'")) {
|
|
fail('Figma fixture must be gated behind explicit ?mock=1');
|
|
}
|
|
if (/catch[\s\S]{0,500}mockData/.test(js)) {
|
|
fail('API errors must not silently fall back to Figma business data');
|
|
}
|
|
|
|
console.log(
|
|
`[fami-room-reward-layer-audit] passed: ${assets.length} traced single-layer assets, DOM/API business content verified`
|
|
);
|