Files
vibn-frontend/scripts/smoke-attach-e2e.ts
Mark Henderson 651ddf1e11 Rip out Theia, ship P5.1 attach E2E + Justine UI work-in-progress
Theia rip-out:
- Delete app/api/theia-auth/route.ts (Traefik ForwardAuth shim)
- Delete app/api/projects/[projectId]/workspace/route.ts and
  app/api/projects/prewarm/route.ts (Cloud Run Theia provisioning)
- Delete lib/cloud-run-workspace.ts and lib/coolify-workspace.ts
- Strip provisionTheiaWorkspace + theiaWorkspaceUrl/theiaAppUuid/
  theiaError from app/api/projects/create/route.ts response
- Remove Theia callbackUrl branch in app/auth/page.tsx
- Drop "Open in Theia" button + xterm/Theia PTY copy in build/page.tsx
- Drop theiaWorkspaceUrl from deployment/page.tsx Project type
- Strip Theia IDE line + theia-code-os from advisor + agent-chat
  context strings
- Scrub Theia mention from lib/auth/workspace-auth.ts comment

P5.1 (custom apex domains + DNS):
- lib/coolify.ts + lib/opensrs.ts: nameserver normalization, OpenSRS
  XML auth, Cloud DNS plumbing
- scripts/smoke-attach-e2e.ts: full prod GCP + sandbox OpenSRS +
  prod Coolify smoke covering register/zone/A/NS/PATCH/cleanup

In-progress (Justine onboarding/build, MVP setup, agent telemetry):
- New (justine)/stories, project (home) layouts, mvp-setup, run, tasks
  routes + supporting components
- Project shell + sidebar + nav refactor for the Stackless palette
- Agent session API hardening (sessions, events, stream, approve,
  retry, stop) + atlas-chat, advisor, design-surfaces refresh
- New scripts/sync-db-url-from-coolify.mjs +
  scripts/prisma-db-push.mjs + docker-compose.local-db.yml for
  local Prisma workflows
- lib/dev-bypass.ts, lib/chat-context-refs.ts, lib/prd-sections.ts
- Misc: stories CSS, debug/prisma route, modal-theme, BuildLivePlanPanel

Made-with: Cursor
2026-04-22 18:05:01 -07:00

297 lines
14 KiB
TypeScript

/**
* 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<CoolifyApp> {
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<CoolifyApp>;
}
async function coolifySetAppDomains(uuid: string, domainsCsv: string): Promise<void> {
// 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);
});