import { readFile, readdir } from 'node:fs/promises'; import { join } from 'node:path'; import yaml from 'js-yaml'; const fail = (message) => { throw new Error(message); }; const assert = (condition, message) => { if (!condition) fail(message); }; const same = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected); function expectSubset(actual, expected, path) { assert(actual && typeof actual === 'object', `${path} must be an object.`); for (const [key, value] of Object.entries(expected)) { const currentPath = `${path}.${key}`; if (value && typeof value === 'object' && !Array.isArray(value)) expectSubset(actual[key], value, currentPath); else assert(same(actual[key], value), `${currentPath} must be ${JSON.stringify(value)}.`); } } const configPath = 'public/admin/config.yml'; const config = yaml.load(await readFile(configPath, 'utf8')); const expectedOrigin = 'https://cms-pilot-3031023-xyz-www.ksloan90.workers.dev'; const expectedBackend = { name: 'gitea', repo: 'SloanOps/3031023-xyz', branch: 'cms-pilot', api_root: 'https://git.sloanops.com/api/v1', base_url: 'https://git.sloanops.com', auth_endpoint: 'https://git.sloanops.com/login/oauth/authorize', }; expectSubset(config.backend, expectedBackend, 'backend'); assert(same(Object.keys(config.backend).sort(), [...Object.keys(expectedBackend), 'app_id'].sort()), 'The backend must not contain secret-bearing or unreviewed options.'); assert(typeof config.backend.app_id === 'string' && /^[A-Za-z0-9._-]+$/.test(config.backend.app_id), 'backend.app_id must be a non-confidential public client ID or the review placeholder.'); assert(config.publish_mode === 'simple', 'The Gitea pilot must use simple mode on its fixed review branch.'); assert(config.site_url === expectedOrigin && config.display_url === expectedOrigin, 'The CMS must use the stable primary preview origin.'); assert(config.local_backend === undefined, 'The pilot configuration must not enable an alternate local backend.'); expectSubset(config, { media_folder: 'public/images/uploads', public_folder: '/images/uploads', }, 'config'); assert(config.media_library === undefined, 'Top-level media_library is reserved for registered external libraries; bound the built-in uploader on the image field.'); const markdownField = { widget: 'markdown', buttons: ['bold', 'italic', 'link', 'heading-two', 'heading-three', 'quote', 'bulleted-list', 'numbered-list'], editor_components: [], }; const expectedCollections = { primary_faq: { folder: 'sites/www/content/faq', identifier_field: 'question', fields: { question: { widget: 'string', pattern: ['^.{12,180}$', 'Use 12–180 characters.'] }, category: { widget: 'string', default: 'General', pattern: ['^.{2,48}$', 'Use 2–48 characters.'] }, order: { widget: 'number', value_type: 'int', min: 0, max: 99, step: 1, default: 0 }, draft: { widget: 'boolean', default: false }, body: markdownField, }, }, primary_services: { folder: 'sites/www/content/services', identifier_field: 'title', fields: { title: { widget: 'string', pattern: ['^.{3,80}$', 'Use 3–80 characters.'] }, price: { widget: 'string', pattern: ['^.{2,48}$', 'Use 2–48 characters.'] }, items: { widget: 'list', min: 1, max: 8, field: { widget: 'string', pattern: ['^.{3,80}$', 'Use 3–80 characters.'] } }, order: { widget: 'number', value_type: 'int', min: 0, max: 99, step: 1, default: 0 }, featured: { widget: 'boolean', default: false }, draft: { widget: 'boolean', default: false }, body: markdownField, }, }, primary_team: { folder: 'sites/www/content/team', identifier_field: 'name', fields: { name: { widget: 'string', pattern: ['^.{3,80}$', 'Use 3–80 characters.'] }, role: { widget: 'string', pattern: ['^.{3,80}$', 'Use 3–80 characters.'] }, image: { widget: 'image', choose_url: false, allow_multiple: false, pattern: [ '^/images/(?:uploads/)?[A-Za-z0-9][A-Za-z0-9._-]*\\.(?:jpe?g|png|webp|avif)$', 'Choose an existing approved image or a JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.', ], media_library: { config: { max_file_size: 512000, multiple: false } }, }, order: { widget: 'number', value_type: 'int', min: 0, max: 99, step: 1, default: 0 }, draft: { widget: 'boolean', default: false }, body: markdownField, }, }, journal_posts: { folder: 'sites/blog/content/posts', identifier_field: 'title', fields: { title: { widget: 'string', pattern: ['^.{12,100}$', 'Use 12–100 characters.'] }, description: { widget: 'text', pattern: ['^[\\s\\S]{30,180}$', 'Use 30–180 characters.'] }, pubDate: { widget: 'date', format: 'YYYY-MM-DD' }, updatedDate: { widget: 'date', format: 'YYYY-MM-DD', required: false }, tags: { widget: 'list', min: 1, max: 4, field: { widget: 'string', pattern: ['^.{2,32}$', 'Use 2–32 characters.'] } }, draft: { widget: 'boolean', default: false }, body: markdownField, }, }, }; assert(Array.isArray(config.collections), 'collections must be an array.'); assert(same(config.collections.map(({ name }) => name).sort(), Object.keys(expectedCollections).sort()), 'Decap must expose exactly the four approved collections.'); for (const collection of config.collections) { const expected = expectedCollections[collection.name]; expectSubset(collection, { folder: expected.folder, identifier_field: expected.identifier_field, create: true, delete: true, extension: 'md', format: 'frontmatter', slug: '{{slug}}', }, `collections.${collection.name}`); const fields = Object.fromEntries(collection.fields.map((field) => [field.name, field])); assert(same(Object.keys(fields).sort(), Object.keys(expected.fields).sort()), `${collection.name} must preserve every modeled frontmatter field and body.`); for (const [fieldName, fieldContract] of Object.entries(expected.fields)) expectSubset(fields[fieldName], fieldContract, `${collection.name}.${fieldName}`); } function visitField(field) { assert(field.widget !== 'file', `General file widget is forbidden: ${field.name}.`); if (field.fields) field.fields.forEach(visitField); if (field.field) visitField(field.field); } config.collections.flatMap(({ fields }) => fields).forEach(visitField); const imageFields = config.collections.flatMap(({ fields }) => fields).filter(({ widget }) => widget === 'image'); assert(imageFields.length === 1 && imageFields[0].name === 'image', 'Only the bounded team raster-image field may expose media selection.'); const schema = await readFile('src/content.config.ts', 'utf8'); for (const fragment of [ 'title: z.string().min(12).max(100)', 'description: z.string().min(30).max(180)', 'updatedDate: z.coerce.date().optional()', 'tags: z.array(z.string().min(2).max(32)).min(1).max(4)', "question: z.string().min(12).max(180)", "category: z.string().min(2).max(48).default('General')", 'title: z.string().min(3).max(80)', 'price: z.string().min(2).max(48)', 'items: z.array(z.string().min(3).max(80)).min(1).max(8)', 'name: z.string().min(3).max(80)', 'role: z.string().min(3).max(80)', "image: z.string().startsWith('/images/')", 'order: z.number().int().min(0).max(99).default(0)', 'draft: z.boolean().default(false)', 'featured: z.boolean().default(false)', ]) assert(schema.includes(fragment), `Astro schema changed; review the Decap contract for: ${fragment}`); async function verifyContentPaths(directory) { const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { const path = join(directory, entry.name); if (entry.isDirectory()) { assert(/^[a-z0-9][a-z0-9-]*$/.test(entry.name), `Content directory is outside the Markdown slug contract: ${path}`); await verifyContentPaths(path); } else { assert(entry.isFile(), `Special content paths are outside the pilot contract: ${path}`); assert(/^[a-z0-9][a-z0-9-]*\.md$/.test(entry.name), `Content filename is outside the Markdown slug contract: ${path}`); } } } for (const directory of Object.values(expectedCollections).map(({ folder }) => folder)) await verifyContentPaths(directory); const admin = await readFile('public/admin/index.html', 'utf8'); assert(admin.includes('content="noindex,nofollow,noarchive"'), 'The admin shell must always carry no-index metadata.'); assert(admin.includes('href="/admin/config.yml"') && admin.includes('rel="cms-config-url"'), 'The admin shell must load the reviewed configuration.'); assert(admin.includes('https://unpkg.com/decap-cms@3.15.1/dist/decap-cms.js'), 'The admin shell must pin Decap CMS 3.15.1 exactly.'); assert(admin.includes('sha384-in6eHztHveqQ7uMZ1fDaKlDmacQLFuLH2wWrFTiymyuS8zQ5bixwL8U3AeRi8h/L'), 'The Decap bundle must retain its verified SHA-384 integrity value.'); assert(!/decap-cms@(latest|[~^*])/.test(admin), 'Floating Decap CMS versions are forbidden.'); const headers = await readFile('public/_headers', 'utf8'); assert(/\/admin\/\*[\s\S]*X-Robots-Tag: noindex, nofollow, noarchive/.test(headers), 'Admin responses must include an X-Robots-Tag no-index rule.'); const workflow = yaml.load(await readFile('.gitea/workflows/cms-content-validation.yml', 'utf8')); assert(workflow.name === 'CMS content validation', 'The validation workflow display name is part of the required status context.'); assert(workflow.on?.pull_request && same(workflow.on.pull_request.branches, ['main']), 'The validation workflow must run for every pull request into main.'); assert(workflow.on.pull_request.paths === undefined, 'The required validation workflow must not use path filters.'); assert(workflow.jobs?.validate?.name === 'cms-content-validation', 'The validation job name is part of the required status context.'); const workflowText = await readFile('.gitea/workflows/cms-content-validation.yml', 'utf8'); assert(!workflowText.includes('secrets.'), 'CMS pull-request validation must remain secret-free.'); for (const command of ['npm run verify:cms', 'SITE_KEY=www npm run build:site', 'SITE_KEY=www npm run verify:build', 'SITE_KEY=blog npm run build:site', 'SITE_KEY=blog npm run verify:build']) { assert(workflowText.includes(command), `Validation workflow is missing: ${command}`); } console.log('Verified Decap backend, four content contracts, text-first media boundary, admin pin, and stable PR status context.');