From 31f41cb49bb2d4ac22059b296a996115a1d822dd Mon Sep 17 00:00:00 2001 From: nav Date: Thu, 9 Apr 2026 04:38:07 +0000 Subject: [PATCH] Fix strict TypeScript checks for server --- plugin/core/config.ts | 18 +++++++++++------- plugin/core/persistence.ts | 5 +++++ plugin/core/runtime.ts | 11 +++++++---- plugin/notifications/discord.ts | 2 +- plugin/types/ws.d.ts | 21 +++++++++++++++++++++ tests/state-recovery.test.ts | 3 +++ tsconfig.json | 6 ++++-- 7 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 plugin/types/ws.d.ts diff --git a/plugin/core/config.ts b/plugin/core/config.ts index 0bb78cc..eee4bc8 100644 --- a/plugin/core/config.ts +++ b/plugin/core/config.ts @@ -35,7 +35,7 @@ function normalizeOptionalString(value: unknown): string | undefined { } function isValidPort(value: unknown): value is number { - return Number.isInteger(value) && value >= 1 && value <= 65535; + return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535; } function isValidWsUrl(value: string): boolean { @@ -67,18 +67,18 @@ export function validateYonexusServerConfig(raw: unknown): YonexusServerConfig { issues.push("followerIdentifiers must not contain duplicates"); } - const notifyBotToken = source.notifyBotToken; - if (!isNonEmptyString(notifyBotToken)) { + const rawNotifyBotToken = source.notifyBotToken; + if (!isNonEmptyString(rawNotifyBotToken)) { issues.push("notifyBotToken is required"); } - const adminUserId = source.adminUserId; - if (!isNonEmptyString(adminUserId)) { + const rawAdminUserId = source.adminUserId; + if (!isNonEmptyString(rawAdminUserId)) { issues.push("adminUserId is required"); } - const listenPort = source.listenPort; - if (!isValidPort(listenPort)) { + const rawListenPort = source.listenPort; + if (!isValidPort(rawListenPort)) { issues.push("listenPort must be an integer between 1 and 65535"); } @@ -93,6 +93,10 @@ export function validateYonexusServerConfig(raw: unknown): YonexusServerConfig { throw new YonexusServerConfigError(issues); } + const notifyBotToken = rawNotifyBotToken as string; + const adminUserId = rawAdminUserId as string; + const listenPort = rawListenPort as number; + return { followerIdentifiers, notifyBotToken: notifyBotToken.trim(), diff --git a/plugin/core/persistence.ts b/plugin/core/persistence.ts index da93fcc..80705cd 100644 --- a/plugin/core/persistence.ts +++ b/plugin/core/persistence.ts @@ -79,6 +79,9 @@ export interface ClientRecord { /** Last successful authentication timestamp (UTC unix seconds) */ lastAuthenticatedAt?: number; + /** Last successful pairing timestamp (UTC unix seconds) */ + pairedAt?: number; + /** * Recent nonces used in authentication attempts. * This is a rolling window that may be cleared on restart. @@ -151,6 +154,7 @@ export interface SerializedClientRecord { status: ClientLivenessStatus; lastHeartbeatAt?: number; lastAuthenticatedAt?: number; + pairedAt?: number; createdAt: number; updatedAt: number; // Note: recentNonces and recentHandshakeAttempts are intentionally @@ -203,6 +207,7 @@ export function serializeClientRecord(record: ClientRecord): SerializedClientRec status: record.status, lastHeartbeatAt: record.lastHeartbeatAt, lastAuthenticatedAt: record.lastAuthenticatedAt, + pairedAt: record.pairedAt, createdAt: record.createdAt, updatedAt: record.updatedAt }; diff --git a/plugin/core/runtime.ts b/plugin/core/runtime.ts index 207b7b8..68bf972 100644 --- a/plugin/core/runtime.ts +++ b/plugin/core/runtime.ts @@ -52,6 +52,7 @@ export interface YonexusServerRuntimeOptions { config: YonexusServerConfig; store: YonexusServerStore; transport: ServerTransport; + notificationService?: DiscordNotificationService; now?: () => number; sweepIntervalMs?: number; } @@ -80,10 +81,12 @@ export class YonexusServerRuntime { }; this.sweepIntervalMs = options.sweepIntervalMs ?? 30_000; this.pairingService = createPairingService({ now: this.now }); - this.notificationService = createDiscordNotificationService({ - botToken: options.config.notifyBotToken, - adminUserId: options.config.adminUserId - }); + this.notificationService = + options.notificationService ?? + createDiscordNotificationService({ + botToken: options.config.notifyBotToken, + adminUserId: options.config.adminUserId + }); } get state(): ServerLifecycleState { diff --git a/plugin/notifications/discord.ts b/plugin/notifications/discord.ts index c629965..b10a342 100644 --- a/plugin/notifications/discord.ts +++ b/plugin/notifications/discord.ts @@ -42,7 +42,7 @@ interface CreateDmChannelResponse { interface SendDiscordDirectMessageOptions { config: DiscordNotificationConfig; message: string; - fetcher?: DiscordFetch; + fetcher: DiscordFetch; } const DISCORD_API_BASE_URL = "https://discord.com/api/v10"; diff --git a/plugin/types/ws.d.ts b/plugin/types/ws.d.ts new file mode 100644 index 0000000..b87451f --- /dev/null +++ b/plugin/types/ws.d.ts @@ -0,0 +1,21 @@ +declare module "ws" { + export type RawData = Buffer | ArrayBuffer | Buffer[] | string; + + export class WebSocket { + static readonly OPEN: number; + readonly readyState: number; + send(data: string): void; + close(code?: number, reason?: string): void; + on(event: "message", listener: (data: RawData) => void): this; + on(event: "close", listener: (code: number, reason: Buffer) => void): this; + on(event: "error", listener: (error: Error) => void): this; + } + + export class WebSocketServer { + constructor(options: { host?: string; port: number }); + on(event: "error", listener: (error: Error) => void): this; + on(event: "listening", listener: () => void): this; + on(event: "connection", listener: (ws: WebSocket, req: import("http").IncomingMessage) => void): this; + close(callback?: () => void): void; + } +} diff --git a/tests/state-recovery.test.ts b/tests/state-recovery.test.ts index 5bcdbdd..bfa03d7 100644 --- a/tests/state-recovery.test.ts +++ b/tests/state-recovery.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createYonexusServerRuntime } from "../plugin/core/runtime.js"; +import { createMockNotificationService } from "../plugin/notifications/discord.js"; import { createYonexusServerStore, loadServerStore, @@ -90,6 +91,7 @@ describe("YNX-1105e: Server state recovery", () => { }, store, transport: firstTransport.transport, + notificationService: createMockNotificationService(), now: () => now }); @@ -145,6 +147,7 @@ describe("YNX-1105e: Server state recovery", () => { }, store, transport: secondTransport.transport, + notificationService: createMockNotificationService(), now: () => now }); diff --git a/tsconfig.json b/tsconfig.json index cf17390..658f108 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "dist", - "rootDir": ".", + "rootDir": "..", "strict": true, "skipLibCheck": true, "esModuleInterop": true, @@ -15,7 +15,9 @@ }, "include": [ "plugin/**/*.ts", - "servers/**/*.ts" + "plugin/**/*.d.ts", + "servers/**/*.ts", + "../Yonexus.Protocol/src/**/*.ts" ], "exclude": [ "dist",