Add preview-only Decap CMS pilot

This commit is contained in:
Kevin
2026-09-04 00:33:03 -04:00
parent ee1010d751
commit 7eeb9e1e01
12 changed files with 759 additions and 10 deletions
+44 -5
View File
@@ -1,7 +1,7 @@
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
import { readdir, readFile, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { extname, join, relative, sep } from 'node:path';
const readJson = async (path) => JSON.parse(await readFile(path, 'utf8'));
const ajv = new Ajv2020({ allErrors: true, strict: true });
@@ -23,8 +23,47 @@ for (const key of Object.keys(sites.sites)) {
if (!siteCheck(data)) throw new Error(`${key} site data rejected: ${ajv.errorsText(siteCheck.errors)}`);
}
for (const file of await readdir('public/images')) {
const info = await stat(join('public/images', file));
if (info.size > 500 * 1024) throw new Error(`Image exceeds the 500 KiB pilot limit: ${file}`);
async function filesUnder(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const path = join(directory, entry.name);
if (entry.isDirectory()) files.push(...await filesUnder(path));
else if (entry.isFile()) files.push(path);
else throw new Error(`Unsupported filesystem entry in public images: ${path}`);
}
return files;
}
console.log('Intake fixtures, deployment manifest, site data, and image limits are valid.');
const uploadRoot = join('public', 'images', 'uploads');
const allowedUploadExtensions = new Set(['.jpg', '.jpeg', '.png', '.webp', '.avif']);
const ascii = (value) => Buffer.from(value, 'ascii');
const startsWith = (buffer, signature) => buffer.subarray(0, signature.length).equals(signature);
const isRasterForExtension = (buffer, extension) => {
if (extension === '.jpg' || extension === '.jpeg') return startsWith(buffer, Buffer.from([0xff, 0xd8, 0xff]));
if (extension === '.png') return startsWith(buffer, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
if (extension === '.webp') return buffer.length >= 12 && buffer.subarray(0, 4).equals(ascii('RIFF')) && buffer.subarray(8, 12).equals(ascii('WEBP'));
if (extension === '.avif' && buffer.length >= 16 && buffer.subarray(4, 8).equals(ascii('ftyp'))) {
for (let offset = 8; offset + 4 <= Math.min(buffer.length, 64); offset += 4) {
const brand = buffer.subarray(offset, offset + 4).toString('ascii');
if (brand === 'avif' || brand === 'avis') return true;
}
}
return false;
};
for (const path of await filesUnder('public/images')) {
const imagePath = relative('public/images', path).split(sep).join('/');
const info = await stat(path);
if (info.size > 500 * 1024) throw new Error(`Image exceeds the 500 KiB pilot limit: ${imagePath}`);
const uploadPath = relative(uploadRoot, path);
const isUpload = uploadPath !== '..' && !uploadPath.startsWith(`..${sep}`);
if (!isUpload) continue;
if (uploadPath.includes(sep)) throw new Error(`Nested CMS media paths are not allowed: ${imagePath}`);
const extension = extname(uploadPath).toLowerCase();
if (!allowedUploadExtensions.has(extension)) throw new Error(`CMS media is not an allowed raster extension: ${imagePath}`);
const buffer = await readFile(path);
if (!isRasterForExtension(buffer, extension)) throw new Error(`CMS media does not match its raster extension: ${imagePath}`);
}
console.log('Intake fixtures, deployment manifest, site data, and bounded raster images are valid.');