import { DatabaseSync } from 'node:sqlite'; import { readFile, open, lstat, mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import { CustodyMarket } from './custody.ts'; import { PostgresCustody, type CustodyOptions } from './postgres-custody.ts'; import { custodyStorage, postgresConfig } from './postgres-config.ts'; /** No automatic SQLite reinterpretation or fallback after a database connection/configuration failure. */ export async function openCustodyStorage(directory: string, options: CustodyOptions, env = process.env): Promise { const storage = custodyStorage(env), marker = join(directory, 'testnet', 'custody-postgres.json'); const observerMode = env.WIKIDECK_CUSTODY_CANONICAL_REHEARSAL; if (observerMode !== undefined && observerMode !== '0' && observerMode !== '1') { throw new Error('WIKIDECK_CUSTODY_CANONICAL_REHEARSAL must be exactly 0 or 1'); } if (observerMode === '1' && (env.NODE_ENV === 'production' || storage !== 'postgres' || env.WIKIDECK_PG_DISPOSABLE_TEST !== '1')) { throw new Error('Canonical observer rehearsal requires PostgreSQL and the disposable database test guard'); } const custodyOptions = observerMode === '1' ? { ...options, disposableSaleRehearsal: true } : options; await mkdir(join(directory, 'testnet'), { recursive: true, mode: 0o700 }); let existing: { version: number; custodianId: string } | undefined; try { existing = JSON.parse(await readFile(marker, 'utf8')); } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; } if (storage === 'sqlite') { if (existing) throw new Error('Custody data directory is bound to PostgreSQL; SQLite fallback prohibited'); return CustodyMarket.open(directory, custodyOptions); } const source = join(directory, 'testnet', 'custody-sandbox.sqlite'); let frozen: string | undefined; try { const info = await lstat(source); if (!info.isFile() || info.isSymbolicLink()) throw new Error('Invalid custody source file'); const db = new DatabaseSync(source, { readOnly: true }); try { frozen = (db.prepare("SELECT value FROM custody_meta WHERE key='postgres_cutover'").get() as { value: string } | undefined)?.value; if (!frozen) throw new Error('Existing SQLite custody requires explicit stopped-node import before PostgreSQL activation'); } finally { db.close(); } } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; } const market = await PostgresCustody.open(await postgresConfig(env), custodyOptions); try { if (frozen && await market.importSource() !== frozen) throw new Error('PostgreSQL import does not match local frozen custody source'); if (existing && (existing.version !== 1 || existing.custodianId !== market.id)) throw new Error('PostgreSQL custodian differs from data-directory binding'); if (!existing) { const handle = await open(marker, 'wx', 0o600); try { await handle.writeFile(JSON.stringify({ version: 1, custodianId: market.id })); await handle.sync(); } finally { await handle.close(); } if (process.platform !== 'win32') { const parent = await open(join(directory, 'testnet'), 'r'); try { await parent.sync(); } finally { await parent.close(); } } } return market; } catch (e) { await market.close(); throw e; } }