fix: harden prerender publication boundary
This commit is contained in:
@@ -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',
|
||||
|
||||
19
src/content/production-input-snapshot.ts
Normal file
19
src/content/production-input-snapshot.ts
Normal 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));
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user