export interface YonexusServerConfig { followerIdentifiers: string[]; notifyBotToken: string; adminUserId: string; listenHost?: string; listenPort: number; publicWsUrl?: string; } export class YonexusServerConfigError extends Error { readonly issues: string[]; constructor(issues: string[]) { super(`Invalid Yonexus.Server config: ${issues.join("; ")}`); this.name = "YonexusServerConfigError"; this.issues = issues; } } function isNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } function normalizeOptionalString(value: unknown): string | undefined { if (value === undefined || value === null) { return undefined; } if (typeof value !== "string") { return undefined; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function isValidPort(value: unknown): value is number { return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535; } function isValidWsUrl(value: string): boolean { try { const url = new URL(value); return url.protocol === "ws:" || url.protocol === "wss:"; } catch { return false; } } export function validateYonexusServerConfig(raw: unknown): YonexusServerConfig { const source = (raw && typeof raw === "object" ? raw : {}) as Record; const issues: string[] = []; const rawIdentifiers = source.followerIdentifiers; const followerIdentifiers = Array.isArray(rawIdentifiers) ? rawIdentifiers .filter((value): value is string => typeof value === "string") .map((value) => value.trim()) .filter((value) => value.length > 0) : []; if (!Array.isArray(rawIdentifiers)) { issues.push("followerIdentifiers must be an array"); } if (new Set(followerIdentifiers).size !== followerIdentifiers.length) { issues.push("followerIdentifiers must not contain duplicates"); } const rawNotifyBotToken = source.notifyBotToken; if (!isNonEmptyString(rawNotifyBotToken)) { issues.push("notifyBotToken is required"); } const rawAdminUserId = source.adminUserId; if (!isNonEmptyString(rawAdminUserId)) { issues.push("adminUserId is required"); } const rawListenPort = source.listenPort; if (!isValidPort(rawListenPort)) { issues.push("listenPort must be an integer between 1 and 65535"); } const listenHost = normalizeOptionalString(source.listenHost) ?? "0.0.0.0"; const publicWsUrl = normalizeOptionalString(source.publicWsUrl); if (publicWsUrl !== undefined && !isValidWsUrl(publicWsUrl)) { issues.push("publicWsUrl must be a valid ws:// or wss:// URL when provided"); } if (issues.length > 0) { throw new YonexusServerConfigError(issues); } const notifyBotToken = rawNotifyBotToken as string; const adminUserId = rawAdminUserId as string; const listenPort = rawListenPort as number; return { followerIdentifiers, notifyBotToken: notifyBotToken.trim(), adminUserId: adminUserId.trim(), listenHost, listenPort, publicWsUrl }; }