360 lines
15 KiB
TypeScript
360 lines
15 KiB
TypeScript
import { z } from 'zod';
|
|
import type { PortfolioContent, PublicAssetManifest, ProductionInputs, SiteConfig } from './types.ts';
|
|
|
|
const canonicalNavigationIds = [
|
|
'start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact',
|
|
] as const;
|
|
const socialHosts = {
|
|
GitHub: 'github.com',
|
|
GitLab: 'gitlab.com',
|
|
LinkedIn: 'linkedin.com',
|
|
} as const;
|
|
const approvedPrivateProjectTitles = [
|
|
'Private self-hosted medication tracker',
|
|
'Private healthcare platform',
|
|
'Private game prototype',
|
|
] as const;
|
|
|
|
const hasUnpairedSurrogate = (value: string): boolean => {
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
const codeUnit = value.charCodeAt(index);
|
|
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
const next = value.charCodeAt(index + 1);
|
|
if (next < 0xdc00 || next > 0xdfff) return true;
|
|
index += 1;
|
|
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
const hasVisibleCopy = (value: string): boolean =>
|
|
value.trim().length > 0 &&
|
|
value === value.normalize('NFC') &&
|
|
!hasUnpairedSurrogate(value) &&
|
|
!/[\u0000-\u001f\u007f]/u.test(value);
|
|
const visibleString = z.string().refine(hasVisibleCopy, 'Visible copy must be non-empty, well-formed Unicode, NFC-normalized, and control-character free.');
|
|
const opaqueId = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/, 'IDs must be generated safe identifiers.');
|
|
const httpUrl = z.string().refine((value) => {
|
|
if (value !== value.normalize('NFC') || hasUnpairedSurrogate(value) || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
return false;
|
|
}
|
|
try {
|
|
const url = new URL(value);
|
|
const authority = /^https:\/\/([^/?#]+)/u.exec(value)?.[1];
|
|
return url.protocol === 'https:' &&
|
|
!url.username &&
|
|
!url.password &&
|
|
!url.search &&
|
|
!url.hash &&
|
|
authority !== undefined &&
|
|
!authority.includes(':');
|
|
} catch {
|
|
return false;
|
|
}
|
|
}, 'URL must be an absolute HTTPS URL without credentials, query, or fragment.');
|
|
|
|
const navigationItemSchema = z.object({
|
|
id: z.enum(canonicalNavigationIds),
|
|
label: visibleString,
|
|
}).strict();
|
|
|
|
const soundtrackAssetSourcesSchema = z.object({
|
|
opusAssetId: opaqueId,
|
|
mp3AssetId: opaqueId,
|
|
}).strict();
|
|
|
|
const mediaPresentationBase = {
|
|
assetId: opaqueId,
|
|
alt: visibleString,
|
|
caption: visibleString.optional(),
|
|
longDescription: visibleString.optional(),
|
|
};
|
|
const mediaTableSchema = z.object({
|
|
caption: visibleString,
|
|
columns: z.array(visibleString).min(2),
|
|
rows: z.array(z.array(visibleString).min(2)).min(1),
|
|
}).strict().superRefine((table, context) => {
|
|
for (const [index, row] of table.rows.entries()) {
|
|
if (row.length !== table.columns.length) {
|
|
context.addIssue({ code: 'custom', message: 'Every chart table row must match the column count.', path: ['rows', index] });
|
|
}
|
|
}
|
|
});
|
|
const mediaPresentationSchema = z.discriminatedUnion('kind', [
|
|
z.object({ kind: z.literal('image'), ...mediaPresentationBase }).strict(),
|
|
z.object({
|
|
kind: z.literal('video'),
|
|
...mediaPresentationBase,
|
|
fallbackAssetId: opaqueId.optional(),
|
|
posterAssetId: opaqueId,
|
|
caption: visibleString,
|
|
longDescription: visibleString,
|
|
}).strict(),
|
|
z.object({
|
|
kind: z.literal('chart'),
|
|
...mediaPresentationBase,
|
|
caption: visibleString,
|
|
chart: z.object({
|
|
variables: visibleString,
|
|
trend: visibleString,
|
|
conclusion: visibleString,
|
|
table: mediaTableSchema.optional(),
|
|
}).strict(),
|
|
}).strict(),
|
|
]);
|
|
const projectSchema = z.object({
|
|
id: opaqueId,
|
|
title: visibleString,
|
|
visibility: z.enum(['public', 'private']),
|
|
maturity: z.enum(['learning', 'experimental', 'prototype', 'work-in-progress', 'active']),
|
|
summary: visibleString,
|
|
problem: visibleString,
|
|
contribution: visibleString,
|
|
technicalDecisions: z.array(visibleString).min(1),
|
|
outcome: visibleString,
|
|
url: httpUrl.optional(),
|
|
assetIds: z.array(opaqueId),
|
|
media: z.array(mediaPresentationSchema).optional(),
|
|
}).strict().superRefine((project, context) => {
|
|
if (project.visibility === 'private' && project.url !== undefined) {
|
|
context.addIssue({ code: 'custom', message: 'A private project may not include a url.', path: ['url'] });
|
|
}
|
|
if (project.visibility === 'private' && !approvedPrivateProjectTitles.includes(project.title as (typeof approvedPrivateProjectTitles)[number])) {
|
|
context.addIssue({ code: 'custom', message: 'Private project title must use an approved generic title.', path: ['title'] });
|
|
}
|
|
if (project.media !== undefined) {
|
|
const mediaAssetIds = project.media.flatMap((media) => media.kind === 'video'
|
|
? [media.assetId, ...(media.fallbackAssetId === undefined ? [] : [media.fallbackAssetId]), media.posterAssetId]
|
|
: [media.assetId]);
|
|
const declared = [...project.assetIds].sort();
|
|
const presented = [...mediaAssetIds].sort();
|
|
if (new Set(declared).size !== declared.length || new Set(presented).size !== presented.length ||
|
|
JSON.stringify(declared) !== JSON.stringify(presented)) {
|
|
context.addIssue({
|
|
code: 'custom',
|
|
message: 'Project media must have an exact, duplicate-free asset closure.',
|
|
path: ['assetIds'],
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
const portfolioContentSchema = z.object({
|
|
identity: z.object({
|
|
name: z.literal('Luca Pîslaru'),
|
|
role: z.literal('System Administrator & Full-Stack Developer'),
|
|
valueProposition: visibleString,
|
|
}).strict(),
|
|
navigation: z.array(navigationItemSchema).length(8),
|
|
profile: z.object({
|
|
introduction: visibleString,
|
|
screeningFacts: z.array(z.object({ label: visibleString, value: visibleString }).strict()),
|
|
}).strict(),
|
|
experience: z.array(z.object({
|
|
id: opaqueId,
|
|
role: visibleString,
|
|
organization: visibleString,
|
|
dates: visibleString,
|
|
location: visibleString.optional(),
|
|
highlights: z.array(visibleString).min(2).max(4),
|
|
}).strict()).min(1),
|
|
systemsOperations: z.object({
|
|
id: opaqueId,
|
|
title: z.literal('Professional systems operations'),
|
|
summary: visibleString,
|
|
evidence: z.array(visibleString).min(1),
|
|
}).strict(),
|
|
infrastructure: z.object({
|
|
id: opaqueId,
|
|
title: z.literal('Secure self-hosted platform and lifecycle'),
|
|
summary: visibleString,
|
|
bands: z.array(z.object({ id: opaqueId, title: visibleString, summary: visibleString }).strict()).length(4),
|
|
}).strict(),
|
|
projects: z.object({
|
|
selected: z.array(projectSchema).length(3),
|
|
additional: z.array(projectSchema).min(1),
|
|
}).strict(),
|
|
toolkit: z.array(z.object({
|
|
title: visibleString,
|
|
technologies: z.array(z.object({ name: visibleString, assetId: opaqueId.optional() }).strict()).min(1),
|
|
}).strict()).min(1),
|
|
contact: z.object({
|
|
email: z.email(),
|
|
social: z.array(z.object({
|
|
service: z.enum(['GitHub', 'GitLab', 'LinkedIn']),
|
|
url: httpUrl,
|
|
}).strict()).superRefine((links, context) => {
|
|
for (const [index, link] of links.entries()) {
|
|
const host = new URL(link.url).hostname;
|
|
if (host !== socialHosts[link.service]) {
|
|
context.addIssue({ code: 'custom', message: `Social URL must use the approved ${link.service} host.`, path: [index, 'url'] });
|
|
}
|
|
}
|
|
}),
|
|
cvMessage: visibleString,
|
|
}).strict(),
|
|
metadata: z.object({
|
|
title: visibleString,
|
|
description: visibleString,
|
|
socialPreviewAssetId: opaqueId.optional(),
|
|
}).strict(),
|
|
soundtrack: z.object({
|
|
intro: soundtrackAssetSourcesSchema,
|
|
main: soundtrackAssetSourcesSchema,
|
|
afterglow: soundtrackAssetSourcesSchema,
|
|
driveLift: soundtrackAssetSourcesSchema,
|
|
}).strict().optional(),
|
|
sceneDescription: visibleString,
|
|
}).strict().superRefine((content, context) => {
|
|
const navigationIds = content.navigation.map((item) => item.id);
|
|
if (new Set(navigationIds).size !== canonicalNavigationIds.length ||
|
|
canonicalNavigationIds.some((id) => !navigationIds.includes(id))) {
|
|
context.addIssue({ code: 'custom', message: 'Navigation must contain each canonical section ID exactly once.', path: ['navigation'] });
|
|
}
|
|
|
|
const projects = [...content.projects.selected, ...content.projects.additional];
|
|
const projectIds = projects.map((project) => project.id);
|
|
if (new Set(projectIds).size !== projectIds.length) {
|
|
context.addIssue({ code: 'custom', message: 'Project IDs must be unique.', path: ['projects'] });
|
|
}
|
|
if (content.projects.selected.filter((project) => project.maturity === 'experimental').length !== 1) {
|
|
context.addIssue({ code: 'custom', message: 'Selected builds must include exactly one supporting experimental research panel.', path: ['projects', 'selected'] });
|
|
}
|
|
const experienceIds = content.experience.map((record) => record.id);
|
|
if (new Set(experienceIds).size !== experienceIds.length) {
|
|
context.addIssue({ code: 'custom', message: 'Experience IDs must be unique.', path: ['experience'] });
|
|
}
|
|
const bandIds = content.infrastructure.bands.map((band) => band.id);
|
|
if (new Set(bandIds).size !== bandIds.length) {
|
|
context.addIssue({ code: 'custom', message: 'Infrastructure band IDs must be unique.', path: ['infrastructure', 'bands'] });
|
|
}
|
|
});
|
|
|
|
const mediaTypes = [
|
|
'image/avif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/webp',
|
|
'video/mp4', 'video/webm',
|
|
'audio/ogg', 'audio/mpeg',
|
|
] as const;
|
|
const fileExtensionByMediaType: Record<(typeof mediaTypes)[number], string> = {
|
|
'image/avif': 'avif',
|
|
'image/jpeg': 'jpg',
|
|
'image/png': 'png',
|
|
'image/svg+xml': 'svg',
|
|
'image/webp': 'webp',
|
|
'video/mp4': 'mp4',
|
|
'video/webm': 'webm',
|
|
'audio/ogg': 'opus',
|
|
'audio/mpeg': 'mp3',
|
|
};
|
|
const publicAssetSchema = z.object({
|
|
id: opaqueId,
|
|
fileName: z.string().regex(/^[a-z0-9][a-z0-9-]*\.(avif|jpg|png|svg|webp|mp4|webm|opus|mp3)$/, 'Asset filenames must be generated safe filenames.'),
|
|
sha256: z.string().regex(/^[a-f0-9]{64}$/, 'Asset sha256 must be lowercase hexadecimal.'),
|
|
mediaType: z.enum(mediaTypes),
|
|
width: z.number().int().positive().nullable(),
|
|
height: z.number().int().positive().nullable(),
|
|
durationMs: z.number().int().positive().nullable(),
|
|
rightsHolder: visibleString,
|
|
license: visibleString,
|
|
attribution: visibleString,
|
|
}).strict().superRefine((asset, context) => {
|
|
if (!asset.fileName.endsWith(`.${fileExtensionByMediaType[asset.mediaType]}`)) {
|
|
context.addIssue({ code: 'custom', message: 'Asset filename extension must match media type.', path: ['fileName'] });
|
|
}
|
|
const image = asset.mediaType.startsWith('image/');
|
|
const video = asset.mediaType.startsWith('video/');
|
|
const audio = asset.mediaType.startsWith('audio/');
|
|
if (image && (asset.width === null || asset.height === null || asset.durationMs !== null)) {
|
|
context.addIssue({ code: 'custom', message: 'Image assets require dimensions and no duration.', path: ['width'] });
|
|
}
|
|
if (video && (asset.width === null || asset.height === null || asset.durationMs === null)) {
|
|
context.addIssue({ code: 'custom', message: 'Video assets require dimensions and duration.', path: ['durationMs'] });
|
|
}
|
|
if (audio && (asset.width !== null || asset.height !== null || asset.durationMs === null)) {
|
|
context.addIssue({ code: 'custom', message: 'Audio assets require null dimensions and a positive duration.', path: ['durationMs'] });
|
|
}
|
|
});
|
|
const publicAssetManifestSchema = z.object({ assets: z.array(publicAssetSchema) }).strict().superRefine((manifest, context) => {
|
|
const ids = manifest.assets.map((asset) => asset.id);
|
|
if (new Set(ids).size !== ids.length) {
|
|
context.addIssue({ code: 'custom', message: 'Duplicate asset ID in manifest.', path: ['assets'] });
|
|
}
|
|
const filenames = manifest.assets.map((asset) => asset.fileName);
|
|
if (new Set(filenames).size !== filenames.length || new Set(filenames.map((name) => name.toLocaleLowerCase('en-US'))).size !== filenames.length) {
|
|
context.addIssue({ code: 'custom', message: 'Asset filenames must be unique without case-fold collisions.', path: ['assets'] });
|
|
}
|
|
});
|
|
|
|
const siteConfigSchema = z.object({
|
|
environment: z.enum(['preview', 'production']),
|
|
canonicalUrl: httpUrl,
|
|
buildId: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,127}$/, 'Build ID must be a safe immutable identifier.'),
|
|
}).strict().superRefine((site, context) => {
|
|
if (site.environment === 'preview' && site.canonicalUrl !== 'https://portfolio.localhost/') {
|
|
context.addIssue({ code: 'custom', message: 'Preview canonical URL must be exactly https://portfolio.localhost/.', path: ['canonicalUrl'] });
|
|
}
|
|
if (site.environment === 'production') {
|
|
context.addIssue({ code: 'custom', message: 'Production hostname is not approved.', path: ['canonicalUrl'] });
|
|
}
|
|
});
|
|
|
|
export const parsePortfolioContent = (input: unknown): PortfolioContent => portfolioContentSchema.parse(input) as PortfolioContent;
|
|
export const parsePublicAssetManifest = (input: unknown): PublicAssetManifest => publicAssetManifestSchema.parse(input) as PublicAssetManifest;
|
|
export const parseSiteConfig = (input: unknown): SiteConfig => siteConfigSchema.parse(input) as SiteConfig;
|
|
|
|
const productionInputsSchema = z.object({
|
|
content: portfolioContentSchema,
|
|
manifest: publicAssetManifestSchema,
|
|
site: siteConfigSchema,
|
|
}).strict();
|
|
|
|
export const parseProductionInputSnapshot = (input: unknown): ProductionInputs => {
|
|
const { content, manifest, site } = productionInputsSchema.parse(input);
|
|
const manifestIds = new Set(manifest.assets.map((asset) => asset.id));
|
|
const referencedAssetIds = [
|
|
...content.projects.selected.flatMap((project) => project.assetIds),
|
|
...content.projects.additional.flatMap((project) => project.assetIds),
|
|
...content.toolkit.flatMap((group) => group.technologies.flatMap((technology) =>
|
|
technology.assetId === undefined ? [] : [technology.assetId])),
|
|
...(content.metadata.socialPreviewAssetId === undefined ? [] : [content.metadata.socialPreviewAssetId]),
|
|
...(content.soundtrack === undefined ? [] : [
|
|
content.soundtrack.intro.opusAssetId,
|
|
content.soundtrack.intro.mp3AssetId,
|
|
content.soundtrack.main.opusAssetId,
|
|
content.soundtrack.main.mp3AssetId,
|
|
content.soundtrack.afterglow.opusAssetId,
|
|
content.soundtrack.afterglow.mp3AssetId,
|
|
content.soundtrack.driveLift.opusAssetId,
|
|
content.soundtrack.driveLift.mp3AssetId,
|
|
]),
|
|
];
|
|
const seenReferences = new Set<string>();
|
|
for (const assetId of referencedAssetIds) {
|
|
if (seenReferences.has(assetId)) {
|
|
throw new Error(`Duplicate public asset reference: ${assetId}`);
|
|
}
|
|
seenReferences.add(assetId);
|
|
if (!manifestIds.has(assetId)) {
|
|
throw new Error(`Unmanifested asset reference: ${assetId}`);
|
|
}
|
|
}
|
|
for (const assetId of manifestIds) {
|
|
if (!seenReferences.has(assetId)) {
|
|
throw new Error(`Unreferenced manifest asset: ${assetId}`);
|
|
}
|
|
}
|
|
return { content, manifest, site } as ProductionInputs;
|
|
};
|
|
|
|
export const validateProductionInputs = (
|
|
contentInput: unknown,
|
|
manifestInput: unknown,
|
|
siteInput: unknown,
|
|
_preparedAssetRoot: URL,
|
|
): ProductionInputs => parseProductionInputSnapshot({
|
|
content: contentInput,
|
|
manifest: manifestInput,
|
|
site: siteInput,
|
|
});
|