/** * Workspace-scoped domain helpers. * * Thin DB accessor layer on top of vibn_domains / vibn_domain_events / * vibn_billing_ledger. All inserts are idempotent by `(workspace_id, domain)` * so an accidental double-POST from an agent can't charge twice. */ import { query, queryOne } from '@/lib/db-postgres'; import { encryptSecret } from '@/lib/auth/secret-box'; import { domainTld } from '@/lib/opensrs'; export type DomainStatus = | 'pending' // intent recorded, not yet sent to registrar | 'registering' // OpenSRS call in flight | 'active' // registered, on our books | 'failed' // registrar rejected | 'expired' | 'deleted'; export interface VibnDomain { id: string; workspace_id: string; domain: string; tld: string; status: DomainStatus; registrar: string; registrar_order_id: string | null; registrar_username: string | null; /** Encrypted; call `getDomainRegistrarPassword` to decrypt. */ registrar_password_enc: string | null; period_years: number; whois_privacy: boolean; auto_renew: boolean; registered_at: Date | null; expires_at: Date | null; dns_provider: string | null; dns_zone_id: string | null; dns_nameservers: string[] | null; price_paid_cents: number | null; price_currency: string | null; created_by: string; created_at: Date; updated_at: Date; } export async function listDomainsForWorkspace(workspaceId: string): Promise { return query( `SELECT * FROM vibn_domains WHERE workspace_id = $1 ORDER BY created_at DESC`, [workspaceId], ); } export async function getDomainForWorkspace( workspaceId: string, domain: string, ): Promise { const row = await queryOne( `SELECT * FROM vibn_domains WHERE workspace_id = $1 AND domain = $2 LIMIT 1`, [workspaceId, domain.toLowerCase()], ); return row ?? null; } export interface CreateDomainIntentInput { workspaceId: string; domain: string; createdBy: string; periodYears?: number; whoisPrivacy?: boolean; } export async function createDomainIntent(input: CreateDomainIntentInput): Promise { const normalized = input.domain.toLowerCase().trim(); const tld = domainTld(normalized); const existing = await getDomainForWorkspace(input.workspaceId, normalized); if (existing) return existing; const rows = await query( `INSERT INTO vibn_domains (workspace_id, domain, tld, status, registrar, period_years, whois_privacy, created_by) VALUES ($1, $2, $3, 'pending', 'opensrs', $4, $5, $6) RETURNING *`, [ input.workspaceId, normalized, tld, input.periodYears ?? 1, input.whoisPrivacy ?? true, input.createdBy, ], ); return rows[0]; } export interface MarkRegisteredInput { domainId: string; registrarOrderId: string; registrarUsername: string; registrarPassword: string; periodYears: number; pricePaidCents: number | null; priceCurrency: string | null; registeredAt?: Date; /** Days from registeredAt to expiry (365*years). */ expiresAt?: Date; } export async function markDomainRegistered(input: MarkRegisteredInput): Promise { const encPw = encryptSecret(input.registrarPassword); const rows = await query( `UPDATE vibn_domains SET status = 'active', registrar_order_id = $2, registrar_username = $3, registrar_password_enc = $4, period_years = $5, price_paid_cents = $6, price_currency = $7, registered_at = COALESCE($8, now()), expires_at = $9, updated_at = now() WHERE id = $1 RETURNING *`, [ input.domainId, input.registrarOrderId, input.registrarUsername, encPw, input.periodYears, input.pricePaidCents, input.priceCurrency, input.registeredAt ?? null, input.expiresAt ?? null, ], ); return rows[0]; } export interface AttachDomainDnsInput { domainId: string; dnsProvider: string; dnsZoneId: string; dnsNameservers: string[]; } export async function markDomainAttached(input: AttachDomainDnsInput): Promise { const rows = await query( `UPDATE vibn_domains SET dns_provider = $2, dns_zone_id = $3, dns_nameservers = $4::jsonb, updated_at = now() WHERE id = $1 RETURNING *`, [input.domainId, input.dnsProvider, input.dnsZoneId, JSON.stringify(input.dnsNameservers)], ); return rows[0]; } export async function markDomainFailed(domainId: string, reason: string): Promise { const row = await queryOne<{ workspace_id: string }>( `UPDATE vibn_domains SET status = 'failed', updated_at = now() WHERE id = $1 RETURNING workspace_id`, [domainId], ); if (!row) return; await recordDomainEvent({ domainId, workspaceId: row.workspace_id, type: 'register.failed', payload: { reason }, }); } export interface RecordDomainEventInput { domainId: string; workspaceId: string; type: string; payload?: Record; } export async function recordDomainEvent(input: RecordDomainEventInput): Promise { await query( `INSERT INTO vibn_domain_events (domain_id, workspace_id, type, payload) VALUES ($1, $2, $3, $4::jsonb)`, [input.domainId, input.workspaceId, input.type, JSON.stringify(input.payload ?? {})], ); } export interface RecordLedgerInput { workspaceId: string; kind: 'debit' | 'credit'; amountCents: number; currency?: string; refType?: string; refId?: string; note?: string; } export async function recordLedgerEntry(input: RecordLedgerInput): Promise { await query( `INSERT INTO vibn_billing_ledger (workspace_id, kind, amount_cents, currency, ref_type, ref_id, note) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [ input.workspaceId, input.kind, input.amountCents, input.currency ?? 'CAD', input.refType ?? null, input.refId ?? null, input.note ?? null, ], ); }