import { createHash } from "node:crypto"; import { posix, win32 } from "node:path"; export const ASSET_ACCESS_CLASSES = [ "public_release_asset", "internal_preview_asset", "private_user_asset", ] as const; export type AssetAccessClass = typeof ASSET_ACCESS_CLASSES[number]; export type PublicAssetCacheKind = "font" | "template_conversion" | "thumbnail"; export interface AssetReleaseItemInput { access_class: AssetAccessClass; cache_kind?: PublicAssetCacheKind; content: Uint8Array; mime_type: string; owner_id?: string; relative_path: string; resource_id: string; root_ref: string; sha256?: string; } export interface AssetReleaseManifestInput { items: readonly AssetReleaseItemInput[]; release_version: string; } export interface AssetReleaseManifestItem { access_class: AssetAccessClass; byte_size: number; cache_kind?: PublicAssetCacheKind; mime_type: string; release_version: string; resource_id: string; sha256: string; url: string; } export interface AssetReleaseManifestProjection { items: readonly AssetReleaseManifestItem[]; manifest_sha256: string; release_version: string; schema_version: "AssetReleaseManifest/v1"; } export interface AssetReleasePayload { accessClass: AssetAccessClass; bytes: Buffer; mimeType: string; ownerId?: string; releaseVersion: string; resourceId: string; sha256: string; } export interface AssetReleaseReader { project( accessClass: AssetAccessClass, releaseVersion: string, options?: { ownerId?: string; resourceIds?: readonly string[] }, ): AssetReleaseManifestProjection | undefined; read(accessClass: AssetAccessClass, releaseVersion: string, resourceId: string): AssetReleasePayload | undefined; } interface StoredItem { accessClass: AssetAccessClass; bytes: Buffer; cacheKind?: PublicAssetCacheKind; mimeType: string; ownerId?: string; projection: AssetReleaseManifestItem; relativePath: string; rootRef: string; sha256: string; } const releaseVersionPattern = /^[a-z0-9][a-z0-9._-]{0,79}$/i; const resourceIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const sha256Pattern = /^[0-9a-f]{64}$/; const rootRefPattern = /^[a-z0-9][a-z0-9._-]{0,79}$/i; function assetUrl(accessClass: AssetAccessClass, releaseVersion: string, resourceId: string) { if (accessClass === "public_release_asset") return `/api/v1/assets/public/${releaseVersion}/${resourceId}`; if (accessClass === "internal_preview_asset") return `/api/v1/assets/preview/${releaseVersion}/${resourceId}`; return `/api/v1/private-assets/${releaseVersion}/${resourceId}`; } function isSafeRelativePath(value: string) { if (!value || value.includes("\\") || posix.isAbsolute(value) || win32.isAbsolute(value)) return false; const segments = value.split("/"); return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== ".."); } function sha256(value: string | Uint8Array) { return createHash("sha256").update(value).digest("hex"); } function immutableProjection(input: Omit): AssetReleaseManifestProjection { const items = input.items.map((item) => Object.freeze({ ...item })); const manifestBody = JSON.stringify({ ...input, items }); return Object.freeze({ ...input, items: Object.freeze(items), manifest_sha256: sha256(manifestBody) }); } function validateItem(item: AssetReleaseItemInput, releaseVersion: string, seenIds: Set): StoredItem { if (!ASSET_ACCESS_CLASSES.includes(item.access_class)) throw new Error("asset access class is unsupported"); if (!resourceIdPattern.test(item.resource_id) || seenIds.has(item.resource_id)) throw new Error("resource_id must be a unique opaque UUID"); seenIds.add(item.resource_id); if (!rootRefPattern.test(item.root_ref)) throw new Error("root_ref is invalid"); if (!isSafeRelativePath(item.relative_path)) throw new Error("relative path must stay within its declared root"); if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/i.test(item.mime_type)) throw new Error("mime_type is invalid"); if (item.access_class === "public_release_asset" && !item.cache_kind) throw new Error("public release asset requires an allowlisted cache kind"); if (item.access_class !== "public_release_asset" && item.cache_kind) throw new Error("non-public assets cannot declare a public cache kind"); if (item.access_class === "private_user_asset" && !item.owner_id) throw new Error("private user asset requires owner_id"); if (item.access_class !== "private_user_asset" && item.owner_id) throw new Error("only private user assets can declare owner_id"); const bytes = Buffer.from(item.content); const digest = sha256(bytes); if (item.sha256 !== undefined && (!sha256Pattern.test(item.sha256) || item.sha256 !== digest)) { throw new Error("file SHA-256 does not match content"); } const projection: AssetReleaseManifestItem = { access_class: item.access_class, byte_size: bytes.byteLength, ...(item.cache_kind ? { cache_kind: item.cache_kind } : {}), mime_type: item.mime_type, release_version: releaseVersion, resource_id: item.resource_id, sha256: digest, url: assetUrl(item.access_class, releaseVersion, item.resource_id), }; return { accessClass: item.access_class, bytes, ...(item.cache_kind ? { cacheKind: item.cache_kind } : {}), mimeType: item.mime_type, ...(item.owner_id ? { ownerId: item.owner_id } : {}), projection: Object.freeze(projection), relativePath: item.relative_path, rootRef: item.root_ref, sha256: digest, }; } export function createAssetReleaseManifest(input: AssetReleaseManifestInput): AssetReleaseReader { if (!releaseVersionPattern.test(input.release_version)) throw new Error("release_version is invalid"); const seenIds = new Set(); const items = input.items .map((item) => validateItem(item, input.release_version, seenIds)) .sort((left, right) => left.projection.resource_id.localeCompare(right.projection.resource_id)); return Object.freeze({ project(accessClass: AssetAccessClass, releaseVersion: string, options: { ownerId?: string; resourceIds?: readonly string[] } = {}) { if (releaseVersion !== input.release_version) return undefined; const selected = items.filter((item) => item.accessClass === accessClass && (accessClass !== "private_user_asset" || Boolean(options.ownerId) && item.ownerId === options.ownerId) && (!options.resourceIds || options.resourceIds.includes(item.projection.resource_id))); return immutableProjection({ items: selected.map((item) => item.projection), release_version: input.release_version, schema_version: "AssetReleaseManifest/v1", }); }, read(accessClass: AssetAccessClass, releaseVersion: string, resourceId: string) { if (releaseVersion !== input.release_version) return undefined; const item = items.find((candidate) => candidate.accessClass === accessClass && candidate.projection.resource_id === resourceId); if (!item) return undefined; return { accessClass: item.accessClass, bytes: Buffer.from(item.bytes), mimeType: item.mimeType, ...(item.ownerId ? { ownerId: item.ownerId } : {}), releaseVersion, resourceId, sha256: item.sha256, }; }, }); }