import { createHash, randomUUID } from "node:crypto"; import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs"; import { createRequire } from "node:module"; import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path"; const require = createRequire(import.meta.url); const Database = require("better-sqlite3") as typeof import("better-sqlite3"); const assetIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const fixedDirectories = [ "db", "content/references", "content/generated", "content/exports", "managed-assets", "derived-assets", "staging", "logs/api", "logs/worker", "logs/supervisor", ] as const; export const DATA_TRANSFER_POLICY = { allowed_downloads: ["original_generation", "jpg", "png"], application_backup: false, business_import: false, editable_project_archive: false, p0b_migration: false, recovery: false, } as const; export interface LocalDataRootBoundaries { downloadsRoot: string; programRoot: string; projectRoots: string[]; readOnlyAssetRoots: string[]; repositoryRoot: string; } export type LocalDataRootRejectionReason = | "repository_root" | "program_root" | "project_root" | "downloads_root" | "read_only_asset_root" | "symbolic_link"; export type LocalDataRootValidation = | { ok: true; normalized_path: string } | { ok: false; reason: LocalDataRootRejectionReason }; interface InstanceConfiguration { data_root: string; initialized: true; instance_id: string; schema_version: 1; } export function readConfiguredLocalDataRoot(configFile = defaultInstanceConfigPath()) { const configuration = JSON.parse(readFileSync(configFile, "utf8")) as Record; const candidate = configuration.data_root ?? configuration.local_data_root; if (typeof candidate !== "string" || !isAbsolute(candidate)) throw new Error("data_root_configuration_invalid"); return resolve(candidate); } export interface ValidatedReadOnlyAssetRoot { absolute_root: string; ok: true; root_ref: string; } interface PublicAssetEntry { assetId: string; mimeType: string; relativePath: string; resourceVersion: string; rootRef: string; sha256: string; } export interface PublicAssetPayload { assetId: string; bytes: Buffer; mimeType: string; resourceVersion: string; sha256: string; } export interface PublicAssetResolver { read(resourceVersion: string, assetId: string): PublicAssetPayload | undefined; } export function defaultLocalDataRoot(environment: NodeJS.ProcessEnv = process.env) { const localAppData = environment.LOCALAPPDATA; if (!localAppData || !isAbsolute(localAppData)) throw new Error("local_app_data_unavailable"); return join(localAppData, "Dada", "P0A", "data"); } export function defaultInstanceConfigPath(environment: NodeJS.ProcessEnv = process.env) { const localAppData = environment.LOCALAPPDATA; if (!localAppData || !isAbsolute(localAppData)) throw new Error("local_app_data_unavailable"); return join(localAppData, "Dada", "P0A", "config", "instance.json"); } function comparisonPath(path: string) { const normalized = resolve(path).replace(/[\\/]+$/, ""); return process.platform === "win32" ? normalized.toLocaleLowerCase("en-US") : normalized; } function isSameOrWithin(candidate: string, root: string) { const candidatePath = comparisonPath(candidate); const rootPath = comparisonPath(root); const child = relative(rootPath, candidatePath); return child === "" || (!child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child)); } function pathsOverlap(left: string, right: string) { return isSameOrWithin(left, right) || isSameOrWithin(right, left); } function containsSymbolicLink(path: string) { const absolute = resolve(path); const parsed = parse(absolute); let current = parsed.root; const segments = absolute.slice(parsed.root.length).split(sep).filter(Boolean); for (const segment of segments) { current = join(current, segment); if (!existsSync(current)) break; if (lstatSync(current).isSymbolicLink()) return true; } return false; } function canonicalExistingPath(path: string) { const absolute = resolve(path); let existing = absolute; const missing: string[] = []; while (!existsSync(existing)) { const parent = dirname(existing); if (parent === existing) break; missing.unshift(existing.slice(parent.length + 1)); existing = parent; } const canonical = existsSync(existing) ? realpathSync.native(existing) : existing; return resolve(canonical, ...missing); } export function validateLocalDataRoot( candidate: string, boundaries: LocalDataRootBoundaries, ): LocalDataRootValidation { const absolute = resolve(candidate); if (containsSymbolicLink(absolute)) return { ok: false, reason: "symbolic_link" }; const canonical = canonicalExistingPath(absolute); const restricted: ReadonlyArray = [ ["repository_root", boundaries.repositoryRoot], ["program_root", boundaries.programRoot], ...boundaries.projectRoots.map((root) => ["project_root", root] as const), ["downloads_root", boundaries.downloadsRoot], ...boundaries.readOnlyAssetRoots.map((root) => ["read_only_asset_root", root] as const), ]; for (const [reason, root] of restricted) { if (pathsOverlap(canonical, canonicalExistingPath(root))) return { ok: false, reason }; } return { normalized_path: absolute, ok: true }; } export function resolvePathWithinRoot(root: string, objectKey: string) { if (!objectKey || isAbsolute(objectKey) || objectKey.split(/[\\/]/).includes("..")) { throw new Error("path_escape"); } if (containsSymbolicLink(root)) throw new Error("symbolic_link"); const target = resolve(root, objectKey); if (!isSameOrWithin(target, root) || containsSymbolicLink(target)) throw new Error("path_escape"); const canonicalRoot = canonicalExistingPath(root); const canonicalTarget = canonicalExistingPath(target); if (!isSameOrWithin(canonicalTarget, canonicalRoot)) throw new Error("path_escape"); return target; } function openInstanceDatabase(databasePath: string) { const nativeBinding = process.env.DADA_SQLITE_NATIVE_BINDING; const database = new Database(databasePath, nativeBinding ? { nativeBinding } : undefined); database.pragma("journal_mode = WAL"); database.exec(` CREATE TABLE instance_metadata ( singleton INTEGER PRIMARY KEY CHECK (singleton = 1), schema_version INTEGER NOT NULL CHECK (schema_version = 1) ); INSERT INTO instance_metadata (singleton, schema_version) VALUES (1, 1); `); database.close(); } export function initializeLocalDataRoot(input: { boundaries: LocalDataRootBoundaries; configFile: string; dataRoot: string; }) { const validation = validateLocalDataRoot(input.dataRoot, input.boundaries); if (!validation.ok) throw new Error(validation.reason); if (existsSync(input.configFile)) throw new Error("already_initialized"); if (existsSync(validation.normalized_path) && readdirSync(validation.normalized_path).length > 0) { throw new Error("data_root_not_empty"); } const createdRoot = !existsSync(validation.normalized_path); try { for (const directory of fixedDirectories) { mkdirSync(join(validation.normalized_path, directory), { recursive: true }); } openInstanceDatabase(join(validation.normalized_path, "db", "dada.sqlite3")); const configuration: InstanceConfiguration = { data_root: validation.normalized_path, initialized: true, instance_id: randomUUID(), schema_version: 1, }; mkdirSync(dirname(input.configFile), { recursive: true }); const temporaryConfig = `${input.configFile}.${randomUUID()}.tmp`; writeFileSync(temporaryConfig, `${JSON.stringify(configuration, null, 2)}\n`, { flag: "wx" }); renameSync(temporaryConfig, input.configFile); return { database: "db/dada.sqlite3", directories: [...fixedDirectories], status: "ready" as const, }; } catch (error) { if (createdRoot && existsSync(validation.normalized_path)) { rmSync(validation.normalized_path, { force: true, recursive: true }); } throw error; } } export function inspectInitializedLocalDataRoot(input: { boundaries: LocalDataRootBoundaries; configFile: string; }) { const configuration = JSON.parse(readFileSync(input.configFile, "utf8")) as InstanceConfiguration; const validation = validateLocalDataRoot(configuration.data_root, input.boundaries); if (!validation.ok) throw new Error(validation.reason); const rootExists = existsSync(validation.normalized_path) && statSync(validation.normalized_path).isDirectory(); const databasePath = join(validation.normalized_path, "db", "dada.sqlite3"); const databaseExists = rootExists && existsSync(databasePath) && statSync(databasePath).isFile(); if (!rootExists || !databaseExists) { return { database_exists: databaseExists, root_exists: rootExists, status: "data_missing" as const, }; } let writable = true; try { accessSync(validation.normalized_path, constants.R_OK | constants.W_OK); } catch { writable = false; } return { database_exists: true, root_exists: true, status: "ready" as const, writable, }; } export function validateReadOnlyAssetRoot(input: { dataRoot: string; expectedSha256: string; manifestRelativePath: string; root: string; rootRef: string; }): ValidatedReadOnlyAssetRoot | { ok: false; reason: string } { const absoluteRoot = resolve(input.root); if (!existsSync(absoluteRoot) || !statSync(absoluteRoot).isDirectory()) { return { ok: false, reason: "asset_root_missing" }; } if (containsSymbolicLink(absoluteRoot)) return { ok: false, reason: "symbolic_link" }; if (pathsOverlap(absoluteRoot, input.dataRoot)) return { ok: false, reason: "data_root_overlap" }; let manifestPath: string; try { manifestPath = resolvePathWithinRoot(absoluteRoot, input.manifestRelativePath); } catch { return { ok: false, reason: "manifest_path_invalid" }; } if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) { return { ok: false, reason: "manifest_missing" }; } const actualSha256 = createHash("sha256").update(readFileSync(manifestPath)).digest("hex"); if (actualSha256.toLowerCase() !== input.expectedSha256.toLowerCase()) { return { ok: false, reason: "manifest_hash_invalid" }; } return { absolute_root: absoluteRoot, ok: true, root_ref: input.rootRef }; } export function createPublicAssetResolver(input: { entries: PublicAssetEntry[]; roots: ValidatedReadOnlyAssetRoot[]; }): PublicAssetResolver { const roots = new Map(input.roots.map((root) => [root.root_ref, root.absolute_root])); const entries = new Map(); for (const entry of input.entries) { if (!assetIdPattern.test(entry.assetId) || entries.has(entry.assetId)) throw new Error("asset_id_invalid"); if (!roots.has(entry.rootRef)) throw new Error("asset_root_unvalidated"); if (!/^[a-z0-9][a-z0-9._-]{0,79}$/i.test(entry.resourceVersion)) throw new Error("resource_version_invalid"); if (!/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i.test(entry.mimeType)) throw new Error("mime_type_invalid"); entries.set(entry.assetId, { ...entry }); } return { read(resourceVersion, assetId) { if (!assetIdPattern.test(assetId)) return undefined; const entry = entries.get(assetId); if (!entry || entry.resourceVersion !== resourceVersion) return undefined; const root = roots.get(entry.rootRef); if (!root) return undefined; let path: string; try { path = resolvePathWithinRoot(root, entry.relativePath); } catch { return undefined; } if (!existsSync(path) || !statSync(path).isFile()) return undefined; const bytes = readFileSync(path); const sha256 = createHash("sha256").update(bytes).digest("hex"); if (sha256.toLowerCase() !== entry.sha256.toLowerCase()) return undefined; return { assetId: entry.assetId, bytes, mimeType: entry.mimeType, resourceVersion: entry.resourceVersion, sha256, }; }, }; }