/** * Deploy-level backup target policy (issue #192, ADR 0026). A backup * destination is an egress path for the full content of the instance, so * the set of permissible destination HOSTS is fixed at deploy time via * `BACKUP_ALLOWED_TARGETS` — a runtime setting could be widened by a * compromised Site-Admin account. An empty allowlist disables every * remote target (WebDAV and rsync mirror); "local only" is the VS-NfD * reference configuration. Both the api (settings writes, admin view) and * the backup sidecar (the actual egress) enforce the same policy through * these helpers. */ /** Comma-separated env value → normalized, deduplicated host list. */ export function parseBackupTargetAllowlist(raw: string): string[] { return [ ...new Set( raw .split(',') .map((entry) => entry.trim().toLowerCase()) .filter(Boolean), ), ]; } /** * The destination host of a backup target: the hostname of a WebDAV URL, * or the host part of an rsync-over-ssh target (`user@host:/path`). * `null` for anything unparsable — which is never allowed. */ export function backupTargetHost(target: string): string | null { if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) { try { const hostname = new URL(target).hostname; return hostname ? hostname.toLowerCase() : null; } catch { return null; } } const rsync = /^(?:[^@\s]+@)?([^/:@\s]+):/.exec(target); return rsync ? rsync[1]!.toLowerCase() : null; } /** Whether `target` points at an allowlisted host. Empty list ⇒ never. */ export function isBackupTargetAllowed(allowlist: string[], target: string): boolean { const host = backupTargetHost(target); return host !== null && allowlist.includes(host); }