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

View File

@@ -0,0 +1,9 @@
export { buildJoinSlug, extractSlugFromHostname } from './slug';
export {
assignJoinAddress,
formatJoinAddress,
getEdgeListenPort,
getEdgePublicHost,
getPlayDomain,
syncServerDnsRecords,
} from './sync';

View File

@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { buildJoinSlug, extractSlugFromHostname } from './slug';
describe('buildJoinSlug', () => {
it('sanitizes server names', () => {
const slug = buildJoinSlug('My Cool Server!', '00000000-0000-4000-8000-000000000099');
expect(slug).toMatch(/^my-cool-server-000000$/);
});
});
describe('extractSlugFromHostname', () => {
it('extracts slug from play domain hostname', () => {
expect(extractSlugFromHostname('abc.play.example.net', 'play.example.net')).toBe('abc');
});
it('rejects unknown domains', () => {
expect(extractSlugFromHostname('abc.other.net', 'play.example.net')).toBeNull();
});
});

34
packages/dns/src/slug.ts Normal file
View File

@@ -0,0 +1,34 @@
export function buildJoinSlug(serverName: string, serverId: string): string {
const base =
serverName
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 24) || 'server';
const suffix = serverId.replace(/-/g, '').slice(0, 6);
return `${base}-${suffix}`;
}
export function extractSlugFromHostname(hostname: string, playDomain: string): string | null {
const normalizedHost = hostname.split('\0')[0]?.toLowerCase() ?? '';
const normalizedDomain = playDomain.toLowerCase();
if (normalizedHost === normalizedDomain) {
return null;
}
const suffix = `.${normalizedDomain}`;
if (!normalizedHost.endsWith(suffix)) {
return null;
}
const slug = normalizedHost.slice(0, -suffix.length);
if (!slug || slug.includes('.')) {
return null;
}
return slug;
}

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()}`;
}