Phase8
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 9s
CI / Go — edge-gateway build (push) Successful in 17s

This commit is contained in:
smueller
2026-06-26 13:33:03 +02:00
parent ab21f53cdd
commit b335f6a497
39 changed files with 1477 additions and 20 deletions

158
packages/dns/src/sync.ts Normal file
View File

@@ -0,0 +1,158 @@
import { prisma } from '@hexahost/database';
import type { GameNode, GameServer } from '@hexahost/database';
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}`;
}
async function upsertDnsRecord(input: {
serverId: string;
fqdn: string;
recordType: string;
target: string;
port?: number;
srvPriority?: number;
srvWeight?: number;
}): Promise<void> {
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: 'ACTIVE',
lastSyncedAt: new Date(),
},
update: {
target: input.target,
port: input.port,
srvPriority: input.srvPriority,
srvWeight: input.srvWeight,
status: 'ACTIVE',
lastSyncedAt: new Date(),
lastError: null,
},
});
}
export async function syncServerDnsRecords(
server: GameServer & { node: GameNode | null },
playDomain = getPlayDomain(),
): Promise<void> {
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,
});
}
}
async function ensureUniqueSlug(serverId: string, serverName: string): Promise<string> {
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()}`;
}