import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { createPublicAssetResolver, readConfiguredAssetRoot, validateReadOnlyAssetRoot, type PublicAssetEntry, type PublicAssetResolver, } from "./local-data-root.js"; const rootRef = "p0a_runtime_assets"; const schemaVersion = "DadaRuntimeAssets/v1"; const assetIdPattern = /^[a-z0-9][a-z0-9_-]{2,119}$/i; const releasePattern = /^[a-z0-9][a-z0-9._-]{0,79}$/i; const shaPattern = /^[a-f0-9]{64}$/i; export interface RuntimeAssetState { checked_at: string; configured: boolean; pause_reason: "asset_manifest_invalid" | "asset_root_missing" | "asset_root_state_missing" | null; status: "active" | "unavailable"; } export interface LoadedRuntimeAssets { publicAssets?: PublicAssetResolver; state: RuntimeAssetState; } function parseRuntimeManifest(bytes: Buffer): PublicAssetEntry[] { const value = JSON.parse(bytes.toString("utf8")) as Record; if (value.schema_version !== schemaVersion || value.source !== "external_read_only" || value.root_ref !== rootRef) { throw new Error("runtime_asset_manifest_invalid"); } if (!Array.isArray(value.entries) || value.entries.length === 0) throw new Error("runtime_asset_manifest_invalid"); return value.entries.map((candidate) => { if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new Error("runtime_asset_manifest_invalid"); const entry = candidate as Record; if ( typeof entry.assetId !== "string" || !assetIdPattern.test(entry.assetId) || typeof entry.mimeType !== "string" || !/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i.test(entry.mimeType) || typeof entry.relativePath !== "string" || entry.relativePath.includes("\\") || entry.relativePath.split("/").includes("..") || typeof entry.resourceVersion !== "string" || !releasePattern.test(entry.resourceVersion) || entry.rootRef !== rootRef || typeof entry.sha256 !== "string" || !shaPattern.test(entry.sha256) ) throw new Error("runtime_asset_manifest_invalid"); return entry as unknown as PublicAssetEntry; }); } function unavailable( configured: boolean, pauseReason: Exclude, checkedAt: string, ): LoadedRuntimeAssets { return { state: { checked_at: checkedAt, configured, pause_reason: pauseReason, status: "unavailable" } }; } export function loadConfiguredRuntimeAssets(input: { configFile: string; dataRoot: string; trustedManifestPath: string; clock?: () => number; }): LoadedRuntimeAssets { const checkedAt = new Date((input.clock ?? Date.now)()).toISOString(); let assetRoot: string; try { assetRoot = readConfiguredAssetRoot(input.configFile); } catch { return unavailable(false, "asset_root_state_missing", checkedAt); } if (!existsSync(input.trustedManifestPath)) return unavailable(true, "asset_manifest_invalid", checkedAt); try { const trustedBytes = readFileSync(input.trustedManifestPath); const entries = parseRuntimeManifest(trustedBytes); const validatedRoot = validateReadOnlyAssetRoot({ dataRoot: input.dataRoot, expectedSha256: createHash("sha256").update(trustedBytes).digest("hex"), manifestRelativePath: "manifest.json", root: assetRoot, rootRef, }); if (!validatedRoot.ok) { return unavailable(true, validatedRoot.reason === "asset_root_missing" ? "asset_root_missing" : "asset_manifest_invalid", checkedAt); } return { publicAssets: createPublicAssetResolver({ entries, roots: [validatedRoot] }), state: { checked_at: checkedAt, configured: true, pause_reason: null, status: "active" }, }; } catch { return unavailable(true, "asset_manifest_invalid", checkedAt); } }