fix: harden prerender publication boundary

This commit is contained in:
2026-09-02 02:44:23 +03:00
parent 3dd4a818d0
commit c16c4c57da
22 changed files with 1275 additions and 231 deletions

View File

@@ -160,6 +160,9 @@ export const createStaticHostArtifacts = (
'',
].join('\n'),
headers: [
'/assets/*',
' Cache-Control: public, max-age=31536000, immutable',
'',
'/*',
...commonHeaders(metadata),
' Cache-Control: public, max-age=0, must-revalidate',

View File

@@ -0,0 +1,19 @@
import { parseProductionInputSnapshot } from './schemas';
import type { ProductionInputs } from './types';
const deepFreeze = <T>(value: T, seen = new WeakSet<object>()): Readonly<T> => {
if (value !== null && typeof value === 'object') {
const object = value as object;
if (!seen.has(object)) {
seen.add(object);
for (const key of Reflect.ownKeys(object)) {
deepFreeze(Reflect.get(object, key), seen);
}
Object.freeze(object);
}
}
return value;
};
export const validateAndFreezeProductionInputSnapshot = (input: unknown): Readonly<ProductionInputs> =>
deepFreeze(parseProductionInputSnapshot(input));

View File

@@ -225,15 +225,14 @@ export const parsePortfolioContent = (input: unknown): PortfolioContent => portf
export const parsePublicAssetManifest = (input: unknown): PublicAssetManifest => publicAssetManifestSchema.parse(input) as PublicAssetManifest;
export const parseSiteConfig = (input: unknown): SiteConfig => siteConfigSchema.parse(input) as SiteConfig;
export const validateProductionInputs = (
contentInput: unknown,
manifestInput: unknown,
siteInput: unknown,
_preparedAssetRoot: URL,
): ProductionInputs => {
const content = parsePortfolioContent(contentInput);
const manifest = parsePublicAssetManifest(manifestInput);
const site = parseSiteConfig(siteInput);
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),
@@ -247,5 +246,16 @@ export const validateProductionInputs = (
throw new Error(`Unmanifested asset reference: ${assetId}`);
}
}
return { content, manifest, site };
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,
});

View File

@@ -1,4 +1,5 @@
import { createApp } from './app';
import { validateAndFreezeProductionInputSnapshot } from './content/production-input-snapshot';
import type { ProductionInputs } from './content/types';
export const readProductionInputsSnapshot = (source: Document): Readonly<ProductionInputs> => {
@@ -16,7 +17,7 @@ export const readProductionInputsSnapshot = (source: Document): Readonly<Product
throw new Error('The inert portfolio input snapshot is empty.');
}
return JSON.parse(serializedInputs) as Readonly<ProductionInputs>;
return validateAndFreezeProductionInputSnapshot(JSON.parse(serializedInputs) as unknown);
};
const appRoot = document.getElementById('app');