feat(config): add local startup boundary
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { resolveIdentity } from './identity.ts';
|
||||
import { PROFILES } from './profiles.ts';
|
||||
import {
|
||||
resolveRuntimeConfig,
|
||||
type ResolveRuntimeConfigOptions,
|
||||
type RuntimeConfig,
|
||||
} from './runtime-config.ts';
|
||||
|
||||
export interface LocalStartupContext {
|
||||
readonly hostKind: 'h5';
|
||||
readonly profile: 'local';
|
||||
readonly config: RuntimeConfig;
|
||||
}
|
||||
|
||||
const QUERY_CONTROLS = Object.freeze([
|
||||
'profile',
|
||||
'mode',
|
||||
'gameconfig',
|
||||
'agentid',
|
||||
'channelid',
|
||||
'marketid',
|
||||
'version',
|
||||
'gameid',
|
||||
] as const);
|
||||
|
||||
function fail(field: string, reason: string): never {
|
||||
throw new Error(`Invalid local startup ${field}: ${reason}`);
|
||||
}
|
||||
|
||||
function validateLocalUrl(url: string, field: string): void {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
} catch {
|
||||
fail(field, 'url must be absolute');
|
||||
}
|
||||
if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
|
||||
fail(field, 'url protocol must be ws: or wss:');
|
||||
}
|
||||
if (parsed.username !== '' || parsed.password !== '') {
|
||||
fail(field, 'url credentials are forbidden');
|
||||
}
|
||||
const hostname = parsed.hostname.toLowerCase();
|
||||
if (
|
||||
hostname !== 'localhost'
|
||||
&& hostname !== '[::1]'
|
||||
&& !/^127(?:\.[0-9]{1,3}){3}$/.test(hostname)
|
||||
) {
|
||||
fail(field, 'url hostname must be loopback');
|
||||
}
|
||||
}
|
||||
|
||||
function assertLocalQuery(search: string): void {
|
||||
const query = new URLSearchParams(search);
|
||||
for (const field of QUERY_CONTROLS) {
|
||||
if (query.getAll(field).length > 1) fail(field, 'duplicate query control');
|
||||
}
|
||||
|
||||
const profiles = query.getAll('profile');
|
||||
if (profiles.length !== 1 || profiles[0] !== 'local') {
|
||||
fail('profile', 'must occur exactly once with value local');
|
||||
}
|
||||
const modes = query.getAll('mode');
|
||||
if (modes.length === 1 && modes[0] !== 'debug') fail('mode', 'must be debug');
|
||||
const gameconfigs = query.getAll('gameconfig');
|
||||
if (gameconfigs.length === 1 && gameconfigs[0] !== '') {
|
||||
fail('gameconfig', 'must be empty');
|
||||
}
|
||||
if (query.has('gameid')) fail('gameid', 'query override is forbidden');
|
||||
}
|
||||
|
||||
export async function resolveLocalStartup(
|
||||
options: ResolveRuntimeConfigOptions,
|
||||
): Promise<LocalStartupContext> {
|
||||
if (options.mode !== 'debug') fail('mode', 'must be debug');
|
||||
if (options.hostKind !== 'h5') fail('hostKind', 'must be h5');
|
||||
assertLocalQuery(options.search);
|
||||
|
||||
const config = await resolveRuntimeConfig(options);
|
||||
const context = Object.freeze({ hostKind: 'h5' as const, profile: 'local' as const, config });
|
||||
assertLocalStartupContext(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export function assertLocalStartupContext(context: LocalStartupContext): void {
|
||||
if (context.hostKind !== 'h5') fail('hostKind', 'must be h5');
|
||||
if (context.profile !== 'local') fail('profile', 'must be local');
|
||||
|
||||
const { config } = context;
|
||||
if (config.mode !== 'debug') fail('mode', 'must be debug');
|
||||
if (config.isDebugger !== true) fail('isDebugger', 'must be true');
|
||||
if (config.source !== 'direct') fail('source', 'must be direct');
|
||||
if (config.gameserver !== null) fail('gameserver', 'must be null');
|
||||
if (config.rawConfig !== null) fail('rawConfig', 'must be null');
|
||||
|
||||
try {
|
||||
resolveIdentity([() => config.identity]);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) throw error;
|
||||
fail('identity', 'is invalid');
|
||||
}
|
||||
|
||||
const authoritative = PROFILES.local?.servers;
|
||||
if (!authoritative || authoritative.length === 0) {
|
||||
fail('servers', 'PROFILES.local must define servers');
|
||||
}
|
||||
if (config.servers.length !== authoritative.length) {
|
||||
fail('servers', 'must equal PROFILES.local.servers');
|
||||
}
|
||||
for (let index = 0; index < authoritative.length; index += 1) {
|
||||
const expected = authoritative[index];
|
||||
validateLocalUrl(expected, `servers[${index}]`);
|
||||
if (config.servers[index] !== expected) {
|
||||
fail('servers', 'must equal PROFILES.local.servers');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function assertLocalConnectionTarget(context: LocalStartupContext, url: string): void {
|
||||
assertLocalStartupContext(context);
|
||||
validateLocalUrl(url, 'url');
|
||||
if (!PROFILES.local?.servers?.includes(url)) {
|
||||
fail('url', 'target must occur in PROFILES.local.servers');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user