import { prisma } from '@hexahost/database'; import type { GameNode, GameServer } from '@hexahost/database'; import { createDnsProvider, resolveDnsProviderName } from './providers'; import type { DnsRecordSpec } from './providers'; import { buildJoinSlug } from './slug'; export function getPlayDomain(): string { return process.env['GAME_BASE_DOMAIN'] ?? 'play.example.net'; } export function getEdgePublicHost(): string { return process.env['EDGE_PUBLIC_HOST'] ?? '127.0.0.1'; } export function getEdgeListenPort(): number { const raw = process.env['EDGE_LISTEN_PORT'] ?? '25565'; const port = Number.parseInt(raw, 10); return Number.isFinite(port) ? port : 25565; } function joinHostname(slug: string, playDomain = getPlayDomain()): string { return `${slug}.${playDomain}`; } function initialDnsStatus(): 'PENDING' | 'ACTIVE' { return resolveDnsProviderName() === 'rfc2136' ? 'PENDING' : 'ACTIVE'; } async function upsertDnsRecord(input: { serverId: string; fqdn: string; recordType: string; target: string; port?: number; srvPriority?: number; srvWeight?: number; }): Promise { const status = initialDnsStatus(); await prisma.serverDnsRecord.upsert({ where: { serverId_recordType_fqdn: { serverId: input.serverId, recordType: input.recordType, fqdn: input.fqdn, }, }, create: { serverId: input.serverId, fqdn: input.fqdn, recordType: input.recordType, target: input.target, port: input.port, srvPriority: input.srvPriority, srvWeight: input.srvWeight, status, lastSyncedAt: status === 'ACTIVE' ? new Date() : null, }, update: { target: input.target, port: input.port, srvPriority: input.srvPriority, srvWeight: input.srvWeight, status, lastSyncedAt: status === 'ACTIVE' ? new Date() : null, lastError: null, }, }); } function toDnsRecordSpec(record: { serverId: string; fqdn: string; recordType: string; target: string; port: number | null; srvPriority: number | null; srvWeight: number | null; }): DnsRecordSpec { return { serverId: record.serverId, fqdn: record.fqdn, recordType: record.recordType as 'A' | 'SRV', target: record.target, port: record.port ?? undefined, srvPriority: record.srvPriority ?? undefined, srvWeight: record.srvWeight ?? undefined, }; } export async function pushPendingDnsRecords(limit = 50): Promise { const provider = createDnsProvider(); const pending = await prisma.serverDnsRecord.findMany({ where: { status: 'PENDING' }, take: limit, orderBy: { updatedAt: 'asc' }, }); let synced = 0; for (const record of pending) { try { await provider.syncRecord(toDnsRecordSpec(record)); await prisma.serverDnsRecord.update({ where: { id: record.id }, data: { status: 'ACTIVE', lastSyncedAt: new Date(), lastError: null, }, }); synced += 1; } catch (error) { await prisma.serverDnsRecord.update({ where: { id: record.id }, data: { status: 'FAILED', lastError: error instanceof Error ? error.message : 'DNS sync failed', }, }); } } return synced; } export async function syncServerDnsRecords( server: GameServer & { node: GameNode | null }, playDomain = getPlayDomain(), ): Promise { if (!server.joinSlug) { return; } const fqdn = joinHostname(server.joinSlug, playDomain); const edgeTarget = getEdgePublicHost(); const edgePort = getEdgeListenPort(); await upsertDnsRecord({ serverId: server.id, fqdn, recordType: 'A', target: edgeTarget, }); if (server.edition === 'JAVA') { await upsertDnsRecord({ serverId: server.id, fqdn: `_minecraft._tcp.${fqdn}`, recordType: 'SRV', target: fqdn, port: edgePort, srvPriority: 0, srvWeight: 5, }); } if (resolveDnsProviderName() === 'local') { return; } await pushPendingDnsRecords(10); } async function ensureUniqueSlug(serverId: string, serverName: string): Promise { let slug = buildJoinSlug(serverName, serverId); let attempt = 0; while ( await prisma.gameServer.findFirst({ where: { joinSlug: slug, NOT: { id: serverId } }, select: { id: true }, }) ) { attempt += 1; slug = `${buildJoinSlug(serverName, serverId)}-${attempt}`; } return slug; } export async function assignJoinAddress( serverId: string, options?: { playDomain?: string }, ): Promise<{ joinSlug: string; joinHostname: string } | null> { const playDomain = options?.playDomain ?? getPlayDomain(); const server = await prisma.gameServer.findUnique({ where: { id: serverId }, include: { node: true }, }); if (!server || !server.nodeId || server.hostPort === null) { return null; } const joinSlug = server.joinSlug ?? (await ensureUniqueSlug(serverId, server.name)); const updated = server.joinSlug === joinSlug ? server : await prisma.gameServer.update({ where: { id: serverId }, data: { joinSlug }, include: { node: true }, }); await syncServerDnsRecords(updated, playDomain); return { joinSlug, joinHostname: joinHostname(joinSlug, playDomain), }; } export function formatJoinAddress( joinSlug: string | null, edition: GameServer['edition'], playDomain = getPlayDomain(), ): string | null { if (!joinSlug) { return null; } const hostname = joinHostname(joinSlug, playDomain); return edition === 'JAVA' ? hostname : `${hostname}:${getEdgeListenPort()}`; }