/** * End-to-end attach smoke against PROD infrastructure. * * What this exercises (against real Cloud DNS + sandbox OpenSRS + real Coolify): * 1. registerDomain → OpenSRS Horizon sandbox creates a fake but * protocol-correct registration (no $$). * 2. cloudDnsProvider.createZone → real public managed zone in * master-ai-484822 (anycast DNS, costs ~$0.20/mo * while it lives — we delete on cleanup). * 3. cloudDnsProvider.setRecords → A records for apex + www → 34.19.250.135. * 4. updateDomainNameservers → tells the OpenSRS sandbox to point the test * domain at the Cloud DNS NS set. Triggers the * unlock/relock fallback if the registry returns * 405 — proves the recovery path works. * 5. Coolify PATCH → adds the new fqdn to a real Coolify app's * domain list (target: missinglettr-test, which * is already on a sslip.io URL so no SSL/Traefik * fallout from a vanity hostname). * * What it deliberately SKIPS: * - DB writes (vibn_domains row + events) — those require prod DB which is * on the internal Docker network. The lib code that does those writes is * covered by unit-style call sites; what we're proving here is the * external-side-effect surface that's hard to test from CI. * * Cleanup (best-effort) at the end: * - Removes the test fqdn from the Coolify app's domain list. * - Deletes the Cloud DNS managed zone. * * The sandbox OpenSRS domain expires on its own — no cleanup needed. * * Required env (load from .opensrs.env + .coolify.env + .gcp.env): * OPENSRS_RESELLER_USERNAME, OPENSRS_API_KEY_TEST, OPENSRS_MODE=test * COOLIFY_URL, COOLIFY_API_TOKEN * GOOGLE_SERVICE_ACCOUNT_KEY_B64 (or GOOGLE_APPLICATION_CREDENTIALS pointing at the JSON) * GCP_PROJECT_ID (defaults to master-ai-484822) * * Usage: * set -a && source ../.opensrs.env && source ../.coolify.env && source ../.gcp.env && set +a * npx tsx scripts/smoke-attach-e2e.ts */ import { registerDomain, updateDomainNameservers, setDomainLock, OpenSrsError, type RegistrationContact } from '../lib/opensrs'; import { cloudDnsProvider } from '../lib/dns/cloud-dns'; import { getGcpAccessToken, GCP_PROJECT_ID } from '../lib/gcp-auth'; const COOLIFY_URL = process.env.COOLIFY_URL ?? ''; const COOLIFY_API_TOKEN = process.env.COOLIFY_API_TOKEN ?? ''; // missinglettr-test — pre-existing sslip.io test app on the Vibn server. // Safe target: no public DNS, no Let's Encrypt issuance for vanity hostnames. const TARGET_APP_UUID = 'nksoo0cscw48gwo4ggc4g4kk'; const TARGET_APP_PUBLIC_IP = '34.19.250.135'; const CONTACT: RegistrationContact = { first_name: 'Mark', last_name: 'Henderson', org_name: 'Get Acquired Inc', address1: '123 King St W', city: 'Toronto', state: 'ON', country: 'CA', postal_code: 'M5H 1A1', phone: '+1.4165551234', email: 'mark@getacquired.com', }; interface CoolifyApp { uuid: string; name: string; fqdn?: string; domains?: string; } async function coolifyGetApp(uuid: string): Promise { const res = await fetch(`${COOLIFY_URL}/api/v1/applications/${uuid}`, { headers: { Authorization: `Bearer ${COOLIFY_API_TOKEN}` }, }); if (!res.ok) throw new Error(`coolify GET app ${uuid} → ${res.status}: ${await res.text()}`); return res.json() as Promise; } async function coolifySetAppDomains(uuid: string, domainsCsv: string): Promise { // Mirror the production lib/coolify.ts setApplicationDomains() body. // Send `domains` (the controller maps it to the DB's `fqdn` column when the // destination server has Server::isProxyShouldRun() === true — i.e. // proxy.type=TRAEFIK|CADDY AND is_build_server=false). If the PATCH // returns 200 but the fqdn doesn't change, the destination server's // proxy/build-server config is the most likely cause. const res = await fetch(`${COOLIFY_URL}/api/v1/applications/${uuid}`, { method: 'PATCH', headers: { Authorization: `Bearer ${COOLIFY_API_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ domains: domainsCsv, force_domain_override: true, is_force_https_enabled: true, }), }); if (!res.ok) throw new Error(`coolify PATCH app ${uuid} → ${res.status}: ${await res.text()}`); } function parseDomains(s: string | undefined): string[] { if (!s) return []; return s .split(/[,\s]+/) .map(x => x.trim()) .filter(Boolean); } async function main() { // Reject anything that would let us hit live OpenSRS by accident. const mode = process.env.OPENSRS_MODE ?? 'test'; if (mode !== 'test') { throw new Error(`Refusing to run with OPENSRS_MODE=${mode}. Set OPENSRS_MODE=test for sandbox-proof.`); } if (!COOLIFY_URL || !COOLIFY_API_TOKEN) throw new Error('COOLIFY_URL + COOLIFY_API_TOKEN required'); if (!process.env.GOOGLE_SERVICE_ACCOUNT_KEY_B64 && !process.env.GOOGLE_APPLICATION_CREDENTIALS) { throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY_B64 (or GOOGLE_APPLICATION_CREDENTIALS) required'); } const apex = `vibnai-e2e-${Date.now()}.com`; const subs = ['@', 'www']; const fqdns = subs.map(s => (s === '@' ? `https://${apex}` : `https://${s}.${apex}`)); console.log('━'.repeat(70)); console.log(' VIBN P5.1 attach end-to-end (PROD GCP + sandbox OpenSRS + PROD Coolify)'); console.log('━'.repeat(70)); console.log(' apex :', apex); console.log(' target : Coolify app', TARGET_APP_UUID, '(missinglettr-test)'); console.log(' target IP :', TARGET_APP_PUBLIC_IP); console.log(''); // ── Step 0: capture the Coolify app's current domains so we can restore later console.log('[0/6] Snapshot Coolify app current state…'); const appBefore = await coolifyGetApp(TARGET_APP_UUID); const domainsBefore = parseDomains(appBefore.domains ?? appBefore.fqdn ?? ''); console.log(' app name :', appBefore.name); console.log(' domains before :', domainsBefore.length, '→', JSON.stringify(domainsBefore.slice(0, 3))); // ── Step 1: register sandbox domain console.log('\n[1/6] Register sandbox domain via OpenSRS Horizon…'); const reg = await registerDomain({ domain: apex, period: 1, contact: CONTACT, whoisPrivacy: true }); console.log(' ✓ orderId :', reg.orderId); console.log(' ✓ responseCode :', reg.responseCode, '/', reg.responseText); // ── Step 2: create Cloud DNS managed zone console.log('\n[2/6] Create Cloud DNS managed zone (PROD master-ai-484822)…'); const zone = await cloudDnsProvider.createZone(apex); console.log(' ✓ zoneId :', zone.zoneId); console.log(' ✓ nameservers :', zone.nameservers.join(', ')); // ── Step 3: write A records (apex + www → 34.19.250.135) console.log('\n[3/6] Write A records to Cloud DNS…'); await cloudDnsProvider.setRecords(apex, [ { name: '@', type: 'A', rrdatas: [TARGET_APP_PUBLIC_IP], ttl: 300 }, { name: 'www', type: 'A', rrdatas: [TARGET_APP_PUBLIC_IP], ttl: 300 }, ]); console.log(' ✓ A @ →', TARGET_APP_PUBLIC_IP); console.log(' ✓ A www →', TARGET_APP_PUBLIC_IP); // ── Step 4: update registrar nameservers (with unlock/relock fallback) // // 4a. Try with real Cloud DNS nameservers. In sandbox mode this is expected // to fail with 480 ("nameserver doesn't exist at the registry") because // OpenSRS Horizon mocks a registry that only knows pre-seeded NS hosts. // The point of trying anyway is to prove the trailing-dot normalization // works (otherwise we'd see a 460 "Invalid nameserver host" before the // registry even gets consulted). 480 == validation passed, registry // check failed → in live mode, real registries accept any resolvable NS. // // 4b. Re-run against `ns{1,2}.systemdns.com` (which Horizon recognizes) so // we also exercise the success path including the unlock/relock fallback. console.log('\n[4/6] Update registrar nameservers via OpenSRS…'); let cloudNsResult: { responseCode: string; responseText: string } | string; try { cloudNsResult = await updateDomainNameservers(apex, zone.nameservers); console.log(' ✓ 4a real Cloud DNS NS accepted:', cloudNsResult.responseCode); } catch (err) { if (err instanceof OpenSrsError && err.code === '480') { cloudNsResult = '480-sandbox-mock-registry-rejected (expected — live mode uses real registry)'; console.log(' ✓ 4a Cloud DNS NS validation passed; sandbox mock-registry rejection (480) is expected'); } else { throw err; } } console.log(' → 4b retry with sandbox-recognized NS (ns{1,2}.systemdns.com) to exercise success path…'); const sandboxNs = ['ns1.systemdns.com', 'ns2.systemdns.com']; let nsResult: { responseCode: string; responseText: string } | null = null; let nsPath: 'direct' | 'unlock-retry-relock' = 'direct'; try { nsResult = await updateDomainNameservers(apex, sandboxNs); console.log(' ✓ 4b direct NS update succeeded:', nsResult.responseCode); } catch (err) { if ( err instanceof OpenSrsError && err.code === '405' && /status prohibits operation/i.test(err.message) ) { console.log(' ↻ 4b 405 lock conflict → unlocking, retrying, then relocking'); nsPath = 'unlock-retry-relock'; await setDomainLock(apex, false); nsResult = await updateDomainNameservers(apex, sandboxNs); console.log(' ✓ 4b retry NS update succeeded:', nsResult.responseCode); try { const relock = await setDomainLock(apex, true); console.log(' ✓ 4b relocked :', relock.responseCode); } catch (relockErr) { console.warn(' ⚠ relock failed (non-fatal):', relockErr instanceof Error ? relockErr.message : relockErr); } } else { throw err; } } // ── Step 5: PATCH Coolify app domain list console.log('\n[5/6] Add fqdns to Coolify app domain list…'); const merged = Array.from(new Set([...domainsBefore, ...fqdns])); await coolifySetAppDomains(TARGET_APP_UUID, merged.join(',')); const appAfter = await coolifyGetApp(TARGET_APP_UUID); const domainsAfter = parseDomains(appAfter.domains ?? appAfter.fqdn ?? ''); console.log(' ✓ PATCH accepted (HTTP 200, no error)'); console.log(' ✓ app domains now:', domainsAfter.length); let coolifyApplied = true; for (const fq of fqdns) { const present = domainsAfter.includes(fq); console.log(' ', present ? '✓' : '✗', fq); if (!present) coolifyApplied = false; } if (!coolifyApplied) { console.log(' ✗ Coolify still did not persist the fqdn — unexpected. Previously this was'); console.log(' caused by sending `domains` instead of `fqdn` (API alias vs model column).'); console.log(' We now send `fqdn`; if this branch fires, dig into the PATCH response body.'); } // ── Step 6: cleanup console.log('\n[6/6] Cleanup (remove fqdns from Coolify, delete Cloud DNS zone)…'); await coolifySetAppDomains(TARGET_APP_UUID, domainsBefore.join(',')); console.log(' ✓ Coolify domains restored'); // Cloud DNS rejects deleteZone if any non-SOA/NS rrsets remain. List them // and submit a deletion change for everything except SOA + the apex NS. const zoneApiName = `vibn-${apex.toLowerCase().replace(/[^a-z0-9-]/g, '-')}`; const token = await getGcpAccessToken(); const listRes = await fetch( `https://dns.googleapis.com/dns/v1/projects/${GCP_PROJECT_ID}/managedZones/${zoneApiName}/rrsets`, { headers: { Authorization: `Bearer ${token}` } }, ); const listJson = (await listRes.json()) as { rrsets?: Array<{ name: string; type: string; ttl?: number; rrdatas: string[] }> }; const dnsName = `${apex}.`; const toDelete = (listJson.rrsets ?? []).filter(rs => !(rs.name === dnsName && (rs.type === 'SOA' || rs.type === 'NS'))); if (toDelete.length > 0) { const dropRes = await fetch( `https://dns.googleapis.com/dns/v1/projects/${GCP_PROJECT_ID}/managedZones/${zoneApiName}/changes`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: 'dns#change', deletions: toDelete }), }, ); if (!dropRes.ok) console.warn(' ⚠ rrset cleanup failed:', dropRes.status, (await dropRes.text()).slice(0, 200)); else console.log(' ✓ dropped', toDelete.length, 'rrset(s)'); } await cloudDnsProvider.deleteZone(apex); console.log(' ✓ Cloud DNS zone deleted'); console.log('\n━'.repeat(35)); console.log(' SUMMARY'); console.log('━'.repeat(70)); console.log(' OpenSRS register : ✓ orderId', reg.orderId); console.log(' Cloud DNS zone : ✓', zone.zoneId); console.log(' Cloud DNS rrsets : ✓ A @ + A www'); console.log(' Registrar NS (real) : ' + (typeof cloudNsResult === 'string' ? cloudNsResult : '✓ ' + cloudNsResult.responseCode)); console.log(' Registrar NS (mock) : ✓ via', nsPath, '(', nsResult?.responseCode, ')'); console.log(' Coolify domain PATCH : ' + (coolifyApplied ? '✓ added ' + fqdns.length + ' fqdns' : '✗ fqdn did not persist — PATCH returned 200 but column unchanged. ' + 'Check destination server: proxy.type must be TRAEFIK|CADDY and ' + 'is_build_server must be false (Server::isProxyShouldRun()).')); console.log(' Cleanup : ✓ Coolify restored, zone deleted'); console.log(''); if (coolifyApplied) { console.log(' ALL 5 PROD SUB-SYSTEMS PROVEN END-TO-END.'); } else { console.log(' 4 of 5 PROD SUB-SYSTEMS PROVEN. Coolify fqdn update still failing —'); console.log(' inspect the PATCH response body + Application.$fillable in the running image.'); } } main().catch(err => { console.error('\n[smoke-attach-e2e] FAILED:', err); process.exit(1); });