/** Private employee-portal mapping and production-readiness helpers. * These helpers deliberately never decrypt portal streams or read access codes. */ import { createHash } from 'node:crypto'; import fs from 'node:fs/promises'; import path from 'node:path'; import { validatePortalPayload } from '../src/utils/portal-crypto.mjs'; const internal = new Set(['STAFF-INTERNAL', 'MANAGEMENT-INTERNAL']); const safeName = (value) => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,240}$/.test(value) && value !== '.' && value !== '..'; const hash = (value) => createHash('sha256').update(value).digest('hex'); export const payloadDigest = (payload) => hash(Buffer.from(JSON.stringify(validatePortalPayload(payload)))); export const manifestDigest = (manifest) => hash(Buffer.from(JSON.stringify(manifest))); function roleForClassification(classification) { if (classification === 'STAFF-INTERNAL') return 'staff'; if (classification === 'MANAGEMENT-INTERNAL') return 'management'; return null; } function safeDeliveryPath(role, value) { const prefix = `/${role}-resources/`; if (typeof value !== 'string' || !value.startsWith(prefix)) return false; const name = value.slice(prefix.length); return safeName(name) && value === `${prefix}${name}`; } export function mappingsFor(record) { if (Array.isArray(record.installMappings)) return record.installMappings; const role = roleForClassification(record.classification); return role ? [{ role, classification: record.classification, deliveryPath: record.deliveryPath, mappingApproved: record.mappingApproved, accessPolicyApproved: record.accessPolicyApproved, rewriteApproved: record.rewriteApproved, destinationInstalled: record.destinationInstalled, destinationHashVerified: record.destinationHashVerified, destinationPermissionsVerified: record.destinationPermissionsVerified, destinationAccessVerified: record.destinationAccessVerified, }] : []; } export function approvedMapping(record, role) { const mapping = mappingsFor(record).find((candidate) => candidate.role === role); if (!mapping || !internal.has(mapping.classification) || !safeDeliveryPath(role, mapping.deliveryPath)) return null; if (!mapping.mappingApproved || !mapping.accessPolicyApproved || !mapping.rewriteApproved) return null; return mapping; } export function deriveDestinationName(record) { let originalPath = record.originalPath; try { originalPath ||= new URL(record.sourceUrl).pathname; } catch { return null; } const extension = path.extname(originalPath).toLowerCase(); if (!/^[.][a-z0-9]{1,10}$/.test(extension) || !safeName(record.localFile)) return null; return `${record.localFile}${extension}`; } export function summarizeMappings(manifest, streamRoles) { if (!streamRoles || !streamRoles.staff || !streamRoles.management || streamRoles.staff === streamRoles.management) throw new Error('A distinct staff/management stream-role mapping is required'); const summary = { staffOnly: 0, managementOnly: 0, shared: 0, ambiguous: 0, proposed: [] }; for (const record of manifest.resources || []) { const views = new Set((record.references || []).map((reference) => reference.view)); const roles = new Set([...views].map((view) => view === streamRoles.staff ? 'staff' : view === streamRoles.management ? 'management' : null)); if (roles.has(null) || !roles.size || !deriveDestinationName(record)) { summary.ambiguous++; continue; } if (roles.size === 2) summary.shared++; else if (roles.has('staff')) summary.staffOnly++; else summary.managementOnly++; summary.proposed.push({ sourceUrl: record.sourceUrl, localFile: record.localFile, roles: [...roles].sort(), filename: deriveDestinationName(record) }); } return summary; } export function applyRoleMappings(manifest, streamRoles, { approveShared = false } = {}) { const summary = summarizeMappings(manifest, streamRoles); const resources = (manifest.resources || []).map((record) => { const proposal = summary.proposed.find((entry) => entry.sourceUrl === record.sourceUrl); if (!proposal || (proposal.roles.length > 1 && !approveShared)) return { ...record }; const installMappings = proposal.roles.map((role) => ({ role, classification: role === 'staff' ? 'STAFF-INTERNAL' : 'MANAGEMENT-INTERNAL', deliveryPath: `/${role}-resources/${proposal.filename}`, mappingApproved: true, accessPolicyApproved: true, rewriteApproved: true, // Server-installation evidence is intentionally separate and remains false. destinationInstalled: false, destinationHashVerified: false, destinationPermissionsVerified: false, destinationAccessVerified: false, })); return { ...record, references: (record.references || []).map((reference) => ({ ...reference, role: reference.view === streamRoles.staff ? 'staff' : reference.view === streamRoles.management ? 'management' : undefined, })), classification: installMappings.length === 1 ? installMappings[0].classification : 'UNRESOLVED', installMappings, mappingStatus: installMappings.length === 1 ? 'approved-for-installation' : 'approved-shared-role-installation', }; }); return { ...manifest, resources, mappingSchema: 2, mappingUpdatedAt: new Date().toISOString() }; } export function assertProductionMappings(manifest) { let count = 0; for (const record of manifest.resources || []) { const references = record.references || []; if (!references.length) continue; const roles = new Set(references.map((ref) => ref.role).filter(Boolean)); // Role labels are only required after the private stream-role step. if (!roles.size) throw new Error('Private manifest lacks verified stream-to-role references'); for (const role of roles) { if (!approvedMapping(record, role)) throw new Error('Private manifest has an unresolved or unapproved protected resource mapping'); count++; } } if (!count) throw new Error('Private manifest has no approved protected resource mappings'); return count; } export async function readPrivateJson(file, label) { const stat = await fs.lstat(file).catch(() => null); if (!stat?.isFile() || stat.isSymbolicLink() || stat.size > 12_000_000) throw new Error(`${label} is unavailable or unsafe`); return JSON.parse(await fs.readFile(file, 'utf8')); } export async function assertProductionReadiness(privateRoot, payloadFile) { const payload = validatePortalPayload(await readPrivateJson(payloadFile, 'Portal ciphertext')); const manifest = await readPrivateJson(path.join(privateRoot, 'manifest.json'), 'Private portal manifest'); const readiness = await readPrivateJson(path.join(privateRoot, 'production-readiness.json'), 'Private production readiness record'); if (readiness.schema !== 1 || readiness.payloadSha256 !== payloadDigest(payload) || readiness.manifestSha256 !== manifestDigest(manifest)) throw new Error('Production portal readiness record does not match the current private ciphertext and manifest'); const mappings = assertProductionMappings(manifest); if (readiness.protectedMappings !== mappings || readiness.legacyHubfsReferences !== 0) throw new Error('Production portal readiness record does not prove complete protected-link rewriting'); return { payload, manifest, readiness }; }