/** * Google Cloud DNS driver. * * Auth uses the shared vibn-workspace-provisioner SA (which we granted * roles/dns.admin). Zones are created as PUBLIC managed zones in the * configured GCP project. Visibility is global (anycast) — see * `AI_CAPABILITIES_ROADMAP.md#P5.1` for the residency note. * * Every read/write goes through https://dns.googleapis.com/dns/v1. */ import { getGcpAccessToken, GCP_PROJECT_ID } from '@/lib/gcp-auth'; import type { DnsProvider, DnsRecord, DnsZone } from './provider'; const API = `https://dns.googleapis.com/dns/v1/projects/${GCP_PROJECT_ID}`; function zoneName(apex: string): string { // Cloud DNS managed-zone names must match [a-z0-9-]+ and start with a letter. const slug = apex.toLowerCase().replace(/[^a-z0-9-]/g, '-'); return `vibn-${slug}`; } async function authedFetch( method: 'GET' | 'POST' | 'DELETE' | 'PATCH', path: string, body?: unknown, ): Promise { const token = await getGcpAccessToken(); const headers: Record = { Authorization: `Bearer ${token}`, Accept: 'application/json', }; if (body) headers['Content-Type'] = 'application/json'; return fetch(`${API}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, }); } async function parseOrThrow(res: Response, context: string): Promise { const text = await res.text(); if (!res.ok) { throw new Error(`[cloud-dns ${context} ${res.status}] ${text.slice(0, 500)}`); } return text ? JSON.parse(text) : ({} as T); } interface CloudDnsManagedZone { name: string; dnsName: string; nameServers?: string[]; creationTime?: string; visibility?: string; } interface CloudDnsRrSet { name: string; type: string; ttl?: number; rrdatas: string[]; } interface CloudDnsChange { additions?: CloudDnsRrSet[]; deletions?: CloudDnsRrSet[]; kind: 'dns#change'; } export const cloudDnsProvider: DnsProvider = { id: 'cloud_dns', async createZone(apex) { const name = zoneName(apex); const dnsName = apex.endsWith('.') ? apex : `${apex}.`; // Idempotent: if the zone already exists, return it unchanged. const existing = await cloudDnsProvider.getZone(apex); if (existing) return existing; const res = await authedFetch('POST', `/managedZones`, { name, dnsName, description: `Vibn-managed zone for ${apex}`, visibility: 'public', }); const zone = await parseOrThrow(res, 'createZone'); return { apex, zoneId: zone.name, nameservers: zone.nameServers ?? [], createdAt: zone.creationTime, }; }, async getZone(apex) { const name = zoneName(apex); const res = await authedFetch('GET', `/managedZones/${name}`); if (res.status === 404) return null; const zone = await parseOrThrow(res, 'getZone'); return { apex, zoneId: zone.name, nameservers: zone.nameServers ?? [], createdAt: zone.creationTime, }; }, /** * Replaces the record set for each (name,type) pair in `records`. Other * record sets on the zone are untouched. */ async setRecords(apex, records) { const name = zoneName(apex); const dnsName = apex.endsWith('.') ? apex : `${apex}.`; // Fetch existing rrsets for these (name,type) pairs so we can delete // them as part of the atomic change. const existingRes = await authedFetch('GET', `/managedZones/${name}/rrsets`); const existingJson = await parseOrThrow<{ rrsets?: CloudDnsRrSet[] }>(existingRes, 'listRrsets'); const existing = existingJson.rrsets ?? []; const rel = (nm: string) => (nm === '@' ? dnsName : `${nm}.${dnsName}`); const additions: CloudDnsRrSet[] = records.map(r => ({ name: rel(r.name), type: r.type, ttl: r.ttl ?? 300, rrdatas: r.rrdatas, })); const wantPairs = new Set(additions.map(a => `${a.name}|${a.type}`)); const deletions = existing.filter(e => wantPairs.has(`${e.name}|${e.type}`)); const change: CloudDnsChange = { kind: 'dns#change', additions, deletions }; const res = await authedFetch('POST', `/managedZones/${name}/changes`, change); await parseOrThrow(res, 'setRecords'); }, async deleteZone(apex) { const name = zoneName(apex); const res = await authedFetch('DELETE', `/managedZones/${name}`); if (res.status === 404) return; await parseOrThrow(res, 'deleteZone'); }, };