Files
Sauci/apps/api/src/migration/safety.ts
T
jjbrunton 006761d3b0 feat(backend): add standalone data plane
Replace the Supabase application data plane with the Node/PostgreSQL API, migrate mobile, admin, web, and MCP callers, add controlled cutover tooling, and define hardened Dokploy infrastructure.
2026-08-28 14:42:11 +01:00

31 lines
1.8 KiB
TypeScript

const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', 'host.docker.internal']);
function parseDatabaseUrl(label: string, value: string | undefined): URL {
if (!value) throw new Error(`${label} is required`);
let parsed: URL;
try { parsed = new URL(value); } catch { throw new Error(`${label} must be a valid PostgreSQL URL`); }
if (!['postgres:', 'postgresql:'].includes(parsed.protocol)) throw new Error(`${label} must use postgres:// or postgresql://`);
if (!parsed.hostname || !parsed.pathname.slice(1)) throw new Error(`${label} must identify a host and database`);
return parsed;
}
function databaseIdentity(url: URL): string {
return `${url.hostname.toLowerCase()}:${url.port || '5432'}/${decodeURIComponent(url.pathname.slice(1))}`;
}
export function validateEndpoints(sourceValue: string | undefined, targetValue: string | undefined, allowlistValue?: string): { source: string; target: string } {
const source = parseDatabaseUrl('SOURCE_DATABASE_URL', sourceValue);
const target = parseDatabaseUrl('TARGET_DATABASE_URL', targetValue);
if (databaseIdentity(source) === databaseIdentity(target)) throw new Error('Source and target must be different PostgreSQL databases');
const allowlist = new Set((allowlistValue ?? '').split(',').map((item) => item.trim().toLowerCase()).filter(Boolean));
if (!LOCAL_HOSTS.has(target.hostname.toLowerCase()) && !allowlist.has(target.hostname.toLowerCase())) {
throw new Error(`Target host is not allowlisted; add it explicitly to MIGRATION_TARGET_HOST_ALLOWLIST`);
}
return { source: source.toString(), target: target.toString() };
}
export function redactDatabaseUrl(value: string): string {
const parsed = new URL(value); parsed.username = parsed.username ? '***' : ''; parsed.password = parsed.password ? '***' : '';
return parsed.toString();
}